separate rpc and server logic

This commit is contained in:
scilio
2022-07-31 23:17:08 -04:00
parent 326eb943f3
commit 2e02d67dbc
9 changed files with 594 additions and 343 deletions
Generated
+5 -4
View File
@@ -2503,6 +2503,7 @@ dependencies = [
"serde_derive",
"serde_json",
"sha2 0.10.0",
"thiserror",
"tokio 1.12.0",
"toml",
]
@@ -3879,18 +3880,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.30"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
dependencies = [
"proc-macro2 1.0.34",
"quote 1.0.10",
+1
View File
@@ -29,6 +29,7 @@ serde = { version = "1", features= ["derive"]}
serde_derive = "1"
serde_json = "1"
sha2 = "0.10.0"
thiserror = "1.0.31"
tokio = { version = "1", features = ["full"] }
toml = "0.5"
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
+3 -2
View File
@@ -16,6 +16,7 @@ mod config;
mod error;
mod node;
mod onion;
mod rpc;
mod secp;
mod server;
mod types;
@@ -137,8 +138,8 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
stop_state_clone.stop();
});
// Start the mwixnet server
server::listen(server_config, Arc::new(wallet), Arc::new(node), stop_state)
// Start the mwixnet JSON-RPC HTTP server
rpc::listen(server_config, Arc::new(wallet), Arc::new(node), stop_state)
}
async fn build_signals_fut() {
+62 -51
View File
@@ -9,9 +9,8 @@ use grin_core::core::{Input, OutputFeatures, Transaction};
use grin_util::ToHex;
use serde_json::json;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use std::sync::Arc;
pub trait GrinNode: Send + Sync {
/// Retrieves the unspent output with a matching commitment
@@ -147,62 +146,74 @@ impl GrinNode for HttpGrinNode {
}
}
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
/// Use only for testing purposes.
pub struct MockGrinNode {
utxos: HashMap<Commitment, OutputPrintable>,
txns_posted: RwLock<Vec<Transaction>>,
}
#[cfg(test)]
pub mod mock {
use super::GrinNode;
use crate::error::Result;
use crate::secp::Commitment;
impl MockGrinNode {
pub fn new() -> MockGrinNode {
MockGrinNode {
utxos: HashMap::new(),
txns_posted: RwLock::new(Vec::new()),
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()
}
}
pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) {
self.utxos.insert(output_commit.clone(), utxo.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()));
}
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)
}
Ok(None)
}
fn get_chain_height(&self) -> Result<u64> {
Ok(100)
}
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(())
fn post_tx(&self, tx: &Transaction) -> Result<()> {
let mut write = self.txns_posted.write().unwrap();
write.push(tx.clone());
Ok(())
}
}
}
+11 -1
View File
@@ -16,7 +16,7 @@ type HmacSha256 = Hmac<Sha256>;
type RawBytes = Vec<u8>;
/// A data packet with layers of encryption
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Onion {
/// The onion originator's portion of the shared secret
pub ephemeral_pubkey: PublicKey,
@@ -257,6 +257,16 @@ pub mod test_util {
};
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)]
+272
View File
@@ -0,0 +1,272 @@
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();
secs = 0;
}
}
});
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, &secp::random_secret(), &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, &secp::random_secret(), &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(())
}
}
+1
View File
@@ -16,6 +16,7 @@ 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
#[derive(Clone)]
pub struct ComSignature {
pub_nonce: Commitment,
s: SecretKey,
+207 -263
View File
@@ -6,16 +6,11 @@ use crate::wallet::{self, Wallet};
use grin_core::core::{Input, Output, OutputFeatures, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use grin_util::StopState;
use itertools::Itertools;
use jsonrpc_core::{Result, Value};
use jsonrpc_derive::rpc;
use jsonrpc_http_server::jsonrpc_core::*;
use jsonrpc_http_server::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::result::Result;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use thiserror::Error;
#[derive(Clone, Debug, PartialEq)]
struct Submission {
@@ -33,34 +28,49 @@ struct Submission {
onion: Onion,
}
#[derive(Serialize, Deserialize)]
pub struct SwapReq {
onion: Onion,
#[serde(with = "secp::comsig_serde")]
comsig: ComSignature,
/// Swap error types
#[derive(Clone, Error, Debug, PartialEq)]
pub enum SwapError {
#[error("Invalid number of payloads provided (expected {expected:?}, found {found:?})")]
InvalidPayloadLength { expected: usize, found: usize },
#[error("Commitment Signature is invalid")]
InvalidComSignature,
#[error("Rangeproof is missing or invalid")]
InvalidRangeproof,
#[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),
}
#[rpc(server)]
pub trait RPCServer {
#[rpc(name = "swap")]
fn swap(&self, swap: SwapReq) -> Result<Value>;
pub trait Server: Send + Sync {
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
// 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>;
fn execute_round(&self) -> crate::error::Result<()>;
}
#[derive(Clone)]
struct Server {
pub struct ServerImpl {
server_config: ServerConfig,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
submissions: Arc<Mutex<HashMap<Commitment, Submission>>>,
}
impl Server {
fn new(server_config: ServerConfig, wallet: Arc<dyn Wallet>, node: Arc<dyn GrinNode>) -> Self {
Server {
impl ServerImpl {
/// Create a new MWixnet server
pub fn new(
server_config: ServerConfig,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
) -> Self {
ServerImpl {
server_config,
wallet,
node,
@@ -68,24 +78,6 @@ impl Server {
}
}
/// Spin up an instance of the JSON-RPC HTTP server.
fn start_rpc(&self) -> jsonrpc_http_server::Server {
let mut io = IoHandler::new();
io.extend_with(Server::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")
}
/// The fee base to use. For now, just using the default.
fn get_fee_base(&self) -> u64 {
DEFAULT_ACCEPT_FEE_BASE
@@ -96,6 +88,83 @@ impl Server {
fn get_minimum_swap_fee(&self) -> u64 {
TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base()
}
}
impl Server for ServerImpl {
/// Submit a new output to be swapped.
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
// 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
let serialized_onion = onion
.serialize()
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
let _ = comsig
.verify(&onion.commit, &serialized_onion)
.map_err(|_| SwapError::InvalidComSignature)?;
// Verify that commitment is unspent
let input = node::build_input(&self.node, &onion.commit)
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
let input = input.ok_or(SwapError::CoinNotFound {
commit: onion.commit.clone(),
})?;
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,
});
}
// Calculate final output commitment
let output_commit = secp::add_excess(&onion.commit, &peeled.0.excess)
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
let output_commit = secp::sub_value(&output_commit, fee)
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
// Verify the bullet proof and build the final output
if let Some(r) = peeled.0.rangeproof {
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
secp.verify_bullet_proof(output_commit, r, None)
.map_err(|_| SwapError::InvalidRangeproof)?;
} else {
// milestone 3: only the last hop will have a rangeproof
return Err(SwapError::InvalidRangeproof);
}
let mut locked = self.submissions.lock().unwrap();
if locked.contains_key(&onion.commit) {
return Err(SwapError::AlreadySwapped {
commit: onion.commit.clone(),
});
}
locked.insert(
onion.commit,
Submission {
excess: peeled.0.excess,
output_commit,
rangeproof: peeled.0.rangeproof,
input,
fee,
onion: peeled.1,
},
);
Ok(())
}
/// Iterate through all saved submissions, filter out any inputs that are no longer spendable,
/// and assemble the coinswap transaction, posting the transaction to the configured node.
@@ -158,203 +227,73 @@ impl Server {
}
}
impl RPCServer for Server {
/// Implements the 'swap' API
fn swap(&self, swap: SwapReq) -> Result<Value> {
// milestone 3: check that enc_payloads length matches number of configured servers
if swap.onion.enc_payloads.len() != 1 {
return Err(Error::invalid_params(
"Multi server not supported until milestone 3",
));
}
#[cfg(test)]
pub mod mock {
use super::{Server, SwapError};
use crate::onion::Onion;
use crate::secp::ComSignature;
// Verify commitment signature to ensure caller owns the output
let serialized_onion = swap.onion.serialize().map_err(|e| Error {
message: e.to_string(),
code: ErrorCode::InternalError,
data: None,
})?;
let _ = swap
.comsig
.verify(&swap.onion.commit, &serialized_onion)
.map_err(|_| Error::invalid_params("ComSignature invalid"))?;
use std::collections::HashMap;
// Verify that commitment is unspent
let input = node::build_input(&self.node, &swap.onion.commit).map_err(|e| Error {
message: e.to_string(),
code: ErrorCode::InternalError,
data: None,
})?;
let input = input.ok_or(Error::invalid_params("Commitment not found"))?;
let peeled = swap
.onion
.peel_layer(&self.server_config.key)
.map_err(|e| Error::invalid_params(e.message()))?;
// Verify the fee meets the minimum
let fee: u64 = peeled.0.fee.into();
if fee < self.get_minimum_swap_fee() {
return Err(Error::invalid_params("Fee does not meet minimum"));
}
// Calculate final output commitment
let output_commit =
secp::add_excess(&swap.onion.commit, &peeled.0.excess).map_err(|e| Error {
message: e.to_string(),
code: ErrorCode::InternalError,
data: None,
})?;
let output_commit = secp::sub_value(&output_commit, fee).map_err(|e| Error {
message: e.to_string(),
code: ErrorCode::InternalError,
data: None,
})?;
// Verify the bullet proof and build the final output
if let Some(r) = peeled.0.rangeproof {
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
secp.verify_bullet_proof(output_commit, r, None)
.map_err(|_| Error::invalid_params("RangeProof invalid"))?;
} else {
// milestone 3: only the last hop will have a rangeproof
return Err(Error::invalid_params("Rangeproof expected"));
}
let mut locked = self.submissions.lock().unwrap();
if locked.contains_key(&swap.onion.commit) {
return Err(Error::invalid_params("swap already called for coin"));
}
locked.insert(
swap.onion.commit,
Submission {
excess: peeled.0.excess,
output_commit,
rangeproof: peeled.0.rangeproof,
input,
fee,
onion: peeled.1,
},
);
Ok(Value::String("success".into()))
pub struct MockServer {
errors: HashMap<Onion, SwapError>,
}
}
/// 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 = Arc::new(Server::new(
server_config.clone(),
wallet.clone(),
node.clone(),
));
let rpc_server = server.start_rpc();
println!("Server listening on {}", server.server_config.addr);
let close_handle = rpc_server.close_handle();
let round_handle = std::thread::spawn(move || {
let mut secs = 0;
loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
std::thread::sleep(Duration::from_secs(1));
secs = (secs + 1) % server.server_config.interval_s;
if secs == 0 {
let _ = server.execute_round();
secs = 0;
impl MockServer {
pub fn new() -> MockServer {
MockServer {
errors: HashMap::new(),
}
}
});
rpc_server.wait();
round_handle.join().unwrap();
pub fn set_response(&mut self, onion: &Onion, e: SwapError) {
self.errors.insert(onion.clone(), e);
}
}
Ok(())
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)]
mod tests {
use crate::config::ServerConfig;
use crate::node::{GrinNode, MockGrinNode};
use crate::node::mock::MockGrinNode;
use crate::onion::test_util::{self, Hop};
use crate::secp::{self, ComSignature, PublicKey, Secp256k1};
use crate::server::{self, SwapReq};
use crate::onion::Onion;
use crate::secp::{self, ComSignature, PublicKey, Secp256k1, SecretKey};
use crate::server::{Server, ServerImpl, Submission, SwapError};
use crate::types::Payload;
use crate::wallet::{MockWallet, Wallet};
use crate::wallet::mock::MockWallet;
use grin_core::core::{Committed, FeeFields, Transaction};
use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction};
use std::net::TcpListener;
use std::sync::Arc;
use hyper::{Body, Client, Request, Response};
use tokio::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_key: secp::SecretKey,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
req: String,
) -> Result<String, Box<dyn std::error::Error>> {
let server_config = ServerConfig {
key: server_key,
fn new_config(server_key: &SecretKey) -> ServerConfig {
ServerConfig {
key: server_key.clone(),
interval_s: 1,
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
grin_node_url: "127.0.0.1:3413".parse()?,
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()?,
wallet_owner_url: "127.0.0.1:3420".parse().unwrap(),
wallet_owner_secret_path: None,
};
let server_impl = Arc::new(server::ServerImpl::new(
server_config.clone(),
wallet.clone(),
node.clone(),
));
// Start the server
let rpc_server = server_impl.start_server();
let uri = format!("http://{}/v1", server_config.addr);
let threaded_rt = runtime::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_impl.execute_round()?;
// Stop the server
rpc_server.close();
Ok(response_str)
}
}
/// Single hop to demonstrate request validation and onion unwrapping.
@@ -366,7 +305,7 @@ mod tests {
let value: u64 = 200_000_000;
let fee: u64 = 50_000_000;
let blind = secp::random_secret();
let commitment = secp::commit(value, &blind)?;
let input_commit = secp::commit(value, &blind)?;
let hop_excess = secp::random_secret();
let nonce = secp::random_secret();
@@ -384,40 +323,59 @@ mod tests {
let hop = Hop {
pubkey: PublicKey::from_secret_key(&secp, &server_key)?,
payload: Payload {
excess: hop_excess,
excess: hop_excess.clone(),
fee: FeeFields::from(fee as u32),
rangeproof: Some(proof),
},
};
let hops: Vec<Hop> = vec![hop];
let session_key = secp::random_secret();
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let onion_packet = test_util::create_onion(&input_commit, &session_key, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let swap = SwapReq {
onion: onion_packet,
comsig,
};
let wallet = MockWallet {};
let wallet = Arc::new(MockWallet {});
let mut mut_node = MockGrinNode::new();
mut_node.add_default_utxo(&commitment);
mut_node.add_default_utxo(&input_commit);
let node = Arc::new(mut_node);
let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
serde_json::json!(swap)
);
println!("Request: {}", req);
let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
assert_eq!(response, expected);
let server = ServerImpl::new(new_config(&server_key), wallet.clone(), node.clone());
server.swap(&onion_packet, &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_packet, &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());
}
server.execute_round()?;
// Make sure entry is removed from server.submissions
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();
let input_commit = posted_txn.inputs_committed().into_iter().next().unwrap();
assert_eq!(input_commit, commitment);
let posted_input = posted_txn.inputs_committed().into_iter().next().unwrap();
assert_eq!(input_commit, posted_input);
Ok(())
}
@@ -445,39 +403,25 @@ mod tests {
let session_key = secp::random_secret();
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let swap = SwapReq {
onion: onion_packet,
comsig,
};
let wallet = MockWallet {};
let node = MockGrinNode::new();
let wallet = Arc::new(MockWallet {});
let node = Arc::new(MockGrinNode::new());
let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
serde_json::json!(swap)
let server = ServerImpl::new(new_config(&server_key), wallet.clone(), node.clone());
let result = server.swap(&onion_packet, &comsig);
assert!(result.is_err());
assert_eq!(
SwapError::CoinNotFound {
commit: commitment.clone()
},
result.unwrap_err()
);
let response = make_request(server_key, Arc::new(wallet), Arc::new(node), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Commitment not found\"},\"id\":\"1\"}\n";
assert_eq!(response, expected);
// Make sure no entry is added to server.submissions
assert!(server.submissions.lock().unwrap().is_empty());
Ok(())
}
// TODO: Test bulletproof verification and test minimum fee
#[test]
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
let wallet = MockWallet {};
let node = MockGrinNode::new();
let params = "{ \"param\": \"Not a valid Swap request\" }";
let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
params
);
let response = make_request(secp::random_secret(), Arc::new(wallet), Arc::new(node), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
assert_eq!(response, expected);
Ok(())
}
}
+32 -22
View File
@@ -1,11 +1,10 @@
use crate::error::{ErrorKind, Result};
use crate::secp::{self};
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, OutputFeatures, Transaction, TransactionBody,
TxKernel,
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
};
use grin_core::libtx::secp_ser;
use grin_keychain::BlindingFactor;
@@ -222,25 +221,36 @@ impl Wallet for HttpWallet {
}
}
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
#[derive(Clone)]
pub struct MockWallet {}
#[cfg(test)]
pub mod mock {
use super::Wallet;
use crate::error::Result;
use crate::secp;
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))
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))
}
}
}