mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-02-01 17:01:11 +03:00
commit
f4c6e91eb4
17 changed files with 5643 additions and 1447 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -5,3 +5,5 @@ target
|
||||||
*.iml
|
*.iml
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
mwixnet-config.toml
|
3176
Cargo.lock
generated
3176
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -10,19 +10,35 @@ blake2 = { package = "blake2-rfc", version = "0.2"}
|
||||||
byteorder = "1"
|
byteorder = "1"
|
||||||
bytes = "0.5.6"
|
bytes = "0.5.6"
|
||||||
chacha20 = "0.8.1"
|
chacha20 = "0.8.1"
|
||||||
|
clap = { version = "2.33", features = ["yaml"] }
|
||||||
|
dirs = "2.0"
|
||||||
failure = "0.1.8"
|
failure = "0.1.8"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hmac = { version = "0.11.0", features = ["std"]}
|
hmac = { version = "0.12.0", features = ["std"]}
|
||||||
hyper = { version = "0.14", features = ["full"] }
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
itertools = { version = "0.10.3"}
|
||||||
jsonrpc-core = "18.0"
|
jsonrpc-core = "18.0"
|
||||||
jsonrpc-derive = "18.0"
|
jsonrpc-derive = "18.0"
|
||||||
jsonrpc-http-server = "18.0"
|
jsonrpc-http-server = "18.0"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
pbkdf2 = "0.8.0"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
|
ring = "0.16"
|
||||||
|
rpassword = "4.0"
|
||||||
serde = { version = "1", features= ["derive"]}
|
serde = { version = "1", features= ["derive"]}
|
||||||
serde_derive = "1"
|
serde_derive = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha2 = "0.9.8"
|
sha2 = "0.10.0"
|
||||||
|
thiserror = "1.0.31"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
toml = "0.5"
|
||||||
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
|
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
|
||||||
grin_util = "5"
|
grin_util = "5"
|
||||||
|
grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
|
||||||
|
grin_core = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
|
||||||
|
grin_chain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
|
||||||
|
grin_keychain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
|
||||||
|
grin_servers = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
|
||||||
|
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
|
||||||
|
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
|
||||||
|
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" }
|
26
README.md
26
README.md
|
@ -3,6 +3,21 @@ This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/
|
||||||
|
|
||||||
A set of n CoinSwap servers (node<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key.
|
A set of n CoinSwap servers (node<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
#### init-config
|
||||||
|
To setup a new server, run `mwixnet init-config`. Then enter a password for the server key when prompted.
|
||||||
|
|
||||||
|
This will generate a key for the server and then create a new config file named `mwixnet-config.toml` in the current working directory.
|
||||||
|
The configuration file will contain the private key of the server encrypted with the server password you provided.
|
||||||
|
|
||||||
|
**Back this config file up! It's the only copy of the server's private key!**
|
||||||
|
|
||||||
|
#### Wallet
|
||||||
|
A grin-wallet account must be created for receiving extra mwixnet fees. The wallet's owner API should be available (run `grin-wallet owner_api`).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
With your wallet and fully synced node both online and listening at the addresses configured, the mwixnet server can be started by running `mwixnet` and providing the server key password and wallet password when prompted.
|
||||||
|
|
||||||
### SWAP API
|
### SWAP API
|
||||||
The first CoinSwap server (n<sub>1</sub>) provides the `swap` API, publicly available for use by GRIN wallets.
|
The first CoinSwap server (n<sub>1</sub>) provides the `swap` API, publicly available for use by GRIN wallets.
|
||||||
|
|
||||||
|
@ -11,12 +26,13 @@ The first CoinSwap server (n<sub>1</sub>) provides the `swap` API, publicly avai
|
||||||
**params:**
|
**params:**
|
||||||
```
|
```
|
||||||
[{
|
[{
|
||||||
"comsig": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
|
"comsig": "0835f4b8b9cd286c9e35475f575c3e4ae71ceb4ff36598504662627afd628a17d6ba7dedb1aa4c47f0fabad026b76fc86d06f3bef8d0621b8ac4601d4b1b98401586ca3374a401508f32049212478ae91cfa474dfaa5ef2c3dd559d5a292e02334",
|
||||||
"msg": "00010203",
|
|
||||||
"onion": {
|
"onion": {
|
||||||
"commit": "0967593792bc958cd73848c0b948ecab2c6e996ab3c550d462fe41359e447b651f",
|
"commit": "099a8922343f242dd3da29935ba5bbc7e38bf68eccfb8c96aec87aec0535199139",
|
||||||
"data": ["3719e5fba260c71a5a4bcf9d9caa58cd5dc49531388782fae7699c6fa6b30b09fe42"],
|
"data":[
|
||||||
"pubkey": "020dd38a220280f14515f6901a3a366cb7b87630814e4b68b3189a32df964961e5"
|
"758bc7339edfe8a1f117ef2ef02de58b33d99e820ffd2c21a3d108f4cbdadfbcbc061b705fc351ae2fb34a855bb64180fe8b4913ea13b3bf826773e4b293166cdad1fdf59cadd7d33f73a8f3fc0f339a849f9c505c573d8cc2f006082d8f5bf2c2c84c18d5873a821d9f60bcdd44cf0566d04d761a1eda005cd19ab0b1c593da4656dd2a09322fe1d725f61e8855c927844ec9e80b9260a58012f7c519c5ca3d9b192ab1104d30ac09fb844bd7f692214e5cf0f6cdf1477c20708c2f3ecb098dce6be661907593918840b0fe8eb5043996dace45f2fb66e8f1e2a732035b4e6447b8904f313badebcba99f75c003e297d8dd815915f534dfa7ed416af0c415b60d2a0186752af6af33b781f31fdd3016aeee3bd2e47743fe2ce391b3354b9036b56ec38ed7539adafbc96bef1dbaf354a805b03ac0df7a0d32cff91716926bce68c8ccebb607340f2ffe09c08a9c9fd282ea19b33c69107ed5c54d4872eb0ed83c38d7e07606722069d7709fb914e1e02ea23323f3ae9252902dbfa6f15bd83a3f64587c9ae23aaf96b2a95e1341da12a6e423cf95375184752e10c1dd1a599db74ac0c3d74ec270c589f6a3bdd0877eb986d9a58a8548b917e22bfb93a4a06c36d7cad8d4a8791a8d1e1dc683429b440b136c43ad2f664dafc5156b808050a3c4d28771877d3f1d3a9daa2585eae259aaa64745c6cd260f577e538e27be3c985db41b7c456b63c5b18d7d17420a277d4abc04ae892ceb26940b09fb322445846c14898f5f59305490b1338c56384cd0c7bf5950a0a403aec4d2c2f5e2378b5eb7b1e7fcdbd8d6cc547f3b5a372b22e50e37d858bb197392a10fb9e6e292d6ed6bd8eab1fef7f2d069b6250a0e3e597ccf9a062e04b68821f5c57328ddab775d141147b71c1764c911bad03d8b88e2e62034bc899395514ecab4dec8ab341ba114f0a4e5d1dcfa182396c0e4826ddee187b07bb524dfeaa5297f7a5465f99eaaaa37f082c787b94811feb15b57d68369e6a7e3761d"
|
||||||
|
],
|
||||||
|
"pubkey": "033946e6a495e7278027b38be3d500cfc23d3e0836f1b7e24513841437f316ccb0"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
```
|
```
|
||||||
|
|
39
mwixnet.yml
Normal file
39
mwixnet.yml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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
|
||||||
|
- 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
|
||||||
|
- grin_node_secret_path:
|
||||||
|
help: Path to a file containing the secret for the GRIN node api
|
||||||
|
long: grin_node_secret_path
|
||||||
|
takes_value: true
|
||||||
|
- wallet_owner_url:
|
||||||
|
help: Api address of running wallet owner listener
|
||||||
|
short: l
|
||||||
|
long: wallet_owner_url
|
||||||
|
takes_value: true
|
||||||
|
- wallet_owner_secret_path:
|
||||||
|
help: Path to a file containing the secret for the wallet owner api
|
||||||
|
long: wallet_owner_secret_path
|
||||||
|
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
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
hard_tabs = true
|
||||||
|
edition = "2021"
|
265
src/config.rs
Normal file
265
src/config.rs
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
use crate::error::{self, Result};
|
||||||
|
use crate::secp::SecretKey;
|
||||||
|
|
||||||
|
use core::num::NonZeroU32;
|
||||||
|
use grin_util::{file, 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;
|
||||||
|
|
||||||
|
const GRIN_HOME: &str = ".grin";
|
||||||
|
const CHAIN_NAME: &str = "main";
|
||||||
|
const NODE_API_SECRET_FILE_NAME: &str = ".api_secret";
|
||||||
|
const WALLET_OWNER_API_SECRET_FILE_NAME: &str = ".owner_api_secret";
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
pub key: SecretKey,
|
||||||
|
/// interval (in seconds) to wait before each mixing round
|
||||||
|
pub interval_s: u32,
|
||||||
|
/// socket address the server listener should bind to
|
||||||
|
pub addr: SocketAddr,
|
||||||
|
/// foreign api address of the grin node
|
||||||
|
pub grin_node_url: SocketAddr,
|
||||||
|
/// path to file containing api secret for the grin node
|
||||||
|
pub grin_node_secret_path: Option<String>,
|
||||||
|
/// owner api address of the grin wallet
|
||||||
|
pub wallet_owner_url: SocketAddr,
|
||||||
|
/// path to file containing secret for the grin wallet's owner api
|
||||||
|
pub wallet_owner_secret_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerConfig {
|
||||||
|
pub fn node_api_secret(&self) -> Option<String> {
|
||||||
|
file::get_first_line(self.grin_node_secret_path.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wallet_owner_api_secret(&self) -> Option<String> {
|
||||||
|
file::get_first_line(self.wallet_owner_secret_path.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypted server key, for storing on disk and decrypting with a password.
|
||||||
|
/// Includes a salt used by key derivation and a nonce used when sealing the encrypted data.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
struct EncryptedServerKey {
|
||||||
|
encrypted_key: String,
|
||||||
|
salt: String,
|
||||||
|
nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncryptedServerKey {
|
||||||
|
/// Generates a random salt for pbkdf2 key derivation and a random nonce for aead sealing.
|
||||||
|
/// Then derives an encryption key from the password and salt. Finally, it encrypts and seals
|
||||||
|
/// the server key with chacha20-poly1305 using the derived key and random nonce.
|
||||||
|
pub fn from_secret_key(
|
||||||
|
server_key: &SecretKey,
|
||||||
|
password: &ZeroingString,
|
||||||
|
) -> Result<EncryptedServerKey> {
|
||||||
|
let salt: [u8; 8] = thread_rng().gen();
|
||||||
|
let password = password.as_bytes();
|
||||||
|
let mut key = [0; 32];
|
||||||
|
pbkdf2::derive(
|
||||||
|
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 nonce: [u8; 12] = thread_rng().gen();
|
||||||
|
let aad = aead::Aad::from(&[]);
|
||||||
|
let _ = sealing_key
|
||||||
|
.seal_in_place_append_tag(
|
||||||
|
aead::Nonce::assume_unique_for_key(nonce),
|
||||||
|
aad,
|
||||||
|
&mut enc_bytes,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
error::ErrorKind::SaveConfigError(format!(
|
||||||
|
"Failure while encrypting server key: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(EncryptedServerKey {
|
||||||
|
encrypted_key: enc_bytes.to_hex(),
|
||||||
|
salt: salt.to_hex(),
|
||||||
|
nonce: nonce.to_hex(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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("Seed not valid hex".to_string()))?;
|
||||||
|
let salt = grin_util::from_hex(&self.salt.clone())
|
||||||
|
.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("Nonce not valid hex".to_string()))?;
|
||||||
|
let password = password.as_bytes();
|
||||||
|
let mut key = [0; 32];
|
||||||
|
pbkdf2::derive(
|
||||||
|
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(|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();
|
||||||
|
let decrypted = SecretKey::from_slice(&secp, &encrypted_seed).map_err(|_| {
|
||||||
|
error::ErrorKind::LoadConfigError("Decrypted key not valid".to_string())
|
||||||
|
})?;
|
||||||
|
Ok(decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The config attributes saved to disk
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct RawConfig {
|
||||||
|
encrypted_key: String,
|
||||||
|
salt: String,
|
||||||
|
nonce: String,
|
||||||
|
interval_s: u32,
|
||||||
|
addr: SocketAddr,
|
||||||
|
grin_node_url: SocketAddr,
|
||||||
|
grin_node_secret_path: Option<String>,
|
||||||
|
wallet_owner_url: SocketAddr,
|
||||||
|
wallet_owner_secret_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
interval_s: server_config.interval_s,
|
||||||
|
addr: server_config.addr,
|
||||||
|
grin_node_url: server_config.grin_node_url,
|
||||||
|
grin_node_secret_path: server_config.grin_node_secret_path.clone(),
|
||||||
|
wallet_owner_url: server_config.wallet_owner_url,
|
||||||
|
wallet_owner_secret_path: server_config.wallet_owner_secret_path.clone(),
|
||||||
|
};
|
||||||
|
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(|e| {
|
||||||
|
error::ErrorKind::SaveConfigError(format!("Error while writing config to file: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the server config from the config_path given and decrypts it with the provided password.
|
||||||
|
pub fn load_config(config_path: &PathBuf, password: &ZeroingString) -> Result<ServerConfig> {
|
||||||
|
let contents = std::fs::read_to_string(config_path).map_err(|e| {
|
||||||
|
error::ErrorKind::LoadConfigError(format!(
|
||||||
|
"Unable to read server config. Perform init-config or pass in config path.\nError: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let raw_config: RawConfig =
|
||||||
|
toml::from_str(&contents).map_err(|e| error::ErrorKind::LoadConfigError(e.to_string()))?;
|
||||||
|
|
||||||
|
let encrypted_key = EncryptedServerKey {
|
||||||
|
encrypted_key: raw_config.encrypted_key,
|
||||||
|
salt: raw_config.salt,
|
||||||
|
nonce: raw_config.nonce,
|
||||||
|
};
|
||||||
|
let secret_key = encrypted_key.decrypt(&password)?;
|
||||||
|
|
||||||
|
Ok(ServerConfig {
|
||||||
|
key: secret_key,
|
||||||
|
interval_s: raw_config.interval_s,
|
||||||
|
addr: raw_config.addr,
|
||||||
|
grin_node_url: raw_config.grin_node_url,
|
||||||
|
grin_node_secret_path: raw_config.grin_node_secret_path,
|
||||||
|
wallet_owner_url: raw_config.wallet_owner_url,
|
||||||
|
wallet_owner_secret_path: raw_config.wallet_owner_secret_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_grin_path() -> PathBuf {
|
||||||
|
let mut grin_path = match dirs::home_dir() {
|
||||||
|
Some(p) => p,
|
||||||
|
None => PathBuf::new(),
|
||||||
|
};
|
||||||
|
grin_path.push(GRIN_HOME);
|
||||||
|
grin_path.push(CHAIN_NAME);
|
||||||
|
grin_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_secret_path() -> PathBuf {
|
||||||
|
let mut path = get_grin_path();
|
||||||
|
path.push(NODE_API_SECRET_FILE_NAME);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wallet_owner_secret_path() -> PathBuf {
|
||||||
|
let mut path = get_grin_path();
|
||||||
|
path.push(WALLET_OWNER_API_SECRET_FILE_NAME);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
}
|
111
src/error.rs
111
src/error.rs
|
@ -1,4 +1,5 @@
|
||||||
use failure::{self, Context, Fail};
|
use failure::{self, Context, Fail};
|
||||||
|
use grin_wallet_libwallet as libwallet;
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
@ -9,13 +10,11 @@ pub struct Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
pub type StdResult<T, E> = std::result::Result<T, E>;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
|
|
||||||
/// MWixnet error types
|
/// MWixnet error types
|
||||||
|
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
/// Unsupported payload version
|
|
||||||
#[fail(display = "Unsupported Payload Version")]
|
|
||||||
UnsupportedPayload,
|
|
||||||
/// Error from secp256k1-zkp library
|
/// Error from secp256k1-zkp library
|
||||||
#[fail(display = "Secp Error")]
|
#[fail(display = "Secp Error")]
|
||||||
SecpError,
|
SecpError,
|
||||||
|
@ -23,33 +22,35 @@ pub enum ErrorKind {
|
||||||
#[fail(display = "InvalidKeyLength")]
|
#[fail(display = "InvalidKeyLength")]
|
||||||
InvalidKeyLength,
|
InvalidKeyLength,
|
||||||
/// Wraps an io error produced when reading or writing
|
/// Wraps an io error produced when reading or writing
|
||||||
#[fail(display = "IOError")]
|
#[fail(display = "IO Error: {}", _0)]
|
||||||
IOErr(
|
IOErr(String, io::ErrorKind),
|
||||||
String,
|
|
||||||
io::ErrorKind,
|
|
||||||
),
|
|
||||||
/// Expected a given value that wasn't found
|
|
||||||
#[fail(display = "UnexpectedData")]
|
|
||||||
UnexpectedData {
|
|
||||||
/// What we wanted
|
|
||||||
expected: Vec<u8>,
|
|
||||||
/// What we got
|
|
||||||
received: Vec<u8>,
|
|
||||||
},
|
|
||||||
/// Data wasn't in a consumable format
|
/// Data wasn't in a consumable format
|
||||||
#[fail(display = "CorruptedData")]
|
#[fail(display = "CorruptedData")]
|
||||||
CorruptedData,
|
CorruptedData,
|
||||||
/// Incorrect number of elements (when deserializing a vec via read_multi say).
|
/// Error from grin's api crate
|
||||||
#[fail(display = "CountError")]
|
#[fail(display = "GRIN API Error: {}", _0)]
|
||||||
CountError,
|
GrinApiError(String),
|
||||||
/// When asked to read too much data
|
/// Error from grin core
|
||||||
#[fail(display = "TooLargeReadErr")]
|
#[fail(display = "GRIN Core Error: {}", _0)]
|
||||||
TooLargeReadErr,
|
GrinCoreError(String),
|
||||||
|
/// Error from grin-wallet's libwallet
|
||||||
|
#[fail(display = "libwallet error: {}", _0)]
|
||||||
|
LibWalletError(String),
|
||||||
|
/// Error from serde-json
|
||||||
|
#[fail(display = "serde json error: {}", _0)]
|
||||||
|
SerdeJsonError(String),
|
||||||
|
/// Error from invalid signature
|
||||||
|
#[fail(display = "invalid signature")]
|
||||||
|
InvalidSigError,
|
||||||
|
/// Error while saving config
|
||||||
|
#[fail(display = "save config error: {}", _0)]
|
||||||
|
SaveConfigError(String),
|
||||||
|
/// Error while loading config
|
||||||
|
#[fail(display = "load config error: {}", _0)]
|
||||||
|
LoadConfigError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for Error {
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(e: io::Error) -> Error {
|
fn from(e: io::Error) -> Error {
|
||||||
|
@ -63,6 +64,14 @@ impl From<io::ErrorKind> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<grin_core::ser::Error> for Error {
|
||||||
|
fn from(e: grin_core::ser::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
Display::fmt(&self.inner, f)
|
Display::fmt(&self.inner, f)
|
||||||
|
@ -70,8 +79,10 @@ impl Display for Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn kind(&self) -> ErrorKind {
|
pub fn new(kind: ErrorKind) -> Error {
|
||||||
self.inner.get_context().clone()
|
Error {
|
||||||
|
inner: Context::new(kind),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn message(&self) -> String {
|
pub fn message(&self) -> String {
|
||||||
|
@ -101,10 +112,50 @@ impl From<secp256k1zkp::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<hmac::crypto_mac::InvalidKeyLength> for Error {
|
impl From<hmac::digest::InvalidLength> for Error {
|
||||||
fn from(_error: hmac::crypto_mac::InvalidKeyLength) -> Error {
|
fn from(_error: hmac::digest::InvalidLength) -> Error {
|
||||||
Error {
|
Error {
|
||||||
inner: Context::new(ErrorKind::InvalidKeyLength),
|
inner: Context::new(ErrorKind::InvalidKeyLength),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<grin_api::Error> for Error {
|
||||||
|
fn from(e: grin_api::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<grin_api::json_rpc::Error> for Error {
|
||||||
|
fn from(e: grin_api::json_rpc::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<grin_core::core::transaction::Error> for Error {
|
||||||
|
fn from(e: grin_core::core::transaction::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<libwallet::Error> for Error {
|
||||||
|
fn from(e: libwallet::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::LibWalletError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(e: serde_json::Error) -> Error {
|
||||||
|
Error {
|
||||||
|
inner: Context::new(ErrorKind::SerdeJsonError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
189
src/main.rs
189
src/main.rs
|
@ -1,29 +1,190 @@
|
||||||
use server::ServerConfig;
|
use config::ServerConfig;
|
||||||
|
use node::HttpGrinNode;
|
||||||
|
use wallet::HttpWallet;
|
||||||
|
|
||||||
|
use clap::App;
|
||||||
|
use grin_util::{StopState, ZeroingString};
|
||||||
|
use rpassword;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate clap;
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod node;
|
||||||
mod onion;
|
mod onion;
|
||||||
|
mod rpc;
|
||||||
mod secp;
|
mod secp;
|
||||||
mod ser;
|
|
||||||
mod server;
|
mod server;
|
||||||
mod types;
|
mod types;
|
||||||
|
mod wallet;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
|
||||||
let secret_key = secp::insecure_rand_secret()?; // todo - load from encrypted key file
|
|
||||||
let server_config = ServerConfig {
|
fn main() {
|
||||||
key: secret_key,
|
real_main().unwrap();
|
||||||
addr: "127.0.0.1:3000".parse().unwrap(),
|
std::process::exit(0);
|
||||||
is_first: true
|
}
|
||||||
|
|
||||||
|
fn real_main() -> 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 grin_path = config::get_grin_path();
|
||||||
|
grin_path.push("mwixnet-config.toml");
|
||||||
|
grin_path
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let shutdown_signal = async move {
|
let round_time = args
|
||||||
// Wait for the CTRL+C signal
|
.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 grin_node_secret_path = args.value_of("grin_node_secret_path");
|
||||||
|
let wallet_owner_url = args.value_of("wallet_owner_url");
|
||||||
|
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
|
||||||
|
addr: bind_addr.unwrap_or("0.0.0.0:3000").parse()?,
|
||||||
|
grin_node_url: grin_node_url.unwrap_or("127.0.0.1:3413").parse()?,
|
||||||
|
grin_node_secret_path: match grin_node_secret_path {
|
||||||
|
Some(p) => Some(p.to_owned()),
|
||||||
|
None => config::node_secret_path().to_str().map(|p| p.to_owned()),
|
||||||
|
},
|
||||||
|
wallet_owner_url: wallet_owner_url.unwrap_or("127.0.0.1:3420").parse()?,
|
||||||
|
wallet_owner_secret_path: match wallet_owner_secret_path {
|
||||||
|
Some(p) => Some(p.to_owned()),
|
||||||
|
None => config::wallet_owner_secret_path()
|
||||||
|
.to_str()
|
||||||
|
.map(|p| p.to_owned()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = prompt_password_confirm();
|
||||||
|
config::write_config(&config_path, &server_config, &password)?;
|
||||||
|
println!(
|
||||||
|
"Config file written to {:?}. Please back this file up in a safe place.",
|
||||||
|
config_path
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = prompt_password();
|
||||||
|
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 grin_node_secret_path, if supplied
|
||||||
|
if let Some(grin_node_secret_path) = grin_node_secret_path {
|
||||||
|
server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override wallet_owner_url, if supplied
|
||||||
|
if let Some(wallet_owner_url) = wallet_owner_url {
|
||||||
|
server_config.wallet_owner_url = wallet_owner_url.parse()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override wallet_owner_secret_path, if supplied
|
||||||
|
if let Some(wallet_owner_secret_path) = wallet_owner_secret_path {
|
||||||
|
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open wallet
|
||||||
|
let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
|
||||||
|
let wallet = HttpWallet::open_wallet(
|
||||||
|
&server_config.wallet_owner_url,
|
||||||
|
&server_config.wallet_owner_api_secret(),
|
||||||
|
&wallet_pass,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create GrinNode
|
||||||
|
let node = HttpGrinNode::new(
|
||||||
|
&server_config.grin_node_url,
|
||||||
|
&server_config.node_api_secret(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let stop_state = Arc::new(StopState::new());
|
||||||
|
let stop_state_clone = stop_state.clone();
|
||||||
|
|
||||||
|
let rt = Runtime::new()?;
|
||||||
|
rt.spawn(async move {
|
||||||
|
futures::executor::block_on(build_signals_fut());
|
||||||
|
stop_state_clone.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the mwixnet JSON-RPC HTTP server
|
||||||
|
rpc::listen(server_config, Arc::new(wallet), Arc::new(node), stop_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_signals_fut() {
|
||||||
|
if cfg!(unix) {
|
||||||
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
|
|
||||||
|
// Listen for SIGINT, SIGQUIT, and SIGTERM
|
||||||
|
let mut terminate_signal =
|
||||||
|
signal(SignalKind::terminate()).expect("failed to create terminate signal");
|
||||||
|
let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal");
|
||||||
|
let mut interrupt_signal =
|
||||||
|
signal(SignalKind::interrupt()).expect("failed to create interrupt signal");
|
||||||
|
|
||||||
|
futures::future::select_all(vec![
|
||||||
|
Box::pin(terminate_signal.recv()),
|
||||||
|
Box::pin(quit_signal.recv()),
|
||||||
|
Box::pin(interrupt_signal.recv()),
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
tokio::signal::ctrl_c()
|
tokio::signal::ctrl_c()
|
||||||
.await
|
.await
|
||||||
.expect("failed to install CTRL+C signal handler");
|
.expect("failed to install CTRL+C signal handler");
|
||||||
};
|
}
|
||||||
|
}
|
||||||
server::listen(&server_config, shutdown_signal)
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
219
src/node.rs
Normal file
219
src/node.rs
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
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::{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;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub trait GrinNode: Send + Sync {
|
||||||
|
/// Retrieves the unspent output with a matching commitment
|
||||||
|
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>>;
|
||||||
|
|
||||||
|
/// Gets the height of the chain tip
|
||||||
|
fn get_chain_height(&self) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Posts a transaction to the grin node
|
||||||
|
fn post_tx(&self, tx: &Transaction) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a commitment is in the UTXO set
|
||||||
|
pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool> {
|
||||||
|
let utxo = node.get_utxo(&commit)?;
|
||||||
|
Ok(utxo.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a commitment is spendable at the block height provided
|
||||||
|
pub fn is_spendable(
|
||||||
|
node: &Arc<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 {
|
||||||
|
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(node: &Arc<dyn GrinNode>, output_commit: &Commitment) -> Result<Option<Input>> {
|
||||||
|
let output = node.get_utxo(&output_commit)?;
|
||||||
|
|
||||||
|
if let Some(out) = output {
|
||||||
|
let features = match out.output_type {
|
||||||
|
OutputType::Coinbase => OutputFeatures::Coinbase,
|
||||||
|
OutputType::Transaction => OutputFeatures::Plain,
|
||||||
|
};
|
||||||
|
|
||||||
|
let input = Input::new(features, out.commit);
|
||||||
|
return Ok(Some(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HttpGrinNode {
|
||||||
|
node_url: SocketAddr,
|
||||||
|
node_api_secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENDPOINT: &str = "/v2/foreign";
|
||||||
|
|
||||||
|
impl HttpGrinNode {
|
||||||
|
pub fn new(node_url: &SocketAddr, node_api_secret: &Option<String>) -> HttpGrinNode {
|
||||||
|
HttpGrinNode {
|
||||||
|
node_url: node_url.to_owned(),
|
||||||
|
node_api_secret: node_api_secret.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(outputs[0].clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chain_height(&self) -> Result<u64> {
|
||||||
|
let params = json!([]);
|
||||||
|
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(|e| ErrorKind::SerdeJsonError(e.to_string()).into());
|
||||||
|
|
||||||
|
Ok(tip?.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_tx(&self, tx: &Transaction) -> Result<()> {
|
||||||
|
let params = json!([tx, true]);
|
||||||
|
self.send_json_request::<serde_json::Value>("push_transaction", ¶ms)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock {
|
||||||
|
use super::GrinNode;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::secp::Commitment;
|
||||||
|
|
||||||
|
use grin_api::{OutputPrintable, OutputType};
|
||||||
|
use grin_core::core::Transaction;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
/// 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>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockGrinNode {
|
||||||
|
pub fn new() -> MockGrinNode {
|
||||||
|
MockGrinNode {
|
||||||
|
utxos: HashMap::new(),
|
||||||
|
txns_posted: RwLock::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) {
|
||||||
|
self.utxos.insert(output_commit.clone(), utxo.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_default_utxo(&mut self, output_commit: &Commitment) {
|
||||||
|
let utxo = OutputPrintable {
|
||||||
|
output_type: OutputType::Transaction,
|
||||||
|
commit: output_commit.to_owned(),
|
||||||
|
spent: false,
|
||||||
|
proof: None,
|
||||||
|
proof_hash: String::from(""),
|
||||||
|
block_height: None,
|
||||||
|
merkle_proof: None,
|
||||||
|
mmr_index: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_utxo(&output_commit, &utxo);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posted_txns(&self) -> Vec<Transaction> {
|
||||||
|
let read = self.txns_posted.read().unwrap();
|
||||||
|
read.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrinNode for MockGrinNode {
|
||||||
|
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> {
|
||||||
|
if let Some(utxo) = self.utxos.get(&output_commit) {
|
||||||
|
return Ok(Some(utxo.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chain_height(&self) -> Result<u64> {
|
||||||
|
Ok(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_tx(&self, tx: &Transaction) -> Result<()> {
|
||||||
|
let mut write = self.txns_posted.write().unwrap();
|
||||||
|
write.push(tx.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
323
src/onion.rs
323
src/onion.rs
|
@ -1,60 +1,52 @@
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
|
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
|
||||||
use crate::types::{Hop, Onion, RawBytes, Payload, deserialize_payload, serialize_payload};
|
use crate::types::Payload;
|
||||||
use crate::ser;
|
|
||||||
|
|
||||||
use chacha20::{ChaCha20, Key, Nonce};
|
|
||||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use chacha20::{ChaCha20, Key, Nonce};
|
||||||
|
use grin_core::ser::{self, ProtocolVersion, Writeable, Writer};
|
||||||
|
use grin_util::{self, ToHex};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
type RawBytes = Vec<u8>;
|
||||||
|
|
||||||
/// Create an Onion for the Commitment, encrypting the payload for each hop
|
/// A data packet with layers of encryption
|
||||||
pub fn create_onion(commitment: &Commitment, session_key: &SecretKey, hops: &Vec<Hop>) -> Result<Onion> {
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
let secp = Secp256k1::new();
|
pub struct Onion {
|
||||||
let mut ephemeral_key = session_key.clone();
|
/// The onion originator's portion of the shared secret
|
||||||
|
pub ephemeral_pubkey: PublicKey,
|
||||||
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
|
/// The pedersen commitment before adjusting the excess and subtracting the fee
|
||||||
let mut enc_payloads: Vec<RawBytes> = Vec::new();
|
pub commit: Commitment,
|
||||||
for hop in hops {
|
/// The encrypted payloads which represent the layers of the onion
|
||||||
let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key);
|
pub enc_payloads: Vec<RawBytes>,
|
||||||
|
|
||||||
let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?;
|
|
||||||
let blinding_factor = calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
|
|
||||||
|
|
||||||
shared_secrets.push(shared_secret);
|
|
||||||
enc_payloads.push(serialize_payload(&hop.payload)?);
|
|
||||||
ephemeral_key.mul_assign(&secp, &blinding_factor)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in (0..shared_secrets.len()).rev() {
|
impl Onion {
|
||||||
let mut cipher = new_stream_cipher(&shared_secrets[i])?;
|
pub fn serialize(&self) -> Result<Vec<u8>> {
|
||||||
for j in i..shared_secrets.len() {
|
let mut vec = vec![];
|
||||||
cipher.apply_keystream(&mut enc_payloads[j]);
|
ser::serialize_default(&mut vec, &self)?;
|
||||||
}
|
Ok(vec)
|
||||||
}
|
|
||||||
|
|
||||||
let onion = Onion{
|
|
||||||
ephemeral_pubkey: secp::to_public_key(&session_key)?,
|
|
||||||
commit: commitment.clone(),
|
|
||||||
enc_payloads: enc_payloads,
|
|
||||||
};
|
|
||||||
Ok(onion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
|
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
|
||||||
pub fn peel_layer(onion: &Onion, secret_key: &SecretKey) -> Result<(Payload, Onion)> {
|
pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion)> {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let shared_secret = SharedSecret::new(&secp, &onion.ephemeral_pubkey, &secret_key);
|
let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key);
|
||||||
let mut cipher = new_stream_cipher(&shared_secret)?;
|
let mut cipher = new_stream_cipher(&shared_secret)?;
|
||||||
|
|
||||||
let mut decrypted_bytes = onion.enc_payloads[0].clone();
|
let mut decrypted_bytes = self.enc_payloads[0].clone();
|
||||||
cipher.apply_keystream(&mut decrypted_bytes);
|
cipher.apply_keystream(&mut decrypted_bytes);
|
||||||
let decrypted_payload = deserialize_payload(&decrypted_bytes)?;
|
let decrypted_payload = Payload::deserialize(&decrypted_bytes)?;
|
||||||
|
|
||||||
let enc_payloads : Vec<RawBytes> = onion.enc_payloads.iter()
|
let enc_payloads: Vec<RawBytes> = self
|
||||||
|
.enc_payloads
|
||||||
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|&(i, _)| i != 0)
|
.filter(|&(i, _)| i != 0)
|
||||||
.map(|(_, enc_payload)| {
|
.map(|(_, enc_payload)| {
|
||||||
|
@ -64,24 +56,29 @@ pub fn peel_layer(onion: &Onion, secret_key: &SecretKey) -> Result<(Payload, Oni
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let blinding_factor = calc_blinding_factor(&shared_secret, &onion.ephemeral_pubkey)?;
|
let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?;
|
||||||
|
|
||||||
let mut ephemeral_pubkey = onion.ephemeral_pubkey.clone();
|
let mut ephemeral_pubkey = self.ephemeral_pubkey.clone();
|
||||||
ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?;
|
ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?;
|
||||||
|
|
||||||
let mut commitment = onion.commit.clone();
|
let mut commitment = self.commit.clone();
|
||||||
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?;
|
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?;
|
||||||
|
commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?;
|
||||||
|
|
||||||
let peeled_onion = Onion {
|
let peeled_onion = Onion {
|
||||||
ephemeral_pubkey: ephemeral_pubkey,
|
ephemeral_pubkey,
|
||||||
commit: commitment.clone(),
|
commit: commitment.clone(),
|
||||||
enc_payloads: enc_payloads,
|
enc_payloads,
|
||||||
};
|
};
|
||||||
Ok((decrypted_payload, peeled_onion))
|
Ok((decrypted_payload, peeled_onion))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn calc_blinding_factor(shared_secret: &SharedSecret, ephemeral_pubkey: &PublicKey) -> Result<SecretKey> {
|
fn calc_blinding_factor(
|
||||||
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey)?;
|
shared_secret: &SharedSecret,
|
||||||
|
ephemeral_pubkey: &PublicKey,
|
||||||
|
) -> Result<SecretKey> {
|
||||||
|
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?;
|
||||||
|
|
||||||
let mut hasher = Sha256::default();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(&serialized_pubkey);
|
hasher.update(&serialized_pubkey);
|
||||||
|
@ -103,65 +100,249 @@ fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20> {
|
||||||
Ok(ChaCha20::new(&key, &nonce))
|
Ok(ChaCha20::new(&key, &nonce))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Writeable for Onion {
|
||||||
|
fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> {
|
||||||
|
self.ephemeral_pubkey.write(writer)?;
|
||||||
|
writer.write_fixed_bytes(&self.commit)?;
|
||||||
|
writer.write_u64(self.enc_payloads.len() as u64)?;
|
||||||
|
for p in &self.enc_payloads {
|
||||||
|
writer.write_u64(p.len() as u64)?;
|
||||||
|
p.write(writer)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::ser::Serialize for Onion {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("Onion", 3)?;
|
||||||
|
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
state.serialize_field(
|
||||||
|
"pubkey",
|
||||||
|
&self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex(),
|
||||||
|
)?;
|
||||||
|
state.serialize_field("commit", &self.commit.to_hex())?;
|
||||||
|
|
||||||
|
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
|
||||||
|
state.serialize_field("data", &hex_payloads)?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::de::Deserialize<'de> for Onion {
|
||||||
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "snake_case")]
|
||||||
|
enum Field {
|
||||||
|
Pubkey,
|
||||||
|
Commit,
|
||||||
|
Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OnionVisitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for OnionVisitor {
|
||||||
|
type Value = Onion;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str("an Onion")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut pubkey = None;
|
||||||
|
let mut commit = None;
|
||||||
|
let mut data = None;
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::Pubkey => {
|
||||||
|
let val: String = map.next_value()?;
|
||||||
|
let vec =
|
||||||
|
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
pubkey = Some(
|
||||||
|
PublicKey::from_slice(&secp, &vec[..])
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Field::Commit => {
|
||||||
|
let val: String = map.next_value()?;
|
||||||
|
let vec =
|
||||||
|
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||||
|
commit = Some(Commitment::from_vec(vec));
|
||||||
|
}
|
||||||
|
Field::Data => {
|
||||||
|
let val: Vec<String> = map.next_value()?;
|
||||||
|
let mut vec: Vec<Vec<u8>> = Vec::new();
|
||||||
|
for hex in val {
|
||||||
|
vec.push(
|
||||||
|
grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
data = Some(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Onion {
|
||||||
|
ephemeral_pubkey: pubkey.unwrap(),
|
||||||
|
commit: commit.unwrap(),
|
||||||
|
enc_payloads: data.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELDS: &[&str] = &["pubkey", "commit", "data"];
|
||||||
|
deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
pub mod test_util {
|
||||||
use super::super::secp;
|
use super::{Onion, RawBytes};
|
||||||
use super::super::types;
|
use crate::error::Result;
|
||||||
use super::super::onion;
|
use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
|
||||||
|
use crate::types::Payload;
|
||||||
|
|
||||||
|
use crate::secp;
|
||||||
|
use chacha20::cipher::StreamCipher;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Hop {
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
pub payload: Payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an Onion for the Commitment, encrypting the payload for each hop
|
||||||
|
pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion> {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
let session_key = secp::random_secret();
|
||||||
|
let mut ephemeral_key = session_key.clone();
|
||||||
|
|
||||||
|
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
|
||||||
|
let mut enc_payloads: Vec<RawBytes> = Vec::new();
|
||||||
|
for hop in hops {
|
||||||
|
let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key);
|
||||||
|
|
||||||
|
let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?;
|
||||||
|
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
|
||||||
|
|
||||||
|
shared_secrets.push(shared_secret);
|
||||||
|
enc_payloads.push(hop.payload.serialize()?);
|
||||||
|
ephemeral_key.mul_assign(&secp, &blinding_factor)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in (0..shared_secrets.len()).rev() {
|
||||||
|
let mut cipher = super::new_stream_cipher(&shared_secrets[i])?;
|
||||||
|
for j in i..shared_secrets.len() {
|
||||||
|
cipher.apply_keystream(&mut enc_payloads[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let onion = Onion {
|
||||||
|
ephemeral_pubkey: PublicKey::from_secret_key(&secp, &session_key)?,
|
||||||
|
commit: commitment.clone(),
|
||||||
|
enc_payloads,
|
||||||
|
};
|
||||||
|
Ok(onion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the expected next ephemeral pubkey after peeling a layer off of the Onion.
|
||||||
|
pub fn next_ephemeral_pubkey(onion: &Onion, server_key: &SecretKey) -> Result<PublicKey> {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
let mut ephemeral_pubkey = onion.ephemeral_pubkey.clone();
|
||||||
|
let shared_secret = SharedSecret::new(&secp, &ephemeral_pubkey, &server_key);
|
||||||
|
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
|
||||||
|
ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?;
|
||||||
|
Ok(ephemeral_pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::test_util::{self, Hop};
|
||||||
|
use crate::secp;
|
||||||
|
use crate::types::Payload;
|
||||||
|
|
||||||
|
use grin_core::core::FeeFields;
|
||||||
|
|
||||||
/// Test end-to-end Onion creation and unwrapping logic.
|
/// Test end-to-end Onion creation and unwrapping logic.
|
||||||
#[test]
|
#[test]
|
||||||
fn onion() {
|
fn onion() {
|
||||||
let value : u64 = 1000;
|
let total_fee: u64 = 10;
|
||||||
let blind = secp::insecure_rand_secret().unwrap();
|
let fee_per_hop: u64 = 2;
|
||||||
let commitment = secp::commit(value, &blind).unwrap();
|
let in_value: u64 = 1000;
|
||||||
|
let out_value: u64 = in_value - total_fee;
|
||||||
let session_key = secp::insecure_rand_secret().unwrap();
|
let blind = secp::random_secret();
|
||||||
let mut hops : Vec<types::Hop> = Vec::new();
|
let commitment = secp::commit(in_value, &blind).unwrap();
|
||||||
|
|
||||||
|
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 = commitment.clone();
|
let mut final_commit = secp::commit(out_value, &blind).unwrap();
|
||||||
let mut final_blind = blind.clone();
|
let mut final_blind = blind.clone();
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
keys.push(secp::insecure_rand_secret().unwrap());
|
keys.push(secp::random_secret());
|
||||||
|
|
||||||
let excess = secp::insecure_rand_secret().unwrap();
|
let excess = secp::random_secret();
|
||||||
|
|
||||||
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
||||||
final_blind.add_assign(&secp, &excess).unwrap();
|
final_blind.add_assign(&secp, &excess).unwrap();
|
||||||
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
|
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
|
||||||
let proof = if i == 4 {
|
let proof = if i == 4 {
|
||||||
let n1 = secp::insecure_rand_secret().unwrap();
|
let n1 = secp::random_secret();
|
||||||
let rp = secp.bullet_proof(value, final_blind.clone(), n1.clone(), n1.clone(), None, None);
|
let rp = secp.bullet_proof(
|
||||||
|
out_value,
|
||||||
|
final_blind.clone(),
|
||||||
|
n1.clone(),
|
||||||
|
n1.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
|
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
|
||||||
Some(rp)
|
Some(rp)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
hops.push(types::Hop{
|
hops.push(Hop {
|
||||||
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
|
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
|
||||||
payload: types::Payload{
|
payload: Payload {
|
||||||
excess: excess,
|
excess,
|
||||||
|
fee: FeeFields::from(fee_per_hop as u32),
|
||||||
rangeproof: proof,
|
rangeproof: proof,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut onion_packet = onion::create_onion(&commitment, &session_key, &hops).unwrap();
|
let mut onion_packet = test_util::create_onion(&commitment, &hops).unwrap();
|
||||||
|
|
||||||
let mut payload = types::Payload{
|
let mut payload = Payload {
|
||||||
excess: secp::insecure_rand_secret().unwrap(),
|
excess: secp::random_secret(),
|
||||||
rangeproof: None
|
fee: FeeFields::from(fee_per_hop as u32),
|
||||||
|
rangeproof: None,
|
||||||
};
|
};
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
let peeled = onion::peel_layer(&onion_packet, &keys[i]).unwrap();
|
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
|
||||||
payload = peeled.0;
|
payload = peeled.0;
|
||||||
onion_packet = peeled.1;
|
onion_packet = peeled.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(payload.rangeproof.is_some());
|
assert!(payload.rangeproof.is_some());
|
||||||
assert_eq!(payload.rangeproof.unwrap(), hops[4].payload.rangeproof.unwrap());
|
assert_eq!(
|
||||||
assert_eq!(secp::commit(value, &final_blind).unwrap(), final_commit);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
271
src/rpc.rs
Normal file
271
src/rpc.rs
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
use crate::config::ServerConfig;
|
||||||
|
use crate::node::GrinNode;
|
||||||
|
use crate::onion::Onion;
|
||||||
|
use crate::secp::{self, ComSignature};
|
||||||
|
use crate::server::{Server, ServerImpl, SwapError};
|
||||||
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
|
use grin_util::StopState;
|
||||||
|
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::sync::{Arc, Mutex};
|
||||||
|
use std::thread::{sleep, spawn};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SwapReq {
|
||||||
|
onion: Onion,
|
||||||
|
#[serde(with = "secp::comsig_serde")]
|
||||||
|
comsig: ComSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rpc(server)]
|
||||||
|
pub trait API {
|
||||||
|
#[rpc(name = "swap")]
|
||||||
|
fn swap(&self, swap: SwapReq) -> Result<Value>;
|
||||||
|
|
||||||
|
// 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)]
|
||||||
|
struct RPCServer {
|
||||||
|
server_config: ServerConfig,
|
||||||
|
server: Arc<Mutex<dyn Server>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RPCServer {
|
||||||
|
/// Spin up an instance of the JSON-RPC HTTP server.
|
||||||
|
fn start_http(&self) -> jsonrpc_http_server::Server {
|
||||||
|
let mut io = IoHandler::new();
|
||||||
|
io.extend_with(RPCServer::to_delegate(self.clone()));
|
||||||
|
|
||||||
|
ServerBuilder::new(io)
|
||||||
|
.cors(DomainsValidation::Disabled)
|
||||||
|
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
||||||
|
if request.uri() == "/v1" {
|
||||||
|
request.into()
|
||||||
|
} else {
|
||||||
|
jsonrpc_http_server::Response::bad_request("Only v1 supported").into()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start_http(&self.server_config.addr)
|
||||||
|
.expect("Unable to start RPC server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwapError> for Error {
|
||||||
|
fn from(e: SwapError) -> Self {
|
||||||
|
match e {
|
||||||
|
SwapError::UnknownError(_) => Error {
|
||||||
|
message: e.to_string(),
|
||||||
|
code: ErrorCode::InternalError,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
_ => Error::invalid_params(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl API for RPCServer {
|
||||||
|
/// Implements the 'swap' API
|
||||||
|
fn swap(&self, swap: SwapReq) -> Result<Value> {
|
||||||
|
self.server
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.swap(&swap.onion, &swap.comsig)?;
|
||||||
|
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 = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone());
|
||||||
|
let server = Arc::new(Mutex::new(server));
|
||||||
|
|
||||||
|
let rpc_server = RPCServer {
|
||||||
|
server_config: server_config.clone(),
|
||||||
|
server: server.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let http_server = rpc_server.start_http();
|
||||||
|
println!("Server listening on {}", server_config.addr);
|
||||||
|
|
||||||
|
let close_handle = http_server.close_handle();
|
||||||
|
let round_handle = spawn(move || {
|
||||||
|
let mut secs = 0;
|
||||||
|
loop {
|
||||||
|
if stop_state.is_stopped() {
|
||||||
|
close_handle.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
secs = (secs + 1) % server_config.interval_s;
|
||||||
|
|
||||||
|
if secs == 0 {
|
||||||
|
let _ = server.lock().unwrap().execute_round();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
http_server.wait();
|
||||||
|
round_handle.join().unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::config::ServerConfig;
|
||||||
|
use crate::onion::test_util;
|
||||||
|
use crate::rpc::{RPCServer, SwapReq};
|
||||||
|
use crate::secp::{self, ComSignature};
|
||||||
|
use crate::server::mock::MockServer;
|
||||||
|
use crate::server::{Server, SwapError};
|
||||||
|
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use hyper::{Body, Client, Request, Response};
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
async fn body_to_string(req: Response<Body>) -> String {
|
||||||
|
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap();
|
||||||
|
String::from_utf8(body_bytes.to_vec()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spin up a temporary web service, query the API, then cleanup and return response
|
||||||
|
fn make_request(
|
||||||
|
server: Arc<Mutex<dyn Server>>,
|
||||||
|
req: String,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let server_config = ServerConfig {
|
||||||
|
key: secp::random_secret(),
|
||||||
|
interval_s: 1,
|
||||||
|
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||||
|
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||||
|
grin_node_secret_path: None,
|
||||||
|
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||||
|
wallet_owner_secret_path: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rpc_server = RPCServer {
|
||||||
|
server_config: server_config.clone(),
|
||||||
|
server: server.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the JSON-RPC server
|
||||||
|
let http_server = rpc_server.start_http();
|
||||||
|
|
||||||
|
let uri = format!("http://{}/v1", server_config.addr);
|
||||||
|
|
||||||
|
let threaded_rt = Runtime::new()?;
|
||||||
|
let do_request = async move {
|
||||||
|
let request = Request::post(uri)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(Body::from(req))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Client::new().request(request).await
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = threaded_rt.block_on(do_request)?;
|
||||||
|
let response_str: String = threaded_rt.block_on(body_to_string(response));
|
||||||
|
|
||||||
|
// Wait for shutdown
|
||||||
|
threaded_rt.shutdown_background();
|
||||||
|
|
||||||
|
// Execute one round
|
||||||
|
server.lock().unwrap().execute_round()?;
|
||||||
|
|
||||||
|
// Stop the server
|
||||||
|
http_server.close();
|
||||||
|
|
||||||
|
Ok(response_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: Test all error types
|
||||||
|
|
||||||
|
/// Demonstrates a successful swap response
|
||||||
|
#[test]
|
||||||
|
fn swap_success() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||||
|
let onion = test_util::create_onion(&commitment, &vec![])?;
|
||||||
|
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||||
|
let swap = SwapReq {
|
||||||
|
onion: onion.clone(),
|
||||||
|
comsig,
|
||||||
|
};
|
||||||
|
|
||||||
|
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new()));
|
||||||
|
|
||||||
|
let req = format!(
|
||||||
|
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||||
|
serde_json::json!(swap)
|
||||||
|
);
|
||||||
|
println!("Request: {}", req);
|
||||||
|
let response = make_request(server, req)?;
|
||||||
|
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
|
||||||
|
assert_eq!(response, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new()));
|
||||||
|
|
||||||
|
let params = "{ \"param\": \"Not a valid Swap request\" }";
|
||||||
|
let req = format!(
|
||||||
|
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||||
|
params
|
||||||
|
);
|
||||||
|
let response = make_request(server, req)?;
|
||||||
|
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
|
||||||
|
assert_eq!(response, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns "Commitment not found" when there's no matching output in the UTXO set.
|
||||||
|
#[test]
|
||||||
|
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let commitment = secp::commit(1234, &secp::random_secret())?;
|
||||||
|
let onion = test_util::create_onion(&commitment, &vec![])?;
|
||||||
|
let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?;
|
||||||
|
let swap = SwapReq {
|
||||||
|
onion: onion.clone(),
|
||||||
|
comsig,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut server = MockServer::new();
|
||||||
|
server.set_response(
|
||||||
|
&onion,
|
||||||
|
SwapError::CoinNotFound {
|
||||||
|
commit: commitment.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(server));
|
||||||
|
|
||||||
|
let req = format!(
|
||||||
|
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||||
|
serde_json::json!(swap)
|
||||||
|
);
|
||||||
|
let response = make_request(server, req)?;
|
||||||
|
let expected = format!(
|
||||||
|
"{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n",
|
||||||
|
commitment
|
||||||
|
);
|
||||||
|
assert_eq!(response, expected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
277
src/secp.rs
277
src/secp.rs
|
@ -1,92 +1,165 @@
|
||||||
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::ecdh::SharedSecret;
|
||||||
|
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
|
||||||
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
|
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
|
||||||
pub use secp256k1zkp::key::{PublicKey, SecretKey};
|
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
|
||||||
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::{Error, ErrorKind, Result};
|
||||||
use crate::error::{ErrorKind, Result};
|
|
||||||
|
|
||||||
use rand::RngCore;
|
use blake2::blake2b::Blake2b;
|
||||||
use std::cmp;
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||||
|
use secp256k1zkp::rand::thread_rng;
|
||||||
|
|
||||||
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
|
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
|
||||||
pub const COM_SIGNATURE_SIZE : usize = 96;
|
#[derive(Clone)]
|
||||||
|
pub struct ComSignature {
|
||||||
pub struct ComSignature(pub [u8; COM_SIGNATURE_SIZE]);
|
pub_nonce: Commitment,
|
||||||
impl ComSignature {
|
s: SecretKey,
|
||||||
/// Builds a ComSignature from a byte vector. If the vector is too short, it will be
|
t: SecretKey,
|
||||||
/// 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];
|
impl ComSignature {
|
||||||
for i in 0..cmp::min(v.len(), COM_SIGNATURE_SIZE) {
|
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
|
||||||
h[i] = v[i];
|
ComSignature {
|
||||||
|
pub_nonce: pub_nonce.to_owned(),
|
||||||
|
s: s.to_owned(),
|
||||||
|
t: t.to_owned(),
|
||||||
}
|
}
|
||||||
ComSignature(h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn sign(_value: u64, _blind: &SecretKey, _msg: &Vec<u8>) -> Result<ComSignature> {
|
pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec<u8>) -> Result<ComSignature> {
|
||||||
// milestone 2 - todo
|
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||||
let mut h = [0u8; COM_SIGNATURE_SIZE];
|
|
||||||
for i in 0..COM_SIGNATURE_SIZE {
|
let mut amt_bytes = [0; 32];
|
||||||
h[i] = i as u8;
|
BigEndian::write_u64(&mut amt_bytes[24..32], amount);
|
||||||
}
|
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
|
||||||
Ok(ComSignature(h))
|
|
||||||
|
let k_1 = SecretKey::new(&secp, &mut thread_rng());
|
||||||
|
let k_2 = SecretKey::new(&secp, &mut thread_rng());
|
||||||
|
|
||||||
|
let commitment = secp.commit(amount, blind.clone())?;
|
||||||
|
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
|
||||||
|
|
||||||
|
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
|
||||||
|
|
||||||
|
// s = k_1 + (e * amount)
|
||||||
|
let mut s = k_amt.clone();
|
||||||
|
s.mul_assign(&secp, &e)?;
|
||||||
|
s.add_assign(&secp, &k_1)?;
|
||||||
|
|
||||||
|
// t = k_2 + (e * blind)
|
||||||
|
let mut t = blind.clone();
|
||||||
|
t.mul_assign(&secp, &e)?;
|
||||||
|
t.add_assign(&secp, &k_2)?;
|
||||||
|
|
||||||
|
Ok(ComSignature::new(&nonce_commitment, &s, &t))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<[u8]> for ComSignature {
|
fn calc_challenge(
|
||||||
fn as_ref(&self) -> &[u8] {
|
secp: &Secp256k1,
|
||||||
&self.0
|
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
|
/// Serializes a ComSignature to and from hex
|
||||||
pub mod comsig_serde {
|
pub mod comsig_serde {
|
||||||
use super::ComSignature;
|
use super::ComSignature;
|
||||||
use serde::{Deserialize, Serializer};
|
use grin_core::ser::{self, ProtocolVersion};
|
||||||
use grin_util::ToHex;
|
use grin_util::ToHex;
|
||||||
|
use serde::{Deserialize, Serializer};
|
||||||
|
|
||||||
/// Serializes a ComSignature as a hex string
|
/// Serializes a ComSignature as a hex string
|
||||||
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_str(&comsig.to_hex())
|
use serde::ser::Error;
|
||||||
|
let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
|
||||||
|
serializer.serialize_str(&bytes.to_hex())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a ComSignature from a hex string
|
/// Creates a ComSignature from a hex string
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<ComSignature, D::Error>
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<ComSignature, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
String::deserialize(deserializer)
|
let bytes = String::deserialize(deserializer)
|
||||||
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))
|
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
|
||||||
.and_then(|bytes: Vec<u8>| Ok(ComSignature::from_vec(bytes.to_vec())))
|
let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
|
||||||
|
Ok(sig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute a PublicKey from a SecretKey
|
#[allow(non_snake_case)]
|
||||||
pub fn to_public_key(secret_key: &SecretKey) -> Result<PublicKey> {
|
impl Readable for ComSignature {
|
||||||
let secp = Secp256k1::new();
|
fn read<R: Reader>(reader: &mut R) -> std::result::Result<Self, ser::Error> {
|
||||||
let pubkey = PublicKey::from_secret_key(&secp, secret_key)?;
|
let R = Commitment::read(reader)?;
|
||||||
Ok(pubkey)
|
let s = read_secret_key(reader)?;
|
||||||
|
let t = read_secret_key(reader)?;
|
||||||
|
Ok(ComSignature::new(&R, &s, &t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a random SecretKey. Not for production use
|
impl Writeable for ComSignature {
|
||||||
pub fn insecure_rand_secret() -> Result<SecretKey> {
|
fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> {
|
||||||
|
writer.write_fixed_bytes(self.pub_nonce.0)?;
|
||||||
|
writer.write_fixed_bytes(self.s.0)?;
|
||||||
|
writer.write_fixed_bytes(self.t.0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random SecretKey.
|
||||||
|
pub fn random_secret() -> SecretKey {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let mut seed = [0u8; 32];
|
SecretKey::new(&secp, &mut thread_rng())
|
||||||
rand::thread_rng().fill_bytes(&mut seed);
|
}
|
||||||
let secret = SecretKey::from_slice(&secp, &seed)?;
|
|
||||||
Ok(secret)
|
/// Deserialize a SecretKey from a Reader
|
||||||
|
pub fn read_secret_key<R: Reader>(reader: &mut R) -> std::result::Result<SecretKey, ser::Error> {
|
||||||
|
let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
|
||||||
|
let secp = Secp256k1::with_caps(ContextFlag::None);
|
||||||
|
let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?;
|
||||||
|
Ok(pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a Pedersen Commitment using the provided value and blinding factor
|
/// Build a Pedersen Commitment using the provided value and blinding factor
|
||||||
|
@ -106,89 +179,49 @@ pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitm
|
||||||
Ok(sum)
|
Ok(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// secp256k1-zkp object serialization
|
/// Subtracts a value (v*H) from an existing commitment
|
||||||
|
pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment> {
|
||||||
impl Readable for Commitment {
|
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Commitment> {
|
let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
|
||||||
let a = reader.read_fixed_bytes(PEDERSEN_COMMITMENT_SIZE)?;
|
let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
|
||||||
let mut c = [0; PEDERSEN_COMMITMENT_SIZE];
|
Ok(sum)
|
||||||
c[..PEDERSEN_COMMITMENT_SIZE].clone_from_slice(&a[..PEDERSEN_COMMITMENT_SIZE]);
|
|
||||||
Ok(Commitment(c))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writeable for Commitment {
|
/// Signs the message with the provided SecretKey
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature> {
|
||||||
writer.write_fixed_bytes(self)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Readable for RangeProof {
|
#[cfg(test)]
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<RangeProof> {
|
mod tests {
|
||||||
let len = reader.read_u64()?;
|
use super::{ComSignature, ContextFlag, Secp256k1, SecretKey};
|
||||||
let max_len = cmp::min(len as usize, MAX_PROOF_SIZE);
|
use crate::error::Result;
|
||||||
let p = reader.read_fixed_bytes(max_len)?;
|
|
||||||
let mut proof = [0; MAX_PROOF_SIZE];
|
|
||||||
proof[..p.len()].clone_from_slice(&p[..]);
|
|
||||||
Ok(RangeProof {
|
|
||||||
plen: proof.len(),
|
|
||||||
proof,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Writeable for RangeProof {
|
use rand::Rng;
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
use secp256k1zkp::rand::{thread_rng, RngCore};
|
||||||
writer.write_bytes(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Readable for Signature {
|
/// Test signing and verification of ComSignatures
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Signature> {
|
#[test]
|
||||||
let a = reader.read_fixed_bytes(AGG_SIGNATURE_SIZE)?;
|
fn verify_comsig() -> Result<()> {
|
||||||
let mut c = [0; AGG_SIGNATURE_SIZE];
|
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||||
c[..AGG_SIGNATURE_SIZE].clone_from_slice(&a[..AGG_SIGNATURE_SIZE]);
|
|
||||||
Ok(Signature::from_raw_data(&c).unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Writeable for Signature {
|
let amount = thread_rng().next_u64();
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
let blind = SecretKey::new(&secp, &mut thread_rng());
|
||||||
writer.write_fixed_bytes(self)
|
let msg: [u8; 16] = rand::thread_rng().gen();
|
||||||
}
|
let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
|
||||||
}
|
|
||||||
|
|
||||||
impl Readable for PublicKey {
|
let commit = secp.commit(amount, blind.clone())?;
|
||||||
// Read the public key in compressed form
|
assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Self> {
|
|
||||||
let buf = reader.read_fixed_bytes(COMPRESSED_PUBLIC_KEY_SIZE)?;
|
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
|
||||||
let secp = Secp256k1::with_caps(ContextFlag::None);
|
assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
|
||||||
let pk = PublicKey::from_slice(&secp, &buf).map_err(|_| ErrorKind::CorruptedData)?;
|
|
||||||
Ok(pk)
|
let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
|
||||||
}
|
assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
|
||||||
}
|
|
||||||
|
|
||||||
impl Writeable for PublicKey {
|
|
||||||
// Write the public key in compressed form
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
let secp = Secp256k1::with_caps(ContextFlag::None);
|
|
||||||
writer.write_fixed_bytes(self.serialize_vec(&secp, true))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Readable for SecretKey {
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Self> {
|
|
||||||
let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
|
|
||||||
let secp = Secp256k1::with_caps(ContextFlag::None);
|
|
||||||
let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ErrorKind::CorruptedData)?;
|
|
||||||
Ok(pk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Writeable for SecretKey {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
writer.write_fixed_bytes(self.0)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
624
src/ser.rs
624
src/ser.rs
|
@ -1,624 +0,0 @@
|
||||||
// Copyright 2021 The Grin Developers
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//! Serialization and deserialization layer specialized for binary encoding.
|
|
||||||
//! Ensures consistency and safety. Basically a minimal subset or
|
|
||||||
//! rustc_serialize customized for our need.
|
|
||||||
//!
|
|
||||||
//! To use it simply implement `Writeable` or `Readable` and then use the
|
|
||||||
//! `serialize` or `deserialize` functions on them as appropriate.
|
|
||||||
|
|
||||||
use crate::error::{Error, ErrorKind, Result};
|
|
||||||
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
|
|
||||||
use bytes::Buf;
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::marker;
|
|
||||||
|
|
||||||
/// Implementations defined how different numbers and binary structures are
|
|
||||||
/// written to an underlying stream or container (depending on implementation).
|
|
||||||
pub trait Writer {
|
|
||||||
/// Writes a u8 as bytes
|
|
||||||
fn write_u8(&mut self, n: u8) -> Result<()> {
|
|
||||||
self.write_fixed_bytes(&[n])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a u16 as bytes
|
|
||||||
fn write_u16(&mut self, n: u16) -> Result<()> {
|
|
||||||
let mut bytes = [0; 2];
|
|
||||||
BigEndian::write_u16(&mut bytes, n);
|
|
||||||
self.write_fixed_bytes(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a u32 as bytes
|
|
||||||
fn write_u32(&mut self, n: u32) -> Result<()> {
|
|
||||||
let mut bytes = [0; 4];
|
|
||||||
BigEndian::write_u32(&mut bytes, n);
|
|
||||||
self.write_fixed_bytes(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a u32 as bytes
|
|
||||||
fn write_i32(&mut self, n: i32) -> Result<()> {
|
|
||||||
let mut bytes = [0; 4];
|
|
||||||
BigEndian::write_i32(&mut bytes, n);
|
|
||||||
self.write_fixed_bytes(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a u64 as bytes
|
|
||||||
fn write_u64(&mut self, n: u64) -> Result<()> {
|
|
||||||
let mut bytes = [0; 8];
|
|
||||||
BigEndian::write_u64(&mut bytes, n);
|
|
||||||
self.write_fixed_bytes(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a i64 as bytes
|
|
||||||
fn write_i64(&mut self, n: i64) -> Result<()> {
|
|
||||||
let mut bytes = [0; 8];
|
|
||||||
BigEndian::write_i64(&mut bytes, n);
|
|
||||||
self.write_fixed_bytes(&bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a variable number of bytes. The length is encoded as a 64-bit
|
|
||||||
/// prefix.
|
|
||||||
fn write_bytes<T: AsRef<[u8]>>(&mut self, bytes: T) -> Result<()> {
|
|
||||||
self.write_u64(bytes.as_ref().len() as u64)?;
|
|
||||||
self.write_fixed_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a fixed number of bytes. The reader is expected to know the actual length on read.
|
|
||||||
fn write_fixed_bytes<T: AsRef<[u8]>>(&mut self, bytes: T) -> Result<()>;
|
|
||||||
|
|
||||||
/// Writes a fixed length of "empty" bytes.
|
|
||||||
fn write_empty_bytes(&mut self, length: usize) -> Result<()> {
|
|
||||||
self.write_fixed_bytes(vec![0u8; length])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementations defined how different numbers and binary structures are
|
|
||||||
/// read from an underlying stream or container (depending on implementation).
|
|
||||||
pub trait Reader {
|
|
||||||
/// Read a u8 from the underlying Read
|
|
||||||
fn read_u8(&mut self) -> Result<u8>;
|
|
||||||
/// Read a u16 from the underlying Read
|
|
||||||
fn read_u16(&mut self) -> Result<u16>;
|
|
||||||
/// Read a u32 from the underlying Read
|
|
||||||
fn read_u32(&mut self) -> Result<u32>;
|
|
||||||
/// Read a u64 from the underlying Read
|
|
||||||
fn read_u64(&mut self) -> Result<u64>;
|
|
||||||
/// Read a i32 from the underlying Read
|
|
||||||
fn read_i32(&mut self) -> Result<i32>;
|
|
||||||
/// Read a i64 from the underlying Read
|
|
||||||
fn read_i64(&mut self) -> Result<i64>;
|
|
||||||
/// Read a u64 len prefix followed by that number of exact bytes.
|
|
||||||
fn read_bytes_len_prefix(&mut self) -> Result<Vec<u8>>;
|
|
||||||
/// Read a fixed number of bytes from the underlying reader.
|
|
||||||
fn read_fixed_bytes(&mut self, length: usize) -> Result<Vec<u8>>;
|
|
||||||
/// Consumes a byte from the reader, producing an error if it doesn't have
|
|
||||||
/// the expected value
|
|
||||||
fn expect_u8(&mut self, val: u8) -> Result<u8>;
|
|
||||||
|
|
||||||
/// Read a fixed number of "empty" bytes from the underlying reader.
|
|
||||||
/// It is an error if any non-empty bytes encountered.
|
|
||||||
fn read_empty_bytes(&mut self, length: usize) -> Result<()> {
|
|
||||||
for _ in 0..length {
|
|
||||||
if self.read_u8()? != 0u8 {
|
|
||||||
return Err(ErrorKind::CorruptedData.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait that every type that can be serialized as binary must implement.
|
|
||||||
/// Writes directly to a Writer, a utility type thinly wrapping an
|
|
||||||
/// underlying Write implementation.
|
|
||||||
pub trait Writeable {
|
|
||||||
/// Write the data held by this Writeable to the provided writer
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reader that exposes an Iterator interface.
|
|
||||||
pub struct IteratingReader<'a, T, R: Reader> {
|
|
||||||
count: u64,
|
|
||||||
curr: u64,
|
|
||||||
reader: &'a mut R,
|
|
||||||
_marker: marker::PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T, R: Reader> IteratingReader<'a, T, R> {
|
|
||||||
/// Constructor to create a new iterating reader for the provided underlying reader.
|
|
||||||
/// Takes a count so we know how many to iterate over.
|
|
||||||
pub fn new(reader: &'a mut R, count: u64) -> Self {
|
|
||||||
let curr = 0;
|
|
||||||
IteratingReader {
|
|
||||||
count,
|
|
||||||
curr,
|
|
||||||
reader,
|
|
||||||
_marker: marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T, R> Iterator for IteratingReader<'a, T, R>
|
|
||||||
where
|
|
||||||
T: Readable,
|
|
||||||
R: Reader,
|
|
||||||
{
|
|
||||||
type Item = T;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<T> {
|
|
||||||
if self.curr >= self.count {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.curr += 1;
|
|
||||||
T::read(self.reader).ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads multiple serialized items into a Vec.
|
|
||||||
pub fn read_multi<T, R>(reader: &mut R, count: u64) -> Result<Vec<T>>
|
|
||||||
where
|
|
||||||
T: Readable,
|
|
||||||
R: Reader,
|
|
||||||
{
|
|
||||||
// Very rudimentary check to ensure we do not overflow anything
|
|
||||||
// attempting to read huge amounts of data.
|
|
||||||
// Probably better than checking if count * size overflows a u64 though.
|
|
||||||
if count > 1_000_000 {
|
|
||||||
return Err(ErrorKind::TooLargeReadErr.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: Vec<T> = IteratingReader::new(reader, count).collect();
|
|
||||||
if res.len() as u64 != count {
|
|
||||||
return Err(ErrorKind::CountError.into());
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait that every type that can be deserialized from binary must implement.
|
|
||||||
/// Reads directly to a Reader, a utility type thinly wrapping an
|
|
||||||
/// underlying Read implementation.
|
|
||||||
pub trait Readable
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
/// Reads the data necessary to this Readable from the provided reader
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserializes a Readable from any std::io::Read implementation.
|
|
||||||
pub fn deserialize<T: Readable, R: Read>(source: &mut R) -> Result<T> {
|
|
||||||
let mut reader = BinReader::new(source);
|
|
||||||
T::read(&mut reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serializes a Writeable into any std::io::Write implementation.
|
|
||||||
pub fn serialize<W: Writeable>(sink: &mut dyn Write, thing: &W) -> Result<()> {
|
|
||||||
let mut writer = BinWriter::new(sink);
|
|
||||||
thing.write(&mut writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to serialize a writeable directly in memory using a
|
|
||||||
/// Vec<u8>.
|
|
||||||
pub fn ser_vec<W: Writeable>(thing: &W) -> Result<Vec<u8>> {
|
|
||||||
let mut vec = vec![];
|
|
||||||
serialize(&mut vec, thing)?;
|
|
||||||
Ok(vec)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility to read from a binary source
|
|
||||||
pub struct BinReader<'a, R: Read> {
|
|
||||||
source: &'a mut R,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: Read> BinReader<'a, R> {
|
|
||||||
/// Constructor for a new BinReader for the provided source.
|
|
||||||
pub fn new(source: &'a mut R) -> Self {
|
|
||||||
BinReader { source }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_io_err(err: io::Error) -> Error {
|
|
||||||
ErrorKind::IOErr(format!("{}", err), err.kind()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility wrapper for an underlying byte Reader. Defines higher level methods
|
|
||||||
/// to read numbers, byte vectors, hashes, etc.
|
|
||||||
impl<'a, R: Read> Reader for BinReader<'a, R> {
|
|
||||||
fn read_u8(&mut self) -> Result<u8> {
|
|
||||||
self.source.read_u8().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
fn read_u16(&mut self) -> Result<u16> {
|
|
||||||
self.source.read_u16::<BigEndian>().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
fn read_u32(&mut self) -> Result<u32> {
|
|
||||||
self.source.read_u32::<BigEndian>().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
fn read_i32(&mut self) -> Result<i32> {
|
|
||||||
self.source.read_i32::<BigEndian>().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
fn read_u64(&mut self) -> Result<u64> {
|
|
||||||
self.source.read_u64::<BigEndian>().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
fn read_i64(&mut self) -> Result<i64> {
|
|
||||||
self.source.read_i64::<BigEndian>().map_err(map_io_err)
|
|
||||||
}
|
|
||||||
/// Read a variable size vector from the underlying Read. Expects a usize
|
|
||||||
fn read_bytes_len_prefix(&mut self) -> Result<Vec<u8>> {
|
|
||||||
let len = self.read_u64()?;
|
|
||||||
self.read_fixed_bytes(len as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a fixed number of bytes.
|
|
||||||
fn read_fixed_bytes(&mut self, len: usize) -> Result<Vec<u8>> {
|
|
||||||
// not reading more than 100k bytes in a single read
|
|
||||||
if len > 100_000 {
|
|
||||||
return Err(ErrorKind::TooLargeReadErr.into());
|
|
||||||
}
|
|
||||||
let mut buf = vec![0; len];
|
|
||||||
self.source
|
|
||||||
.read_exact(&mut buf)
|
|
||||||
.map(move |_| buf)
|
|
||||||
.map_err(map_io_err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_u8(&mut self, val: u8) -> Result<u8> {
|
|
||||||
let b = self.read_u8()?;
|
|
||||||
if b == val {
|
|
||||||
Ok(b)
|
|
||||||
} else {
|
|
||||||
Err(ErrorKind::UnexpectedData {
|
|
||||||
expected: vec![val],
|
|
||||||
received: vec![b],
|
|
||||||
}.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A reader that reads straight off a stream.
|
|
||||||
/// Tracks total bytes read so we can verify we read the right number afterwards.
|
|
||||||
pub struct StreamingReader<'a> {
|
|
||||||
total_bytes_read: u64,
|
|
||||||
stream: &'a mut dyn Read,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> StreamingReader<'a> {
|
|
||||||
/// Create a new streaming reader with the provided underlying stream.
|
|
||||||
/// Also takes a duration to be used for each individual read_exact call.
|
|
||||||
pub fn new(stream: &'a mut dyn Read) -> StreamingReader<'a> {
|
|
||||||
StreamingReader {
|
|
||||||
total_bytes_read: 0,
|
|
||||||
stream,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the total bytes read via this streaming reader.
|
|
||||||
pub fn total_bytes_read(&self) -> u64 {
|
|
||||||
self.total_bytes_read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Note: We use read_fixed_bytes() here to ensure our "async" I/O behaves as expected.
|
|
||||||
impl<'a> Reader for StreamingReader<'a> {
|
|
||||||
fn read_u8(&mut self) -> Result<u8> {
|
|
||||||
let buf = self.read_fixed_bytes(1)?;
|
|
||||||
Ok(buf[0])
|
|
||||||
}
|
|
||||||
fn read_u16(&mut self) -> Result<u16> {
|
|
||||||
let buf = self.read_fixed_bytes(2)?;
|
|
||||||
Ok(BigEndian::read_u16(&buf[..]))
|
|
||||||
}
|
|
||||||
fn read_u32(&mut self) -> Result<u32> {
|
|
||||||
let buf = self.read_fixed_bytes(4)?;
|
|
||||||
Ok(BigEndian::read_u32(&buf[..]))
|
|
||||||
}
|
|
||||||
fn read_i32(&mut self) -> Result<i32> {
|
|
||||||
let buf = self.read_fixed_bytes(4)?;
|
|
||||||
Ok(BigEndian::read_i32(&buf[..]))
|
|
||||||
}
|
|
||||||
fn read_u64(&mut self) -> Result<u64> {
|
|
||||||
let buf = self.read_fixed_bytes(8)?;
|
|
||||||
Ok(BigEndian::read_u64(&buf[..]))
|
|
||||||
}
|
|
||||||
fn read_i64(&mut self) -> Result<i64> {
|
|
||||||
let buf = self.read_fixed_bytes(8)?;
|
|
||||||
Ok(BigEndian::read_i64(&buf[..]))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a variable size vector from the underlying stream. Expects a usize
|
|
||||||
fn read_bytes_len_prefix(&mut self) -> Result<Vec<u8>> {
|
|
||||||
let len = self.read_u64()?;
|
|
||||||
self.total_bytes_read += 8;
|
|
||||||
self.read_fixed_bytes(len as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a fixed number of bytes.
|
|
||||||
fn read_fixed_bytes(&mut self, len: usize) -> Result<Vec<u8>> {
|
|
||||||
let mut buf = vec![0u8; len];
|
|
||||||
self.stream.read_exact(&mut buf)?;
|
|
||||||
self.total_bytes_read += len as u64;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_u8(&mut self, val: u8) -> Result<u8> {
|
|
||||||
let b = self.read_u8()?;
|
|
||||||
if b == val {
|
|
||||||
Ok(b)
|
|
||||||
} else {
|
|
||||||
Err(ErrorKind::UnexpectedData {
|
|
||||||
expected: vec![val],
|
|
||||||
received: vec![b],
|
|
||||||
}.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Protocol version-aware wrapper around a `Buf` impl
|
|
||||||
pub struct BufReader<'a, B: Buf> {
|
|
||||||
inner: &'a mut B,
|
|
||||||
bytes_read: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, B: Buf> BufReader<'a, B> {
|
|
||||||
/// Construct a new BufReader
|
|
||||||
pub fn new(buf: &'a mut B) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: buf,
|
|
||||||
bytes_read: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether the buffer has enough bytes remaining to perform a read
|
|
||||||
fn has_remaining(&mut self, len: usize) -> Result<()> {
|
|
||||||
if self.inner.remaining() >= len {
|
|
||||||
self.bytes_read += len;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(io::ErrorKind::UnexpectedEof.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The total bytes read
|
|
||||||
pub fn bytes_read(&self) -> u64 {
|
|
||||||
self.bytes_read as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience function to read from the buffer and deserialize
|
|
||||||
pub fn body<T: Readable>(&mut self) -> Result<T> {
|
|
||||||
T::read(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, B: Buf> Reader for BufReader<'a, B> {
|
|
||||||
fn read_u8(&mut self) -> Result<u8> {
|
|
||||||
self.has_remaining(1)?;
|
|
||||||
Ok(self.inner.get_u8())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u16(&mut self) -> Result<u16> {
|
|
||||||
self.has_remaining(2)?;
|
|
||||||
Ok(self.inner.get_u16())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u32(&mut self) -> Result<u32> {
|
|
||||||
self.has_remaining(4)?;
|
|
||||||
Ok(self.inner.get_u32())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u64(&mut self) -> Result<u64> {
|
|
||||||
self.has_remaining(8)?;
|
|
||||||
Ok(self.inner.get_u64())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_i32(&mut self) -> Result<i32> {
|
|
||||||
self.has_remaining(4)?;
|
|
||||||
Ok(self.inner.get_i32())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_i64(&mut self) -> Result<i64> {
|
|
||||||
self.has_remaining(8)?;
|
|
||||||
Ok(self.inner.get_i64())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_bytes_len_prefix(&mut self) -> Result<Vec<u8>> {
|
|
||||||
let len = self.read_u64()?;
|
|
||||||
self.read_fixed_bytes(len as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_fixed_bytes(&mut self, len: usize) -> Result<Vec<u8>> {
|
|
||||||
// not reading more than 100k bytes in a single read
|
|
||||||
if len > 100_000 {
|
|
||||||
return Err(ErrorKind::TooLargeReadErr.into());
|
|
||||||
}
|
|
||||||
self.has_remaining(len)?;
|
|
||||||
|
|
||||||
let mut buf = vec![0; len];
|
|
||||||
self.inner.copy_to_slice(&mut buf[..]);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_u8(&mut self, val: u8) -> Result<u8> {
|
|
||||||
let b = self.read_u8()?;
|
|
||||||
if b == val {
|
|
||||||
Ok(b)
|
|
||||||
} else {
|
|
||||||
Err(ErrorKind::UnexpectedData {
|
|
||||||
expected: vec![val],
|
|
||||||
received: vec![b],
|
|
||||||
}.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility wrapper for an underlying byte Writer. Defines higher level methods
|
|
||||||
/// to write numbers, byte vectors, hashes, etc.
|
|
||||||
pub struct BinWriter<'a> {
|
|
||||||
sink: &'a mut dyn Write,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> BinWriter<'a> {
|
|
||||||
/// Wraps a standard Write in a new BinWriter
|
|
||||||
pub fn new(sink: &'a mut dyn Write) -> BinWriter<'a> {
|
|
||||||
BinWriter { sink }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Writer for BinWriter<'a> {
|
|
||||||
fn write_fixed_bytes<T: AsRef<[u8]>>(&mut self, bytes: T) -> Result<()> {
|
|
||||||
self.sink.write_all(bytes.as_ref())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_int {
|
|
||||||
($int:ty, $w_fn:ident, $r_fn:ident) => {
|
|
||||||
impl Writeable for $int {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
writer.$w_fn(*self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Readable for $int {
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<$int> {
|
|
||||||
reader.$r_fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_int!(u8, write_u8, read_u8);
|
|
||||||
impl_int!(u16, write_u16, read_u16);
|
|
||||||
impl_int!(u32, write_u32, read_u32);
|
|
||||||
impl_int!(i32, write_i32, read_i32);
|
|
||||||
impl_int!(u64, write_u64, read_u64);
|
|
||||||
impl_int!(i64, write_i64, read_i64);
|
|
||||||
|
|
||||||
impl<T> Readable for Vec<T>
|
|
||||||
where
|
|
||||||
T: Readable,
|
|
||||||
{
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Vec<T>> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
loop {
|
|
||||||
let elem = T::read(reader);
|
|
||||||
match elem {
|
|
||||||
Ok(e) => buf.push(e),
|
|
||||||
// Err(ErrorKind::IOErr(ref _d, ref kind)) if *kind == io::ErrorKind::UnexpectedEof => {
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
Err(e) => {
|
|
||||||
match e.kind() {
|
|
||||||
ErrorKind::IOErr(ref _d, ref kind) if *kind == io::ErrorKind::UnexpectedEof => {
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
_ => return Err(e),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Writeable for Vec<T>
|
|
||||||
where
|
|
||||||
T: Writeable,
|
|
||||||
{
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
for elmt in self {
|
|
||||||
elmt.write(writer)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, A: Writeable> Writeable for &'a A {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
Writeable::write(*self, writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Writeable, B: Writeable> Writeable for (A, B) {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
Writeable::write(&self.0, writer)?;
|
|
||||||
Writeable::write(&self.1, writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Readable, B: Readable> Readable for (A, B) {
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<(A, B)> {
|
|
||||||
Ok((Readable::read(reader)?, Readable::read(reader)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Writeable, B: Writeable, C: Writeable> Writeable for (A, B, C) {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
Writeable::write(&self.0, writer)?;
|
|
||||||
Writeable::write(&self.1, writer)?;
|
|
||||||
Writeable::write(&self.2, writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Writeable, B: Writeable, C: Writeable, D: Writeable> Writeable for (A, B, C, D) {
|
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
Writeable::write(&self.0, writer)?;
|
|
||||||
Writeable::write(&self.1, writer)?;
|
|
||||||
Writeable::write(&self.2, writer)?;
|
|
||||||
Writeable::write(&self.3, writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Readable, B: Readable, C: Readable> Readable for (A, B, C) {
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<(A, B, C)> {
|
|
||||||
Ok((
|
|
||||||
Readable::read(reader)?,
|
|
||||||
Readable::read(reader)?,
|
|
||||||
Readable::read(reader)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Readable, B: Readable, C: Readable, D: Readable> Readable for (A, B, C, D) {
|
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<(A, B, C, D)> {
|
|
||||||
Ok((
|
|
||||||
Readable::read(reader)?,
|
|
||||||
Readable::read(reader)?,
|
|
||||||
Readable::read(reader)?,
|
|
||||||
Readable::read(reader)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serializes a Vec<u8> to and from hex
|
|
||||||
pub mod vec_serde {
|
|
||||||
use serde::{Deserialize, Serializer};
|
|
||||||
use grin_util::ToHex;
|
|
||||||
|
|
||||||
/// Serializes a Vec<u8> as a hex string
|
|
||||||
pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&bytes.to_hex())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a Vec<u8> from a hex string
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::Error;
|
|
||||||
String::deserialize(deserializer)
|
|
||||||
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))
|
|
||||||
}
|
|
||||||
}
|
|
742
src/server.rs
742
src/server.rs
|
@ -1,213 +1,649 @@
|
||||||
use crate::onion;
|
use crate::config::ServerConfig;
|
||||||
use crate::secp::{self, Commitment, ComSignature, SecretKey};
|
use crate::node::{self, GrinNode};
|
||||||
use crate::ser;
|
use crate::onion::Onion;
|
||||||
use crate::types::Onion;
|
use crate::secp::{ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
|
||||||
|
use crate::wallet::{self, Wallet};
|
||||||
|
|
||||||
use jsonrpc_derive::rpc;
|
use grin_core::core::{Input, Output, OutputFeatures, TransactionBody};
|
||||||
use jsonrpc_http_server::*;
|
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||||
use jsonrpc_http_server::jsonrpc_core::*;
|
use itertools::Itertools;
|
||||||
use jsonrpc_core::{Result, Value};
|
use std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::result::Result;
|
||||||
use std::net::SocketAddr;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::Mutex;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ServerConfig {
|
struct Submission {
|
||||||
pub key: SecretKey,
|
/// The total excess for the output commitment
|
||||||
pub addr: SocketAddr,
|
excess: SecretKey,
|
||||||
pub is_first: bool,
|
/// The derived output commitment after applying excess and fee
|
||||||
|
output_commit: Commitment,
|
||||||
|
/// The rangeproof, included only for the final hop (node N)
|
||||||
|
rangeproof: Option<RangeProof>,
|
||||||
|
/// Transaction input being spent
|
||||||
|
input: Input,
|
||||||
|
/// Transaction fee
|
||||||
|
fee: u64,
|
||||||
|
/// The remaining onion after peeling off our layer
|
||||||
|
onion: Onion,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Submission {
|
/// Swap error types
|
||||||
pub excess: SecretKey,
|
#[derive(Clone, Error, Debug, PartialEq)]
|
||||||
pub input_commit: Commitment,
|
pub enum SwapError {
|
||||||
pub onion: Onion,
|
#[error("Invalid number of payloads provided (expected {expected:?}, found {found:?})")]
|
||||||
|
InvalidPayloadLength { expected: usize, found: usize },
|
||||||
|
#[error("Commitment Signature is invalid")]
|
||||||
|
InvalidComSignature,
|
||||||
|
#[error("Rangeproof is invalid")]
|
||||||
|
InvalidRangeproof,
|
||||||
|
#[error("Rangeproof is required but was not supplied")]
|
||||||
|
MissingRangeproof,
|
||||||
|
#[error("Output {commit:?} does not exist, or is already spent.")]
|
||||||
|
CoinNotFound { commit: Commitment },
|
||||||
|
#[error("Output {commit:?} is already in the swap list.")]
|
||||||
|
AlreadySwapped { commit: Commitment },
|
||||||
|
#[error("Failed to peel onion layer: {0}")]
|
||||||
|
PeelOnionFailure(String),
|
||||||
|
#[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")]
|
||||||
|
FeeTooLow { minimum_fee: u64, actual_fee: u64 },
|
||||||
|
#[error("{0}")]
|
||||||
|
UnknownError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
/// A MWixnet server
|
||||||
pub struct SwapReq {
|
pub trait Server: Send + Sync {
|
||||||
pub onion: Onion,
|
/// Submit a new output to be swapped.
|
||||||
#[serde(with = "ser::vec_serde")]
|
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
|
||||||
pub msg: Vec<u8>,
|
|
||||||
#[serde(with = "secp::comsig_serde")]
|
/// Iterate through all saved submissions, filter out any inputs that are no longer spendable,
|
||||||
pub comsig: ComSignature,
|
/// 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.
|
||||||
lazy_static! {
|
fn execute_round(&self) -> crate::error::Result<()>;
|
||||||
static ref SERVER_STATE: Mutex<Vec<Submission>> = Mutex::new(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rpc(server)]
|
|
||||||
pub trait Server {
|
|
||||||
#[rpc(name = "swap")]
|
|
||||||
fn swap(&self, swap: SwapReq) -> Result<Value>;
|
|
||||||
|
|
||||||
// milestone 3:
|
|
||||||
// fn derive_outputs(&self, entries: Vec<Onion>) -> Result<Value>;
|
|
||||||
// fn derive_kernel(&self, tx: Tx) -> Result<Value>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The standard MWixnet server implementation
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ServerImpl {
|
pub struct ServerImpl {
|
||||||
server_key: SecretKey,
|
server_config: ServerConfig,
|
||||||
|
wallet: Arc<dyn Wallet>,
|
||||||
|
node: Arc<dyn GrinNode>,
|
||||||
|
submissions: Arc<Mutex<HashMap<Commitment, Submission>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerImpl {
|
impl ServerImpl {
|
||||||
pub fn new(server_key: SecretKey) -> Self {
|
/// Create a new MWixnet server
|
||||||
ServerImpl { server_key }
|
pub fn new(
|
||||||
|
server_config: ServerConfig,
|
||||||
|
wallet: Arc<dyn Wallet>,
|
||||||
|
node: Arc<dyn GrinNode>,
|
||||||
|
) -> Self {
|
||||||
|
ServerImpl {
|
||||||
|
server_config,
|
||||||
|
wallet,
|
||||||
|
node,
|
||||||
|
submissions: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fee base to use. For now, just using the default.
|
||||||
|
fn get_fee_base(&self) -> u64 {
|
||||||
|
DEFAULT_ACCEPT_FEE_BASE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimum fee to perform a swap.
|
||||||
|
/// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap.
|
||||||
|
fn get_minimum_swap_fee(&self) -> u64 {
|
||||||
|
TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server for ServerImpl {
|
impl Server for ServerImpl {
|
||||||
fn swap(&self, swap: SwapReq) -> Result<Value> {
|
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
||||||
// milestone 2 - check that commitment is unspent
|
// milestone 3: check that enc_payloads length matches number of configured servers
|
||||||
|
if onion.enc_payloads.len() != 1 {
|
||||||
|
return Err(SwapError::InvalidPayloadLength {
|
||||||
|
expected: 1,
|
||||||
|
found: onion.enc_payloads.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Verify commitment signature to ensure caller owns the output
|
// Verify commitment signature to ensure caller owns the output
|
||||||
let _ = swap.comsig.verify(&swap.onion.commit, &swap.msg)
|
let serialized_onion = onion
|
||||||
.map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?;
|
.serialize()
|
||||||
|
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
|
||||||
|
let _ = comsig
|
||||||
|
.verify(&onion.commit, &serialized_onion)
|
||||||
|
.map_err(|_| SwapError::InvalidComSignature)?;
|
||||||
|
|
||||||
let peeled = onion::peel_layer(&swap.onion, &self.server_key)
|
// Verify that commitment is unspent
|
||||||
.map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?;
|
let input = node::build_input(&self.node, &onion.commit)
|
||||||
SERVER_STATE.lock().unwrap().push(Submission{
|
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
|
||||||
excess: peeled.0.excess,
|
let input = input.ok_or(SwapError::CoinNotFound {
|
||||||
input_commit: swap.onion.commit,
|
commit: onion.commit.clone(),
|
||||||
onion: peeled.1
|
})?;
|
||||||
|
|
||||||
|
let peeled = onion
|
||||||
|
.peel_layer(&self.server_config.key)
|
||||||
|
.map_err(|e| SwapError::PeelOnionFailure(e.message()))?;
|
||||||
|
|
||||||
|
// Verify the fee meets the minimum
|
||||||
|
let fee: u64 = peeled.0.fee.into();
|
||||||
|
if fee < self.get_minimum_swap_fee() {
|
||||||
|
return Err(SwapError::FeeTooLow {
|
||||||
|
minimum_fee: self.get_minimum_swap_fee(),
|
||||||
|
actual_fee: fee,
|
||||||
});
|
});
|
||||||
Ok(Value::String("success".into()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// Verify the bullet proof and build the final output
|
||||||
|
if let Some(r) = peeled.0.rangeproof {
|
||||||
/// Spin up the JSON-RPC web server
|
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
||||||
pub fn listen<F>(server_config: &ServerConfig, shutdown_signal: F) -> std::result::Result<(), Box<dyn std::error::Error>>
|
secp.verify_bullet_proof(peeled.1.commit, r, None)
|
||||||
where
|
.map_err(|_| SwapError::InvalidRangeproof)?;
|
||||||
F: futures::future::Future<Output = ()> + Send + 'static,
|
|
||||||
{
|
|
||||||
let mut io = IoHandler::new();
|
|
||||||
io.extend_with(ServerImpl::to_delegate(ServerImpl::new(server_config.key.clone())));
|
|
||||||
|
|
||||||
let server = ServerBuilder::new(io)
|
|
||||||
.cors(DomainsValidation::Disabled)
|
|
||||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
|
||||||
if request.uri() == "/v1" {
|
|
||||||
request.into()
|
|
||||||
} else {
|
} else {
|
||||||
jsonrpc_http_server::Response::bad_request("Only v1 supported").into()
|
// milestone 3: only the last hop will have a rangeproof
|
||||||
|
return Err(SwapError::MissingRangeproof);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.start_http(&server_config.addr)
|
|
||||||
.expect("Unable to start RPC server");
|
|
||||||
|
|
||||||
let close_handle = server.close_handle();
|
let mut locked = self.submissions.lock().unwrap();
|
||||||
std::thread::spawn(move || {
|
if locked.contains_key(&onion.commit) {
|
||||||
futures::executor::block_on(shutdown_signal);
|
return Err(SwapError::AlreadySwapped {
|
||||||
close_handle.close();
|
commit: onion.commit.clone(),
|
||||||
});
|
});
|
||||||
server.wait();
|
}
|
||||||
|
|
||||||
|
locked.insert(
|
||||||
|
onion.commit,
|
||||||
|
Submission {
|
||||||
|
excess: peeled.0.excess,
|
||||||
|
output_commit: peeled.1.commit,
|
||||||
|
rangeproof: peeled.0.rangeproof,
|
||||||
|
input,
|
||||||
|
fee,
|
||||||
|
onion: peeled.1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_round(&self) -> crate::error::Result<()> {
|
||||||
|
let mut locked_state = self.submissions.lock().unwrap();
|
||||||
|
let next_block_height = self.node.get_chain_height()? + 1;
|
||||||
|
|
||||||
|
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_unspent(&self.node, &s.output_commit).unwrap_or(true))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if spendable.len() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
.map(|(_, s)| {
|
||||||
|
Output::new(
|
||||||
|
OutputFeatures::Plain,
|
||||||
|
s.output_commit,
|
||||||
|
s.rangeproof.unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
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,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.node.post_tx(&tx)?;
|
||||||
|
locked_state.clear();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock {
|
||||||
|
use super::{Server, SwapError};
|
||||||
|
use crate::onion::Onion;
|
||||||
|
use crate::secp::ComSignature;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct MockServer {
|
||||||
|
errors: HashMap<Onion, SwapError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockServer {
|
||||||
|
pub fn new() -> MockServer {
|
||||||
|
MockServer {
|
||||||
|
errors: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_response(&mut self, onion: &Onion, e: SwapError) {
|
||||||
|
self.errors.insert(onion.clone(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server for MockServer {
|
||||||
|
fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
|
||||||
|
if let Some(e) = self.errors.get(&onion) {
|
||||||
|
return Err(e.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_round(&self) -> crate::error::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{onion, secp, server, types};
|
use crate::config::ServerConfig;
|
||||||
|
use crate::node::mock::MockGrinNode;
|
||||||
|
use crate::onion::test_util::{self, Hop};
|
||||||
|
use crate::onion::Onion;
|
||||||
|
use crate::secp::{
|
||||||
|
self, ComSignature, Commitment, PublicKey, RangeProof, Secp256k1, SecretKey,
|
||||||
|
};
|
||||||
|
use crate::server::{Server, ServerImpl, Submission, SwapError};
|
||||||
|
use crate::types::Payload;
|
||||||
|
use crate::wallet::mock::MockWallet;
|
||||||
|
|
||||||
|
use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction, Weighting};
|
||||||
|
use grin_core::global::{self, ChainTypes};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::time::Duration;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use hyper::{Body, Client, Request, Response};
|
macro_rules! assert_error_type {
|
||||||
use tokio::runtime;
|
($result:expr, $error_type:pat) => {
|
||||||
|
assert!($result.is_err());
|
||||||
async fn body_to_string(req: Response<Body>) -> String {
|
assert!(if let $error_type = $result.unwrap_err() {
|
||||||
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap();
|
true
|
||||||
String::from_utf8(body_bytes.to_vec()).unwrap()
|
} else {
|
||||||
|
false
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spin up a temporary web service, query the API, then cleanup and return response
|
fn new_server(
|
||||||
fn make_request(server_key: secp::SecretKey, req: String) -> Result<String, Box<dyn std::error::Error>> {
|
server_key: &SecretKey,
|
||||||
let server_config = server::ServerConfig {
|
utxos: &Vec<&Commitment>,
|
||||||
key: server_key,
|
) -> (ServerImpl, Arc<MockGrinNode>) {
|
||||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
let config = ServerConfig {
|
||||||
is_first: true
|
key: server_key.clone(),
|
||||||
|
interval_s: 1,
|
||||||
|
addr: TcpListener::bind("127.0.0.1:0")
|
||||||
|
.unwrap()
|
||||||
|
.local_addr()
|
||||||
|
.unwrap(),
|
||||||
|
grin_node_url: "127.0.0.1:3413".parse().unwrap(),
|
||||||
|
grin_node_secret_path: None,
|
||||||
|
wallet_owner_url: "127.0.0.1:3420".parse().unwrap(),
|
||||||
|
wallet_owner_secret_path: None,
|
||||||
};
|
};
|
||||||
|
let wallet = Arc::new(MockWallet {});
|
||||||
|
let mut mut_node = MockGrinNode::new();
|
||||||
|
for utxo in utxos {
|
||||||
|
mut_node.add_default_utxo(&utxo);
|
||||||
|
}
|
||||||
|
let node = Arc::new(mut_node);
|
||||||
|
|
||||||
let threaded_rt = runtime::Runtime::new()?;
|
let server = ServerImpl::new(config, wallet.clone(), node.clone());
|
||||||
let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel();
|
(server, node)
|
||||||
let uri = format!("http://{}/v1", server_config.addr);
|
}
|
||||||
|
|
||||||
// Spawn the server task
|
fn proof(value: u64, fee: u64, input_blind: &SecretKey, hop_excess: &SecretKey) -> RangeProof {
|
||||||
threaded_rt.spawn(async move {
|
let secp = Secp256k1::new();
|
||||||
server::listen(&server_config, async { shutdown_receiver.await.ok(); }).unwrap()
|
let nonce = secp::random_secret();
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for listener
|
let mut blind = input_blind.clone();
|
||||||
thread::sleep(Duration::from_millis(500));
|
blind.add_assign(&secp, &hop_excess).unwrap();
|
||||||
|
|
||||||
let do_request = async move {
|
secp.bullet_proof(
|
||||||
let request = Request::post(uri)
|
value - fee,
|
||||||
.header("Content-Type", "application/json")
|
blind.clone(),
|
||||||
.body(Body::from(req))
|
nonce.clone(),
|
||||||
.unwrap();
|
nonce.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Client::new().request(request).await
|
fn new_hop(
|
||||||
};
|
server_key: &SecretKey,
|
||||||
|
hop_excess: &SecretKey,
|
||||||
let response = threaded_rt.block_on(do_request)?;
|
fee: u64,
|
||||||
let response_str: String = threaded_rt.block_on(body_to_string(response));
|
proof: Option<RangeProof>,
|
||||||
|
) -> Hop {
|
||||||
shutdown_sender.send(()).ok();
|
let secp = Secp256k1::new();
|
||||||
|
Hop {
|
||||||
// Wait for shutdown
|
pubkey: PublicKey::from_secret_key(&secp, &server_key).unwrap(),
|
||||||
thread::sleep(Duration::from_millis(500));
|
payload: Payload {
|
||||||
threaded_rt.shutdown_background();
|
excess: hop_excess.clone(),
|
||||||
|
fee: FeeFields::from(fee as u32),
|
||||||
Ok(response_str)
|
rangeproof: proof,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single hop to demonstrate request validation and onion unwrapping.
|
/// Single hop to demonstrate request validation and onion unwrapping.
|
||||||
/// UTXO creation and bulletproof generation reserved for milestones 2 & 3.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let server_key = secp::insecure_rand_secret()?;
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
let secp = secp::Secp256k1::new();
|
let server_key = secp::random_secret();
|
||||||
let value: u64 = 100;
|
let hop_excess = secp::random_secret();
|
||||||
let blind = secp::insecure_rand_secret()?;
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
let commitment = secp::commit(value, &blind)?;
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
let session_key = secp::insecure_rand_secret()?;
|
|
||||||
|
|
||||||
let hop = types::Hop {
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
pubkey: secp::PublicKey::from_secret_key(&secp, &server_key)?,
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
payload: types::Payload{
|
|
||||||
excess: secp::insecure_rand_secret()?,
|
let (server, node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
rangeproof: None,
|
server.swap(&onion, &comsig)?;
|
||||||
|
|
||||||
|
// Make sure entry is added to server.
|
||||||
|
let output_commit = secp::add_excess(&input_commit, &hop_excess)?;
|
||||||
|
let output_commit = secp::sub_value(&output_commit, fee)?;
|
||||||
|
|
||||||
|
let expected = Submission {
|
||||||
|
excess: hop_excess.clone(),
|
||||||
|
output_commit: output_commit.clone(),
|
||||||
|
rangeproof: Some(proof),
|
||||||
|
input: Input::new(OutputFeatures::Plain, input_commit.clone()),
|
||||||
|
fee,
|
||||||
|
onion: Onion {
|
||||||
|
ephemeral_pubkey: test_util::next_ephemeral_pubkey(&onion, &server_key)?,
|
||||||
|
commit: output_commit.clone(),
|
||||||
|
enc_payloads: vec![],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let submissions = server.submissions.lock().unwrap();
|
||||||
|
assert_eq!(1, submissions.len());
|
||||||
|
assert!(submissions.contains_key(&input_commit));
|
||||||
|
assert_eq!(&expected, submissions.get(&input_commit).unwrap());
|
||||||
}
|
}
|
||||||
};
|
|
||||||
let hops: Vec<types::Hop> = vec![hop];
|
|
||||||
let onion_packet = onion::create_onion(&commitment, &session_key, &hops)?;
|
|
||||||
let msg : Vec<u8> = vec![0u8, 1u8, 2u8, 3u8];
|
|
||||||
let comsig = secp::ComSignature::sign(value, &blind, &msg)?;
|
|
||||||
let swap = server::SwapReq{
|
|
||||||
onion: onion_packet,
|
|
||||||
msg: msg,
|
|
||||||
comsig: comsig,
|
|
||||||
};
|
|
||||||
|
|
||||||
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap));
|
server.execute_round()?;
|
||||||
let response = make_request(server_key, req)?;
|
|
||||||
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
|
// Make sure entry is removed from server.submissions
|
||||||
assert_eq!(response, expected);
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
assert!(posted_txn.inputs_committed().contains(&input_commit));
|
||||||
|
assert!(posted_txn.outputs_committed().contains(&output_commit));
|
||||||
|
// todo: check that outputs also contain the commitment generated by our wallet
|
||||||
|
|
||||||
|
global::set_local_chain_type(ChainTypes::AutomatedTesting);
|
||||||
|
posted_txn.validate(Weighting::AsTransaction)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns InvalidPayloadLength when too many payloads are provided.
|
||||||
#[test]
|
#[test]
|
||||||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let params = "{ \"param\": \"Not a valid Swap request\" }";
|
let value: u64 = 200_000_000;
|
||||||
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", params);
|
let fee: u64 = 50_000_000;
|
||||||
let response = make_request(secp::insecure_rand_secret()?, req)?;
|
let blind = secp::random_secret();
|
||||||
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
assert_eq!(response, expected);
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let hops: Vec<Hop> = vec![hop.clone(), hop.clone()]; // Multiple payloads
|
||||||
|
let onion = test_util::create_onion(&input_commit, &hops)?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(
|
||||||
|
Err(SwapError::InvalidPayloadLength {
|
||||||
|
expected: 1,
|
||||||
|
found: 2
|
||||||
|
}),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure no entry is added to server.submissions
|
||||||
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// milestone 2 - add tests to cover invalid comsig's & inputs not in utxo set
|
/// Returns InvalidComSignature when ComSignature fails to verify.
|
||||||
|
#[test]
|
||||||
|
fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
|
||||||
|
let wrong_blind = secp::random_secret();
|
||||||
|
let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(Err(SwapError::InvalidComSignature), result);
|
||||||
|
|
||||||
|
// Make sure no entry is added to server.submissions
|
||||||
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment.
|
||||||
|
#[test]
|
||||||
|
fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let wrong_value = value + 10_000_000;
|
||||||
|
let proof = proof(wrong_value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(Err(SwapError::InvalidRangeproof), result);
|
||||||
|
|
||||||
|
// Make sure no entry is added to server.submissions
|
||||||
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns MissingRangeproof when no rangeproof is provided.
|
||||||
|
#[test]
|
||||||
|
fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, None);
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(Err(SwapError::MissingRangeproof), result);
|
||||||
|
|
||||||
|
// Make sure no entry is added to server.submissions
|
||||||
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns CoinNotFound when there's no matching output in the UTXO set.
|
||||||
|
#[test]
|
||||||
|
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(
|
||||||
|
Err(SwapError::CoinNotFound {
|
||||||
|
commit: input_commit.clone()
|
||||||
|
}),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure no entry is added to server.submissions
|
||||||
|
assert!(server.submissions.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns AlreadySwapped when trying to swap the same commitment multiple times.
|
||||||
|
#[test]
|
||||||
|
fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
server.swap(&onion, &comsig)?;
|
||||||
|
|
||||||
|
// Call swap a second time
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(
|
||||||
|
Err(SwapError::AlreadySwapped {
|
||||||
|
commit: input_commit.clone()
|
||||||
|
}),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
|
||||||
|
#[test]
|
||||||
|
fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 50_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
|
||||||
|
let wrong_server_key = secp::random_secret();
|
||||||
|
let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_error_type!(result, SwapError::PeelOnionFailure(_));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns FeeTooLow when the minimum fee is not met.
|
||||||
|
#[test]
|
||||||
|
fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let value: u64 = 200_000_000;
|
||||||
|
let fee: u64 = 1_000_000;
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let input_commit = secp::commit(value, &blind)?;
|
||||||
|
|
||||||
|
let server_key = secp::random_secret();
|
||||||
|
let hop_excess = secp::random_secret();
|
||||||
|
let proof = proof(value, fee, &blind, &hop_excess);
|
||||||
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
||||||
|
|
||||||
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
||||||
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
||||||
|
|
||||||
|
let (server, _node) = new_server(&server_key, &vec![&input_commit]);
|
||||||
|
let result = server.swap(&onion, &comsig);
|
||||||
|
assert_eq!(
|
||||||
|
Err(SwapError::FeeTooLow {
|
||||||
|
minimum_fee: 12_500_000,
|
||||||
|
actual_fee: fee
|
||||||
|
}),
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
161
src/types.rs
161
src/types.rs
|
@ -1,30 +1,42 @@
|
||||||
use crate::error::{ErrorKind, Result};
|
use crate::error::{Result, StdResult};
|
||||||
use crate::secp::{Commitment, PublicKey, RangeProof, SecretKey, Secp256k1};
|
use crate::secp::{self, RangeProof, SecretKey};
|
||||||
use crate::ser::{self, BinReader, Readable, Reader, Writeable, Writer};
|
|
||||||
use grin_util::{self, ToHex};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde::ser::SerializeStruct;
|
|
||||||
use std::fmt;
|
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
pub type RawBytes = Vec<u8>;
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
pub excess: SecretKey,
|
pub excess: SecretKey,
|
||||||
|
pub fee: FeeFields,
|
||||||
pub rangeproof: Option<RangeProof>,
|
pub rangeproof: Option<RangeProof>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Readable for Payload {
|
impl Payload {
|
||||||
fn read<R: Reader>(reader: &mut R) -> Result<Payload> {
|
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload> {
|
||||||
let version = reader.read_u8()?;
|
let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
|
||||||
if version != CURRENT_VERSION {
|
Ok(payload)
|
||||||
return Err(ErrorKind::UnsupportedPayload.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let excess = SecretKey::read(reader)?;
|
#[cfg(test)]
|
||||||
|
pub fn serialize(&self) -> Result<Vec<u8>> {
|
||||||
|
let mut vec = vec![];
|
||||||
|
ser::serialize_default(&mut vec, &self)?;
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Readable for Payload {
|
||||||
|
fn read<R: Reader>(reader: &mut R) -> StdResult<Payload, ser::Error> {
|
||||||
|
let version = reader.read_u8()?;
|
||||||
|
if version != CURRENT_VERSION {
|
||||||
|
return Err(ser::Error::UnsupportedProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
let excess = secp::read_secret_key(reader)?;
|
||||||
|
let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
|
||||||
let rangeproof = if reader.read_u8()? == 0 {
|
let rangeproof = if reader.read_u8()? == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,135 +44,28 @@ impl Readable for Payload {
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = Payload {
|
let payload = Payload {
|
||||||
excess: excess,
|
excess,
|
||||||
rangeproof: rangeproof
|
fee,
|
||||||
|
rangeproof,
|
||||||
};
|
};
|
||||||
Ok(payload)
|
Ok(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writeable for Payload {
|
impl Writeable for Payload {
|
||||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
fn write<W: Writer>(&self, writer: &mut W) -> StdResult<(), ser::Error> {
|
||||||
writer.write_u8(CURRENT_VERSION)?;
|
writer.write_u8(CURRENT_VERSION)?;
|
||||||
writer.write_fixed_bytes(&self.excess)?;
|
writer.write_fixed_bytes(&self.excess)?;
|
||||||
|
writer.write_u64(self.fee.into())?;
|
||||||
|
|
||||||
match &self.rangeproof {
|
match &self.rangeproof {
|
||||||
Some(proof) => {
|
Some(proof) => {
|
||||||
writer.write_u8(1)?;
|
writer.write_u8(1)?;
|
||||||
proof.write(writer)?;
|
proof.write(writer)?;
|
||||||
},
|
}
|
||||||
None => writer.write_u8(0)?,
|
None => writer.write_u8(0)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_payload(payload: &Payload) -> Result<Vec<u8>> {
|
|
||||||
ser::ser_vec(&payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize_payload(bytes: &Vec<u8>) -> Result<Payload> {
|
|
||||||
let mut cursor = Cursor::new(&bytes);
|
|
||||||
let mut reader = BinReader::new(&mut cursor);
|
|
||||||
Payload::read(&mut reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Hop {
|
|
||||||
pub pubkey: PublicKey,
|
|
||||||
pub payload: Payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Onion {
|
|
||||||
pub ephemeral_pubkey: PublicKey,
|
|
||||||
pub commit: Commitment,
|
|
||||||
pub enc_payloads: Vec<RawBytes>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl serde::ser::Serialize for Onion {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::ser::Serializer,
|
|
||||||
{
|
|
||||||
let mut state = serializer.serialize_struct("Onion", 3)?;
|
|
||||||
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
state.serialize_field("pubkey", &self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex())?;
|
|
||||||
state.serialize_field("commit", &self.commit.to_hex())?;
|
|
||||||
|
|
||||||
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
|
|
||||||
state.serialize_field("data", &hex_payloads)?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> serde::de::Deserialize<'de> for Onion {
|
|
||||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::de::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(field_identifier, rename_all = "snake_case")]
|
|
||||||
enum Field {
|
|
||||||
Pubkey,
|
|
||||||
Commit,
|
|
||||||
Data
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OnionVisitor;
|
|
||||||
|
|
||||||
impl<'de> serde::de::Visitor<'de> for OnionVisitor {
|
|
||||||
type Value = Onion;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
formatter.write_str("an Onion")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut pubkey = None;
|
|
||||||
let mut commit = None;
|
|
||||||
let mut data = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
Field::Pubkey => {
|
|
||||||
let val: String = map.next_value()?;
|
|
||||||
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
|
||||||
let secp = Secp256k1::new();
|
|
||||||
pubkey = Some(PublicKey::from_slice(&secp, &vec[..]).map_err(serde::de::Error::custom)?);
|
|
||||||
}
|
|
||||||
Field::Commit => {
|
|
||||||
let val: String = map.next_value()?;
|
|
||||||
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
|
||||||
commit = Some(Commitment::from_vec(vec));
|
|
||||||
}
|
|
||||||
Field::Data => {
|
|
||||||
let val: Vec<String> = map.next_value()?;
|
|
||||||
let mut vec: Vec<Vec<u8>> = Vec::new();
|
|
||||||
for hex in val {
|
|
||||||
vec.push(grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?);
|
|
||||||
}
|
|
||||||
data = Some(vec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Onion {
|
|
||||||
ephemeral_pubkey: pubkey.unwrap(),
|
|
||||||
commit: commit.unwrap(),
|
|
||||||
enc_payloads: data.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIELDS: &[&str] = &[
|
|
||||||
"pubkey",
|
|
||||||
"commit",
|
|
||||||
"data"
|
|
||||||
];
|
|
||||||
deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
265
src/wallet.rs
Normal file
265
src/wallet.rs
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
use crate::error::{ErrorKind, Result};
|
||||||
|
use crate::secp;
|
||||||
|
|
||||||
|
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::libtx::secp_ser;
|
||||||
|
use grin_keychain::BlindingFactor;
|
||||||
|
use grin_util::{ToHex, ZeroingString};
|
||||||
|
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
|
||||||
|
use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub trait Wallet: Send + Sync {
|
||||||
|
/// Builds an output for the wallet with the provided amount.
|
||||||
|
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||||
|
let txn_inputs = Inputs::from(inputs.as_slice());
|
||||||
|
let mut txn_outputs = outputs.clone();
|
||||||
|
let mut txn_excesses = excesses.clone();
|
||||||
|
let mut kernel_fee = total_fee;
|
||||||
|
|
||||||
|
// calculate fee required if we add our own output
|
||||||
|
let fee_required =
|
||||||
|
TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1)
|
||||||
|
* fee_base;
|
||||||
|
|
||||||
|
// calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it
|
||||||
|
let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
|
||||||
|
|
||||||
|
// collect any leftover fees
|
||||||
|
if total_fee > fee_required + fee_to_spend {
|
||||||
|
let amount = total_fee - fee_required;
|
||||||
|
kernel_fee -= amount;
|
||||||
|
|
||||||
|
let wallet_output = wallet.build_output(amount)?;
|
||||||
|
txn_outputs.push(wallet_output.1);
|
||||||
|
|
||||||
|
let output_excess = wallet_output
|
||||||
|
.0
|
||||||
|
.secret_key(&secp)
|
||||||
|
.map_err(|_| ErrorKind::CorruptedData)?;
|
||||||
|
txn_excesses.push(output_excess);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random transaction offset
|
||||||
|
let offset = secp::random_secret();
|
||||||
|
|
||||||
|
// calculate kernel excess
|
||||||
|
let kern_excess = secp.blind_sum(txn_excesses, vec![offset.clone()])?;
|
||||||
|
|
||||||
|
// build and verify kernel
|
||||||
|
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
|
||||||
|
fee: FeeFields::new(0, kernel_fee).unwrap(),
|
||||||
|
});
|
||||||
|
let msg = kernel.msg_to_sign()?;
|
||||||
|
kernel.excess = secp::commit(0, &kern_excess)?;
|
||||||
|
kernel.excess_sig = secp::sign(&kern_excess, &msg)?;
|
||||||
|
kernel.verify()?;
|
||||||
|
|
||||||
|
// assemble the transaction
|
||||||
|
let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel])
|
||||||
|
.with_offset(BlindingFactor::from_secret_key(offset));
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HttpWallet {
|
||||||
|
wallet_owner_url: SocketAddr,
|
||||||
|
wallet_owner_secret: Option<String>,
|
||||||
|
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_owner_secret: &Option<String>,
|
||||||
|
wallet_pass: &ZeroingString,
|
||||||
|
) -> Result<HttpWallet> {
|
||||||
|
println!("Opening wallet at {}", wallet_owner_url);
|
||||||
|
let shared_key = HttpWallet::init_secure_api(&wallet_owner_url, &wallet_owner_secret)?;
|
||||||
|
|
||||||
|
let open_wallet_params = json!({
|
||||||
|
"name": null,
|
||||||
|
"password": wallet_pass.to_string()
|
||||||
|
});
|
||||||
|
let token: Token = HttpWallet::send_enc_request(
|
||||||
|
&wallet_owner_url,
|
||||||
|
&wallet_owner_secret,
|
||||||
|
"open_wallet",
|
||||||
|
&open_wallet_params,
|
||||||
|
&shared_key,
|
||||||
|
)?;
|
||||||
|
println!("Connected to wallet");
|
||||||
|
|
||||||
|
Ok(HttpWallet {
|
||||||
|
wallet_owner_url: wallet_owner_url.clone(),
|
||||||
|
wallet_owner_secret: wallet_owner_secret.clone(),
|
||||||
|
shared_key: shared_key.clone(),
|
||||||
|
token: token.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_secure_api(
|
||||||
|
wallet_owner_url: &SocketAddr,
|
||||||
|
wallet_owner_secret: &Option<String>,
|
||||||
|
) -> 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,
|
||||||
|
&wallet_owner_secret,
|
||||||
|
"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,
|
||||||
|
wallet_owner_secret: &Option<String>,
|
||||||
|
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(),
|
||||||
|
wallet_owner_secret.clone(),
|
||||||
|
&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,
|
||||||
|
wallet_owner_secret: &Option<String>,
|
||||||
|
method: &str,
|
||||||
|
params: &serde_json::Value,
|
||||||
|
) -> Result<D> {
|
||||||
|
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||||
|
let req = build_request(method, params);
|
||||||
|
let res =
|
||||||
|
client::post::<Request, Response>(url.as_str(), wallet_owner_secret.clone(), &req)?;
|
||||||
|
let parsed = res.clone().into_result()?;
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OutputWithBlind {
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "secp_ser::as_hex",
|
||||||
|
deserialize_with = "secp_ser::blind_from_hex"
|
||||||
|
)]
|
||||||
|
blind: BlindingFactor,
|
||||||
|
output: Output,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wallet for HttpWallet {
|
||||||
|
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||||
|
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> {
|
||||||
|
let req_json = json!({
|
||||||
|
"token": self.token.keychain_mask.clone().unwrap().0,
|
||||||
|
"features": "Plain",
|
||||||
|
"amount": amount
|
||||||
|
});
|
||||||
|
let output: OutputWithBlind = HttpWallet::send_enc_request(
|
||||||
|
&self.wallet_owner_url,
|
||||||
|
&self.wallet_owner_secret,
|
||||||
|
"build_output",
|
||||||
|
&req_json,
|
||||||
|
&self.shared_key,
|
||||||
|
)?;
|
||||||
|
Ok((output.blind, output.output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock {
|
||||||
|
use super::Wallet;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::secp;
|
||||||
|
|
||||||
|
use grin_core::core::{Output, OutputFeatures};
|
||||||
|
use grin_keychain::BlindingFactor;
|
||||||
|
use secp256k1zkp::Secp256k1;
|
||||||
|
|
||||||
|
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MockWallet {}
|
||||||
|
|
||||||
|
impl Wallet for MockWallet {
|
||||||
|
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||||
|
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
let blind = secp::random_secret();
|
||||||
|
let commit = secp::commit(amount, &blind)?;
|
||||||
|
let proof = secp.bullet_proof(
|
||||||
|
amount,
|
||||||
|
blind.clone(),
|
||||||
|
secp::random_secret(),
|
||||||
|
secp::random_secret(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let output = Output::new(OutputFeatures::Plain, commit.clone(), proof);
|
||||||
|
Ok((BlindingFactor::from_secret_key(blind), output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue