mirror of
https://github.com/jwinterm/mwixnet.git
synced 2026-10-17 14:21:25 +02:00
separate rpc and server logic
This commit is contained in:
Generated
+5
-4
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user