├── rustfmt.toml ├── src ├── servers │ ├── mod.rs │ ├── mix_rpc.rs │ ├── swap_rpc.rs │ ├── mix.rs │ └── swap.rs ├── lib.rs ├── bin │ ├── mwixnet.yml │ └── mwixnet.rs ├── http.rs ├── mix_client.rs ├── tor.rs ├── wallet.rs ├── tx.rs ├── node.rs ├── config.rs └── store.rs ├── tests ├── common │ ├── mod.rs │ ├── types.rs │ ├── node.rs │ ├── server.rs │ ├── miner.rs │ └── wallet.rs └── e2e.rs ├── .gitignore ├── doc ├── store.md ├── swap_api.md └── onion.md ├── Cargo.toml ├── README.md └── LICENSE /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /src/servers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mix; 2 | pub mod mix_rpc; 3 | pub mod swap; 4 | pub mod swap_rpc; 5 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod miner; 2 | pub mod node; 3 | pub mod server; 4 | pub mod types; 5 | pub mod wallet; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | target 4 | */Cargo.lock 5 | *.iml 6 | .idea/ 7 | .vscode/ 8 | 9 | mwixnet-config.toml -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | pub mod config; 5 | pub mod http; 6 | pub mod mix_client; 7 | pub mod node; 8 | pub mod servers; 9 | pub mod store; 10 | pub mod tor; 11 | pub mod tx; 12 | pub mod wallet; 13 | 14 | pub use config::ServerConfig; 15 | pub use mix_client::MixClient; 16 | pub use node::{GrinNode, HttpGrinNode, NodeError}; 17 | pub use servers::mix::{MixError, MixServer}; 18 | pub use servers::mix_rpc::listen as mix_listen; 19 | pub use servers::swap::{SwapError, SwapServer}; 20 | pub use servers::swap_rpc::listen as swap_listen; 21 | pub use store::{StoreError, SwapStore}; 22 | pub use wallet::{HttpWallet, Wallet, WalletError}; 23 | -------------------------------------------------------------------------------- /tests/common/types.rs: -------------------------------------------------------------------------------- 1 | use grin_core::libtx::secp_ser; 2 | use grin_keychain::Identifier; 3 | use serde_derive::{Deserialize, Serialize}; 4 | 5 | /// Fees in block to use for coinbase amount calculation 6 | /// (Duplicated from Grin wallet project) 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct BlockFees { 9 | /// fees 10 | #[serde(with = "secp_ser::string_or_u64")] 11 | pub fees: u64, 12 | /// height 13 | #[serde(with = "secp_ser::string_or_u64")] 14 | pub height: u64, 15 | /// key id 16 | pub key_id: Option, 17 | } 18 | 19 | impl BlockFees { 20 | /// return key id 21 | pub fn key_id(&self) -> Option { 22 | self.key_id.clone() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /doc/store.md: -------------------------------------------------------------------------------- 1 | # SwapStore 2 | 3 | ## Overview 4 | 5 | The `SwapStore` is an lmdb database, responsible for storing unprocessed and in-process `SwapData` entries. 6 | 7 | The `SwapStore` is used to hold onto new `SwapData` entries until the next swap round, when the mixing process actually occurs. At that time, they will be marked as `InProcess` until the swap is in a confirmed transaction, at which time they will be marked `Completed` and eventually erased. 8 | 9 | ## Data Model 10 | 11 | `SwapData` entries are keyed with prefix 'S' followed by the commitment of the output being swapped. Entries are all unique by key. 12 | 13 | ### `SwapData` 14 | 15 | The `SwapData` structure contains information needed to swap a single output. It has the following fields: 16 | 17 | - `excess`: The total excess for the output commitment. 18 | - `output_commit`: The derived output commitment after applying excess and fee. 19 | - `rangeproof`: The rangeproof, included only for the final hop (node N). 20 | - `input`: The transaction input being spent. 21 | - `fee`: The transaction fee. 22 | - `onion`: The remaining onion after peeling off our layer. 23 | - `status`: The status of the swap, represented by the `SwapStatus` enum, which can be one of the following: 24 | - `Unprocessed`: The swap has been received but not yet processed. 25 | - `InProcess { kernel_hash: Hash }`: The swap is currently being processed, and is expected to be a transaction with the kernel matching the given `kernel_hash`. 26 | - `Completed { kernel_hash: Hash, block_hash: Hash }`: The swap has been successfully processed and included in the block matching the given `block_hash`. 27 | - `Failed`: The swap has failed, potentially due to expiration or because the output is no longer in the UTXO set. -------------------------------------------------------------------------------- /src/bin/mwixnet.yml: -------------------------------------------------------------------------------- 1 | name: mwixnet 2 | about: MWixnet CoinSwap Server 3 | author: scilio 4 | 5 | args: 6 | - config_file: 7 | help: Path to load/save the mwixnet-config.toml configuration file 8 | short: c 9 | long: config_file 10 | takes_value: true 11 | - testnet: 12 | help: Run grin against the Testnet (as opposed to mainnet) 13 | long: testnet 14 | takes_value: false 15 | - grin_node_url: 16 | help: Api address of running GRIN node on which to check inputs and post transactions 17 | short: n 18 | long: grin_node_url 19 | takes_value: true 20 | - grin_node_secret_path: 21 | help: Path to a file containing the secret for the GRIN node api 22 | long: grin_node_secret_path 23 | takes_value: true 24 | - wallet_owner_url: 25 | help: Api address of running wallet owner listener 26 | short: l 27 | long: wallet_owner_url 28 | takes_value: true 29 | - wallet_owner_secret_path: 30 | help: Path to a file containing the secret for the wallet owner api 31 | long: wallet_owner_secret_path 32 | takes_value: true 33 | - wallet_pass: 34 | help: The wallet's password 35 | long: wallet_pass 36 | takes_value: true 37 | - bind_addr: 38 | help: Address to bind the rpc server to (e.g. 127.0.0.1:3000) 39 | long: bind_addr 40 | takes_value: true 41 | - prev_server: 42 | help: Hex public key of the previous swap/mix server 43 | long: prev_server 44 | takes_value: true 45 | - next_server: 46 | help: Hex public key of the next mix server 47 | long: next_server 48 | takes_value: true 49 | subcommands: 50 | - init-config: 51 | about: Writes a new configuration file 52 | - pubkey: 53 | about: Outputs the public key of the server 54 | args: 55 | - output_file: 56 | help: Path to output the public key to 57 | long: output_file 58 | short: o 59 | takes_value: true 60 | required: false -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mwixnet" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | arti-client = { version = "0.18.0", default-features = false, features = ["async-std", "rustls", "onion-service-client", "onion-service-service"] } 10 | arti-hyper = "0.18.0" 11 | async-std = { version = "1", features = ["tokio1"] } 12 | async-trait = "0.1.74" 13 | blake2 = { package = "blake2-rfc", version = "0.2" } 14 | byteorder = "1" 15 | bytes = "1.5.0" 16 | chacha20 = "0.9.1" 17 | chrono = "0.4.31" 18 | clap = { version = "2.33", features = ["yaml"] } 19 | ctrlc = { version = "3.1", features = ["termination"] } 20 | curve25519-dalek = "4.1.2" 21 | dirs = "2.0" 22 | ed25519-dalek = "2.1.1" 23 | function_name = "0.3.0" 24 | futures = "0.3" 25 | fs-mistrust = "0.7.9" 26 | hmac = { version = "0.12.0", features = ["std"] } 27 | hyper = "0.14.28" 28 | hyper-tls = "0.6.0" 29 | itertools = { version = "0.12.0" } 30 | jsonrpc-core = "18.0.0" 31 | jsonrpc-derive = "18.0.0" 32 | jsonrpc-http-server = "18.0.0" 33 | lazy_static = "1" 34 | pbkdf2 = "0.8.0" 35 | rand = "0.7.3" 36 | remove_dir_all = "0.8.2" 37 | ring = "0.16" 38 | rpassword = "4.0" 39 | rusqlite = { version = "0.31.0", features = ["bundled"] } 40 | serde = { version = "1", features = ["derive"] } 41 | serde_derive = "1" 42 | serde_json = "1" 43 | sha2 = "0.10.0" 44 | thiserror = "1.0.30" 45 | tls-api = "0.9.0" 46 | tls-api-native-tls = "0.9.0" 47 | tokio = { version = "1.37.0", features = ["full"] } 48 | toml = "0.8.8" 49 | tor-hscrypto = "0.18.0" 50 | tor-hsrproxy = "0.18.0" 51 | tor-hsservice = "0.18.0" 52 | tor-llcrypto = "0.18.0" 53 | tor-keymgr = "0.18.0" 54 | tor-rtcompat = "0.18.0" 55 | x25519-dalek = "0.6.0" 56 | log = "0.4.20" 57 | 58 | # Bleeding Edge Grin Deps 59 | grin_secp256k1zkp = { version = "0.7.14", features = ["bullet-proof-sizing"] } 60 | grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } 61 | grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } 62 | grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" } 63 | grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } 64 | grin_p2p = { git = "https://github.com/mimblewimble/grin", branch = "master" } 65 | grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "master" } 66 | grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" } 67 | grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } 68 | grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 69 | grin_wallet_config = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 70 | grin_wallet_controller = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 71 | grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 72 | grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 73 | grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "contracts" } 74 | -------------------------------------------------------------------------------- /src/servers/mix_rpc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::FutureExt; 4 | use jsonrpc_derive::rpc; 5 | use jsonrpc_http_server::{DomainsValidation, ServerBuilder}; 6 | use jsonrpc_http_server::jsonrpc_core::{self, BoxFuture, IoHandler}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 10 | use grin_onion::crypto::dalek::{self, DalekSignature}; 11 | use grin_onion::onion::Onion; 12 | 13 | use crate::config::ServerConfig; 14 | use crate::mix_client::MixClient; 15 | use crate::node::GrinNode; 16 | use crate::servers::mix::{MixError, MixServer, MixServerImpl}; 17 | use crate::tx::TxComponents; 18 | use crate::wallet::Wallet; 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct MixReq { 22 | onions: Vec, 23 | #[serde(with = "dalek::dalek_sig_serde")] 24 | sig: DalekSignature, 25 | } 26 | 27 | #[derive(Clone, Serialize, Deserialize)] 28 | pub struct MixResp { 29 | pub indices: Vec, 30 | pub components: TxComponents, 31 | } 32 | 33 | impl MixReq { 34 | pub fn new(onions: Vec, sig: DalekSignature) -> Self { 35 | MixReq { onions, sig } 36 | } 37 | } 38 | 39 | #[rpc(server)] 40 | pub trait MixAPI { 41 | #[rpc(name = "mix")] 42 | fn mix(&self, mix: MixReq) -> BoxFuture>; 43 | } 44 | 45 | #[derive(Clone)] 46 | struct RPCMixServer { 47 | server_config: ServerConfig, 48 | server: Arc>, 49 | } 50 | 51 | impl RPCMixServer { 52 | /// Spin up an instance of the JSON-RPC HTTP server. 53 | fn start_http(&self, runtime_handle: tokio::runtime::Handle) -> jsonrpc_http_server::Server { 54 | let mut io = IoHandler::new(); 55 | io.extend_with(RPCMixServer::to_delegate(self.clone())); 56 | 57 | ServerBuilder::new(io) 58 | .event_loop_executor(runtime_handle) 59 | .cors(DomainsValidation::Disabled) 60 | .request_middleware(|request: hyper::Request| { 61 | if request.uri() == "/v1" { 62 | request.into() 63 | } else { 64 | jsonrpc_http_server::Response::bad_request("Only v1 supported").into() 65 | } 66 | }) 67 | .start_http(&self.server_config.addr) 68 | .expect("Unable to start RPC server") 69 | } 70 | } 71 | 72 | impl From for jsonrpc_core::Error { 73 | fn from(e: MixError) -> Self { 74 | jsonrpc_core::Error::invalid_params(e.to_string()) 75 | } 76 | } 77 | 78 | impl MixAPI for RPCMixServer { 79 | fn mix(&self, mix: MixReq) -> BoxFuture> { 80 | let server = self.server.clone(); 81 | async move { 82 | let response = server 83 | .lock() 84 | .await 85 | .mix_outputs(&mix.onions, &mix.sig) 86 | .await?; 87 | Ok(response) 88 | } 89 | .boxed() 90 | } 91 | } 92 | 93 | /// Spin up the JSON-RPC web server 94 | pub fn listen( 95 | rt_handle: &tokio::runtime::Handle, 96 | server_config: ServerConfig, 97 | next_server: Option>, 98 | wallet: Arc, 99 | node: Arc, 100 | ) -> Result< 101 | ( 102 | Arc>, 103 | jsonrpc_http_server::Server, 104 | ), 105 | Box, 106 | > { 107 | let server = MixServerImpl::new( 108 | server_config.clone(), 109 | next_server, 110 | wallet.clone(), 111 | node.clone(), 112 | ); 113 | let server = Arc::new(tokio::sync::Mutex::new(server)); 114 | 115 | let rpc_server = RPCMixServer { 116 | server_config: server_config.clone(), 117 | server: server.clone(), 118 | }; 119 | 120 | let http_server = rpc_server.start_http(rt_handle.clone()); 121 | 122 | Ok((server, http_server)) 123 | } 124 | -------------------------------------------------------------------------------- /tests/common/node.rs: -------------------------------------------------------------------------------- 1 | extern crate grin_wallet_api as apiwallet; 2 | extern crate grin_wallet_config as wallet_config; 3 | extern crate grin_wallet_controller as wallet_controller; 4 | extern crate grin_wallet_impls as wallet; 5 | extern crate grin_wallet_libwallet as libwallet; 6 | 7 | use futures::channel::oneshot; 8 | 9 | use grin_core as core; 10 | 11 | use grin_p2p as p2p; 12 | use grin_servers as servers; 13 | 14 | use grin_util::logger::LogEntry; 15 | use grin_util::{Mutex, StopState}; 16 | use std::default::Default; 17 | use std::net::SocketAddr; 18 | 19 | use mwixnet::{GrinNode, HttpGrinNode}; 20 | use std::sync::{mpsc, Arc}; 21 | use std::thread; 22 | 23 | #[allow(dead_code)] 24 | pub struct IntegrationGrinNode { 25 | server_config: servers::ServerConfig, 26 | stop_state: Arc, 27 | server: Option>, 28 | } 29 | 30 | impl IntegrationGrinNode { 31 | pub fn start(&mut self) -> Arc { 32 | let stop_state_thread = self.stop_state.clone(); 33 | let server_config_thread = self.server_config.clone(); 34 | 35 | // Create a channel to communicate between threads 36 | let (tx, rx) = mpsc::channel(); 37 | 38 | // Start the node in a new thread 39 | thread::spawn(move || { 40 | let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = 41 | Box::leak(Box::new(oneshot::channel::<()>())); 42 | 43 | servers::Server::start( 44 | server_config_thread.clone(), 45 | None, 46 | move |serv: servers::Server, _: Option>| { 47 | // Signal that the callback has been called 48 | tx.send(serv).unwrap(); 49 | // Do other necessary stuff here 50 | }, 51 | Some(stop_state_thread.clone()), 52 | api_chan, 53 | ) 54 | .unwrap(); 55 | }); 56 | 57 | // Wait for the signal from the node-running thread 58 | let server = Arc::new(rx.recv().unwrap()); 59 | self.server = Some(server.clone()); 60 | 61 | server 62 | } 63 | 64 | pub fn stop(&self) { 65 | self.stop_state.stop(); 66 | } 67 | 68 | pub fn api_address(&self) -> SocketAddr { 69 | self.server_config.api_http_addr.parse().unwrap() 70 | } 71 | 72 | pub fn to_client(&self) -> Arc { 73 | Arc::new(HttpGrinNode::new(&self.api_address().to_string(), &None)) 74 | } 75 | } 76 | 77 | #[allow(dead_code)] 78 | pub struct GrinNodeManager { 79 | // base directory for the server instance 80 | working_dir: String, 81 | 82 | nodes: Vec>>, 83 | } 84 | 85 | impl GrinNodeManager { 86 | pub fn new(test_dir: &str) -> GrinNodeManager { 87 | GrinNodeManager { 88 | working_dir: String::from(test_dir), 89 | nodes: vec![], 90 | } 91 | } 92 | 93 | pub fn new_node(&mut self) -> Arc> { 94 | let server_config = servers::ServerConfig { 95 | api_http_addr: format!("127.0.0.1:{}", 20000 + self.nodes.len()), 96 | api_secret_path: None, 97 | db_root: format!("{}/nodes/{}", self.working_dir, self.nodes.len()), 98 | p2p_config: p2p::P2PConfig { 99 | port: 13414, 100 | seeding_type: p2p::Seeding::None, 101 | ..p2p::P2PConfig::default() 102 | }, 103 | chain_type: core::global::ChainTypes::AutomatedTesting, 104 | skip_sync_wait: Some(true), 105 | stratum_mining_config: None, 106 | ..Default::default() 107 | }; 108 | let node = Arc::new(Mutex::new(IntegrationGrinNode { 109 | server_config, 110 | stop_state: Arc::new(StopState::new()), 111 | server: None, 112 | })); 113 | self.nodes.push(node.clone()); 114 | node 115 | } 116 | 117 | pub fn stop_all(&self) { 118 | for node in &self.nodes { 119 | node.lock().stop(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use grin_api::json_rpc; 4 | use grin_util::to_base64; 5 | use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId}; 6 | use hyper::body::Body as HyperBody; 7 | use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}; 8 | use hyper::Request; 9 | use serde_json::json; 10 | use thiserror::Error; 11 | 12 | use secp256k1zkp::SecretKey; 13 | 14 | /// Error types for HTTP client connections 15 | #[derive(Error, Debug)] 16 | pub enum HttpError { 17 | #[error("Error decrypting response")] 18 | DecryptResponseError(), 19 | #[error("Hyper HTTP error: {0:?}")] 20 | HyperHttpError(hyper::http::Error), 21 | #[error("Hyper request failed with error: {0:?}")] 22 | RequestFailed(hyper::Error), 23 | #[error("Error with response body: {0:?}")] 24 | ResponseBodyError(hyper::Error), 25 | #[error("Error deserializing JSON response: {0:?}")] 26 | ResponseJsonError(serde_json::Error), 27 | #[error("Error decoding JSON-RPC response: {0:?}")] 28 | ResponseParseError(json_rpc::Error), 29 | #[error("Wrong response code: {0}")] 30 | ResponseStatusError(hyper::StatusCode), 31 | } 32 | 33 | pub async fn async_send_enc_request( 34 | url: &String, 35 | api_secret: &Option, 36 | method: &str, 37 | params: &serde_json::Value, 38 | shared_key: &SecretKey, 39 | ) -> Result { 40 | let req = json!({ 41 | "method": method, 42 | "params": params, 43 | "id": JsonId::IntId(1), 44 | "jsonrpc": "2.0", 45 | }); 46 | let enc_req = EncryptedRequest::from_json(&JsonId::IntId(1), &req, &shared_key).unwrap(); 47 | let req = build_request(&url, &api_secret, serde_json::to_string(&enc_req).unwrap())?; 48 | let response_str = send_request_async(req).await?; 49 | let enc_res: EncryptedResponse = 50 | serde_json::from_str(&response_str).map_err(HttpError::ResponseJsonError)?; 51 | 52 | let decrypted = enc_res 53 | .decrypt(&shared_key) 54 | .map_err(|_| HttpError::DecryptResponseError())?; 55 | 56 | let response: json_rpc::Response = 57 | serde_json::from_value(decrypted).map_err(HttpError::ResponseJsonError)?; 58 | let parsed = response 59 | .clone() 60 | .into_result() 61 | .map_err(HttpError::ResponseParseError)?; 62 | Ok(parsed) 63 | } 64 | 65 | pub async fn async_send_json_request( 66 | url: &String, 67 | api_secret: &Option, 68 | method: &str, 69 | params: &serde_json::Value, 70 | ) -> Result { 71 | let req_body = json!({ 72 | "method": method, 73 | "params": params, 74 | "id": 1, 75 | "jsonrpc": "2.0", 76 | }); 77 | let req = build_request(&url, &api_secret, serde_json::to_string(&req_body).unwrap())?; 78 | let data = send_request_async(req).await?; 79 | let ser: json_rpc::Response = 80 | serde_json::from_str(&data).map_err(HttpError::ResponseJsonError)?; 81 | let parsed = ser 82 | .clone() 83 | .into_result() 84 | .map_err(HttpError::ResponseParseError)?; 85 | Ok(parsed) 86 | } 87 | 88 | pub fn build_request( 89 | url: &String, 90 | api_secret: &Option, 91 | req_body: String, 92 | ) -> Result, HttpError> { 93 | let mut req_builder = hyper::Request::builder(); 94 | if let Some(api_secret) = api_secret { 95 | let basic_auth = format!("Basic {}", to_base64(&format!("grin:{}", api_secret))); 96 | req_builder = req_builder.header(AUTHORIZATION, basic_auth); 97 | } 98 | 99 | req_builder 100 | .method(hyper::Method::POST) 101 | .uri(url) 102 | .header(USER_AGENT, "grin-client") 103 | .header(ACCEPT, "application/json") 104 | .header(CONTENT_TYPE, "application/json") 105 | .body(HyperBody::from(req_body)) 106 | .map_err(HttpError::HyperHttpError) 107 | } 108 | 109 | async fn send_request_async(req: Request) -> Result { 110 | let client = hyper::Client::builder() 111 | .pool_idle_timeout(Duration::from_secs(30)) 112 | .build_http(); 113 | 114 | let resp = client 115 | .request(req) 116 | .await 117 | .map_err(HttpError::RequestFailed)?; 118 | if !resp.status().is_success() { 119 | return Err(HttpError::ResponseStatusError(resp.status())); 120 | } 121 | 122 | let raw = hyper::body::to_bytes(resp) 123 | .await 124 | .map_err(HttpError::ResponseBodyError)?; 125 | 126 | Ok(String::from_utf8_lossy(&raw).to_string()) 127 | } 128 | -------------------------------------------------------------------------------- /tests/e2e.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use std::ops::Deref; 5 | 6 | use function_name::named; 7 | use grin_core::global; 8 | use grin_util::logger::LoggingConfig; 9 | use log::Level; 10 | 11 | use crate::common::miner::Miner; 12 | use crate::common::node::GrinNodeManager; 13 | use crate::common::server::Servers; 14 | use crate::common::wallet::GrinWalletManager; 15 | 16 | mod common; 17 | 18 | /// Just removes all results from previous runs 19 | fn clean_all_output(test_dir: &str) { 20 | if let Err(e) = remove_dir_all::remove_dir_all(test_dir) { 21 | println!("can't remove output from previous test :{}, may be ok", e); 22 | } 23 | } 24 | 25 | fn setup_test(test_name: &str) -> (GrinNodeManager, GrinWalletManager, String) { 26 | let test_dir = format!("./target/tmp/.{}", test_name); 27 | clean_all_output(test_dir.as_str()); 28 | 29 | let mut logger = LoggingConfig::default(); 30 | logger.log_to_file = false; 31 | logger.stdout_log_level = Level::Error; 32 | grin_util::init_logger(Some(logger), None); 33 | global::set_local_chain_type(global::ChainTypes::AutomatedTesting); 34 | global::set_local_accept_fee_base(50_000_000); 35 | global::init_global_chain_type(global::ChainTypes::AutomatedTesting); 36 | 37 | let nodes = GrinNodeManager::new(test_dir.as_str()); 38 | let wallets = GrinWalletManager::new(test_dir.as_str()); 39 | 40 | (nodes, wallets, test_dir) 41 | } 42 | 43 | #[test] 44 | #[named] 45 | fn integration_test() -> Result<(), Box> { 46 | let (mut nodes, mut wallets, test_dir) = setup_test(function_name!()); 47 | let rt = tokio::runtime::Builder::new_multi_thread() 48 | .enable_all() 49 | .build()?; 50 | 51 | // Create node 52 | let node1 = nodes.new_node(); 53 | let node1_url = node1.lock().api_address(); 54 | let node1_server = node1.lock().start(); 55 | 56 | // Setup swap & mix servers and their wallets 57 | let rt_handle = rt.handle().clone(); 58 | let mut servers = rt.block_on(Servers::async_setup( 59 | test_dir.as_str(), 60 | &rt_handle, 61 | &mut wallets, 62 | &node1, 63 | 2usize, 64 | )); 65 | 66 | rt.block_on(async { 67 | // Setup wallet to use with miner 68 | let mining_wallet = wallets.async_new_wallet(&node1_url).await; 69 | 70 | // Mine enough blocks to have spendable coins 71 | let miner = Miner::new(node1_server.chain.clone()); 72 | miner 73 | .async_mine_empty_blocks(&mining_wallet, 5 + global::coinbase_maturity() as usize) 74 | .await; 75 | 76 | // Setup wallets for swap users 77 | let user1_wallet = wallets.async_new_wallet(&node1_url).await; 78 | let user2_wallet = wallets.async_new_wallet(&node1_url).await; 79 | 80 | // Send from mining_wallet to user1_wallet 81 | let tx1 = mining_wallet 82 | .lock() 83 | .async_send(user1_wallet.lock().deref(), 10_000_000_000) 84 | .await 85 | .unwrap(); 86 | let tx2 = mining_wallet 87 | .lock() 88 | .async_send(user2_wallet.lock().deref(), 20_000_000_000) 89 | .await 90 | .unwrap(); 91 | miner 92 | .async_mine_next_block(&mining_wallet, &vec![tx1, tx2]) 93 | .await; 94 | 95 | let user1_km = user1_wallet.lock().keychain_mask(); 96 | let (_, outputs) = user1_wallet 97 | .lock() 98 | .owner_api() 99 | .retrieve_outputs(user1_km.as_ref(), false, false, None) 100 | .unwrap(); 101 | assert_eq!(outputs.len(), 1); 102 | for output in &outputs { 103 | let (onion, comsig) = user1_wallet 104 | .lock() 105 | .build_onion(&output.commit, &servers.get_pub_keys()) 106 | .unwrap(); 107 | servers.swapper.async_swap(&onion, &comsig).await.unwrap(); 108 | } 109 | 110 | let mining_wallet_info = mining_wallet 111 | .lock() 112 | .async_retrieve_summary_info() 113 | .await 114 | .unwrap(); 115 | println!("Mining wallet: {:?}", mining_wallet_info); 116 | let user1_wallet_info = user1_wallet 117 | .lock() 118 | .async_retrieve_summary_info() 119 | .await 120 | .unwrap(); 121 | println!("User1 wallet: {:?}", user1_wallet_info); 122 | let user2_wallet_info = user2_wallet 123 | .lock() 124 | .async_retrieve_summary_info() 125 | .await 126 | .unwrap(); 127 | println!("User2 wallet: {:?}", user2_wallet_info); 128 | 129 | let _tx = servers.swapper.async_execute_round().await.unwrap(); 130 | }); 131 | 132 | servers.stop_all(); 133 | nodes.stop_all(); 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MWixnet 2 | This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) with some slight modifications. 3 | 4 | A set of n CoinSwap servers (Ni with i=1...n) are agreed upon in advance. They each have a known public key. 5 | 6 | We refer to the first server (N1) as the "Swap Server." This is the server that wallets can submit their coinswaps too. 7 | 8 | We refer to the remaining servers (N2...Nn) as "Mixers." 9 | 10 | ### Setup 11 | #### init-config 12 | To setup a new server, run `mwixnet init-config`. Then enter a password for the server key when prompted. 13 | 14 | This will generate a key for the server and then create a new config file named `mwixnet-config.toml` in the current working directory. 15 | The configuration file will contain the private key of the server encrypted with the server password you provided. 16 | 17 | **Back this config file up! It's the only copy of the server's private key!** 18 | 19 | #### Wallet 20 | 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`). 21 | 22 | ### Usage 23 | 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. 24 | 25 | ### SWAP API 26 | The Swap Server (N1) provides the `swap` API, which is publicly available for use by GRIN wallets. 27 | 28 | **jsonrpc:** `2.0` 29 | **method:** `swap` 30 | **params:** 31 | ``` 32 | [{ 33 | "comsig": "0835f4b8b9cd286c9e35475f575c3e4ae71ceb4ff36598504662627afd628a17d6ba7dedb1aa4c47f0fabad026b76fc86d06f3bef8d0621b8ac4601d4b1b98401586ca3374a401508f32049212478ae91cfa474dfaa5ef2c3dd559d5a292e02334", 34 | "onion": { 35 | "commit": "099a8922343f242dd3da29935ba5bbc7e38bf68eccfb8c96aec87aec0535199139", 36 | "data":[ 37 | "758bc7339edfe8a1f117ef2ef02de58b33d99e820ffd2c21a3d108f4cbdadfbcbc061b705fc351ae2fb34a855bb64180fe8b4913ea13b3bf826773e4b293166cdad1fdf59cadd7d33f73a8f3fc0f339a849f9c505c573d8cc2f006082d8f5bf2c2c84c18d5873a821d9f60bcdd44cf0566d04d761a1eda005cd19ab0b1c593da4656dd2a09322fe1d725f61e8855c927844ec9e80b9260a58012f7c519c5ca3d9b192ab1104d30ac09fb844bd7f692214e5cf0f6cdf1477c20708c2f3ecb098dce6be661907593918840b0fe8eb5043996dace45f2fb66e8f1e2a732035b4e6447b8904f313badebcba99f75c003e297d8dd815915f534dfa7ed416af0c415b60d2a0186752af6af33b781f31fdd3016aeee3bd2e47743fe2ce391b3354b9036b56ec38ed7539adafbc96bef1dbaf354a805b03ac0df7a0d32cff91716926bce68c8ccebb607340f2ffe09c08a9c9fd282ea19b33c69107ed5c54d4872eb0ed83c38d7e07606722069d7709fb914e1e02ea23323f3ae9252902dbfa6f15bd83a3f64587c9ae23aaf96b2a95e1341da12a6e423cf95375184752e10c1dd1a599db74ac0c3d74ec270c589f6a3bdd0877eb986d9a58a8548b917e22bfb93a4a06c36d7cad8d4a8791a8d1e1dc683429b440b136c43ad2f664dafc5156b808050a3c4d28771877d3f1d3a9daa2585eae259aaa64745c6cd260f577e538e27be3c985db41b7c456b63c5b18d7d17420a277d4abc04ae892ceb26940b09fb322445846c14898f5f59305490b1338c56384cd0c7bf5950a0a403aec4d2c2f5e2378b5eb7b1e7fcdbd8d6cc547f3b5a372b22e50e37d858bb197392a10fb9e6e292d6ed6bd8eab1fef7f2d069b6250a0e3e597ccf9a062e04b68821f5c57328ddab775d141147b71c1764c911bad03d8b88e2e62034bc899395514ecab4dec8ab341ba114f0a4e5d1dcfa182396c0e4826ddee187b07bb524dfeaa5297f7a5465f99eaaaa37f082c787b94811feb15b57d68369e6a7e3761d" 38 | ], 39 | "pubkey": "033946e6a495e7278027b38be3d500cfc23d3e0836f1b7e24513841437f316ccb0" 40 | } 41 | }] 42 | ``` 43 | 44 | ### Data Provisioning 45 | #### Inputs 46 | * Cin: UTXO commitment to swap 47 | * xin: Blinding factor of Cin 48 | * K1...n: The public keys of all n servers 49 | 50 | #### Procedure 51 |
    52 |
  1. Choose random xi for each node ni and create a Payload (Pi) for each containing xi
  2. 53 |
  3. Build a rangeproof for Cn=Cin+(Σx1...n)*G and include it in payload Pn
  4. 54 |
  5. Choose random initial ephemeral keypair (r1, R1)
  6. 55 |
  7. Derive remaining ephemeral keypairs such that ri+1=ri*Sha256(Ri||si) where si=ECDH(Ri, Ki)
  8. 56 |
  9. For each node ni, use ChaCha20 stream cipher with key=HmacSha256("MWIXNET"||si) and nonce "NONCE1234567" to encrypt payloads Pi...n
  10. 57 |
58 | 59 | ### Input Validation 60 | 61 | * Node n1 verifies that Cin is in the current UTXO set 62 | * Node n1 verifies the commitment signature is valid for Cin, proving ownership of the input 63 | 64 | ---- 65 | 66 | `Output derivation`, `Output validation`, `Kernel derivation`, and `Aggregation` steps remain unchanged from the [original design](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) -------------------------------------------------------------------------------- /doc/swap_api.md: -------------------------------------------------------------------------------- 1 | # Swap Server API 2 | 3 | ## Overview 4 | 5 | The Swap Server provides a single JSON-RPC API with the method `swap`. This API is used by clients to initiate the mixing process for their outputs, obscuring their coin history in a transaction with other users. 6 | 7 | ## SWAP 8 | 9 | ### Request 10 | The `swap` method accepts a single JSON object containing the following fields: 11 | 12 | - `onion`: an `Onion` data structure, which is the encrypted onion packet containing the key information necessary to transform the user's output. 13 | - `comsig`: a Commitment Signature that proves the client knows the secret key and value of the output's commitment. 14 | 15 | #### `Onion` data structure 16 | 17 | The `Onion` data structure consists of the following fields: 18 | 19 | - `pubkey`: an ephemeral pubkey to as the onion originator's portion of the shared secret, represented as an `x25519_dalek::PublicKey`. 20 | - `commit`: the Pedersen commitment before adjusting the excess and subtracting the fee, represented as a 33-byte `secp256k1` Pedersen commitment. 21 | - `data`: a vector of encrypted payloads, each representing a layer of the onion. When completely decrypted, these are serialized `Payload` objects. 22 | 23 | Each entry in the `enc_payloads` vector corresponds to a server in the system, in order, with the first entry containing the payload for the swap server, and the last entry containing the payload for the final mix server. 24 | 25 | #### `Payload` data structure 26 | 27 | A `Payload` represents a single, decrypted/peeled layer of an Onion. It consists of the following fields: 28 | 29 | - `next_ephemeral_pk`: an `xPublicKey` representing the public key for the next layer. 30 | - `excess`: a `SecretKey` representing the excess value. 31 | - `fee`: a `FeeFields` value representing the transaction fee. 32 | - `rangeproof`: an optional `RangeProof` value. 33 | 34 | ### Response 35 | 36 | A successful call to the 'swap' API will result in an empty JSON-RPC response with no error. 37 | 38 | In case of errors, the API will return a `SwapError` type with one of the following variants: 39 | 40 | - `InvalidPayloadLength`: The provided number of payloads is invalid. 41 | - `InvalidComSignature`: The Commitment Signature is invalid. 42 | - `InvalidRangeproof`: The provided rangeproof is invalid. 43 | - `MissingRangeproof`: A rangeproof is required but was not supplied. 44 | - `CoinNotFound`: The output does not exist, or it is already spent. 45 | - `AlreadySwapped`: The output is already in the swap list. 46 | - `PeelOnionFailure`: Failed to peel onion layer due to an `OnionError`. 47 | - `FeeTooLow`: The provided fee is too low. 48 | - `StoreError`: An error occurred when saving swap to the data store. 49 | - `ClientError`: An error occurred during client communication. 50 | - `SwapTxNotFound`: The previous swap transaction was not found in data store. 51 | - `UnknownError`: An unknown error occurred. 52 | 53 | ### Example 54 | 55 | Here is an example of how to call the 'swap' API: 56 | ```json 57 | { 58 | "jsonrpc": "2.0", 59 | "method": "swap", 60 | "params": { 61 | "comsig": "09ca34db2ac772a9a0e954b4ae2180ba936d8f96219824fe7ec1f5439bef3a0afe7e18867db3d391f37260285feea38ff740b0b49196a4b0a7910c1a72ceca1c5a3e4a53d6e06ffb0536f0dad78812a72ef14e6ff83df8d0dd2aa71615fb00fbe2", 62 | "onion": { 63 | "commit": "0962da257e8c663d1a35128cf87363657ae6ec4a3c78fda4742a77e9c4f17e1a20", 64 | "data": [ 65 | "fd06dd3e506b1c1e76fd6546beec1e88bb13e7e13be7c02a7e525cd22c43d5dc7a906c77e5c07b08d7a5eeb7e7983b87376b02a33f7582ffc1bf2adac498fefbc2dba840d76d4c8e945f", 66 | "ecead273b9b707d101aae71c2c7cb8ce3e7c95347aa730015af206baaf37302df48e5e635ecc94ddf3eee12b314e276f23e29e7dde9f30f712b14ea227801719ecdd1a53999f854a7f4878b905c94905d5f1bfbb4ad9bcf01afeb55070ebcc665d29b0a85093b4d134a52adc76293ad9e963a9f7156dcfc95c1c600a31b919495bf6d3b7ec75eeffcc70aef15b98c43c41468f34b1a96c49b9e20328849a3b12c84d97893145a65d820c37dae51eba62121d681543d060d600167ede3a8c6e807a5765c5ebb2d568366c89bba2b08590a4615822ca64fb848e54267b18fc35fb0f9f6834f1524d7e0da89163e5385de65613e09fed6fec8d9cc60354baa86131b80aa1c8cd5be916a3d757cd8e8253c17158555539a2f8e4d9d1a4b996b218b1af3e7b28bdf9e0f3db2ea9f4d5e11d798d9b7698d037e69df3ca89c2165760963a4d80207917a70a4986d7df83b463547f4d704d28b1eec2e5a93aa70b5b7c73559120e23cd4cfbf76e4d2b21ef215d4c0210001c17318eba633a3c177c18ef88b6c1718e11c552cc77b297dab5c1020557915853434b8ca5698685b3a66bba73164e83d2440473ebb0591df593e0264b605dc3b35055a7de0d40c5c7cc7542dcbe5ade436098dd41e1ac395d2d0baf5c82fdd5932b2e182f8f11a67bccc90e6e63ec8928bd7f0306c6949122fadf12493a7de17f7bfad72501f4f792fca388b3614d6eb3165d948d7c9efe168b5273b132fa27ea6e8df63d70d8b099a9220903b02898b5cc925010ebfab78ccceb19a9f2f6d6e0392c4837977bf0e3e014913e154913c0204913514684f64d7166b3a7203cbab9dddd96ed7db35b4a17fec50abd752348cdf53181ddd6954bc1fb907ed86206dcf05c04efb432cb6ba6db25082b4ce0bf520e3c508163b44c82efaa44b2ec904ddd938a0b99044666941bc72be58e22122027c2fcbc4299e52bc29916eb51206c41e618bce1a5c0d859d116807217282d0883fdabe6f9250cda63082f71fbf921b65ab17cd9bfb0561c4cabe1369c7d6a85c51c0e4f43f51622e70ab4eb0e3fab5" 67 | ], 68 | "pubkey": "500b161d3bbd9249161d9760ba038d9805be86c0e5273782303a67cda50edb5a" 69 | } 70 | }, 71 | "id": "1" 72 | } 73 | ``` -------------------------------------------------------------------------------- /src/mix_client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | use grin_api::json_rpc::{build_request, Response}; 5 | use grin_core::ser; 6 | use grin_core::ser::ProtocolVersion; 7 | use grin_wallet_util::OnionV3Address; 8 | use serde_json; 9 | use serde_json::json; 10 | use thiserror::Error; 11 | use tor_rtcompat::Runtime; 12 | 13 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 14 | use grin_onion::crypto::dalek::{self, DalekPublicKey}; 15 | use grin_onion::onion::Onion; 16 | 17 | use crate::config::ServerConfig; 18 | use crate::servers::mix_rpc::{MixReq, MixResp}; 19 | use crate::tor::TorService; 20 | use crate::{http, tor}; 21 | 22 | /// Error types for interacting with nodes 23 | #[derive(Error, Debug)] 24 | pub enum MixClientError { 25 | #[error("Tor Error: {0:?}")] 26 | Tor(tor::TorError), 27 | #[error("Communication Error: {0:?}")] 28 | CommError(http::HttpError), 29 | #[error("Dalek Error: {0:?}")] 30 | Dalek(dalek::DalekError), 31 | #[error("Error decoding JSON response: {0:?}")] 32 | DecodeResponseError(serde_json::Error), 33 | #[error("Error in JSON-RPC response: {0:?}")] 34 | ResponseError(grin_api::json_rpc::RpcError), 35 | #[error("Custom client error: {0:?}")] 36 | Custom(String), 37 | } 38 | 39 | /// A client for consuming a mix API 40 | #[async_trait] 41 | pub trait MixClient: Send + Sync { 42 | /// Swaps the outputs provided and returns the final swapped outputs and kernels. 43 | async fn mix_outputs(&self, onions: &Vec) -> Result; 44 | } 45 | 46 | pub struct MixClientImpl { 47 | config: ServerConfig, 48 | tor: Arc>>, 49 | addr: OnionV3Address, 50 | } 51 | 52 | impl MixClientImpl { 53 | pub fn new( 54 | config: ServerConfig, 55 | tor: Arc>>, 56 | next_pubkey: DalekPublicKey, 57 | ) -> Self { 58 | let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes()); 59 | MixClientImpl { config, tor, addr } 60 | } 61 | 62 | async fn async_send_json_request( 63 | &self, 64 | addr: &OnionV3Address, 65 | method: &str, 66 | params: &serde_json::Value, 67 | ) -> Result { 68 | let url = format!("{}/v1", addr.to_http_str()); 69 | let request_str = serde_json::to_string(&build_request(method, params)).unwrap(); 70 | let hyper_request = 71 | http::build_request(&url, &None, request_str).map_err(MixClientError::CommError)?; 72 | 73 | let hyper_client = self.tor.lock().new_hyper_client(); 74 | let res = hyper_client.request(hyper_request).await.unwrap(); 75 | 76 | let body_bytes = hyper::body::to_bytes(res.into_body()).await.unwrap(); 77 | let res = String::from_utf8(body_bytes.to_vec()).unwrap(); 78 | 79 | let response: Response = 80 | serde_json::from_str(&res).map_err(MixClientError::DecodeResponseError)?; 81 | 82 | if let Some(ref e) = response.error { 83 | return Err(MixClientError::ResponseError(e.clone())); 84 | } 85 | 86 | let result = match response.result.clone() { 87 | Some(r) => serde_json::from_value(r).map_err(MixClientError::DecodeResponseError), 88 | None => serde_json::from_value(serde_json::Value::Null) 89 | .map_err(MixClientError::DecodeResponseError), 90 | }?; 91 | 92 | Ok(result) 93 | } 94 | } 95 | 96 | #[async_trait] 97 | impl MixClient for MixClientImpl { 98 | async fn mix_outputs(&self, onions: &Vec) -> Result { 99 | let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); 100 | let sig = 101 | dalek::sign(&self.config.key, serialized.as_slice()).map_err(MixClientError::Dalek)?; 102 | let mix = MixReq::new(onions.clone(), sig); 103 | 104 | self.async_send_json_request::(&self.addr, "mix", &json!([mix])) 105 | .await 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | pub mod mock { 111 | use std::collections::HashMap; 112 | 113 | use async_trait::async_trait; 114 | 115 | use grin_onion::onion::Onion; 116 | 117 | use crate::servers::mix_rpc::MixResp; 118 | 119 | use super::{MixClient, MixClientError}; 120 | 121 | pub struct MockMixClient { 122 | results: HashMap, MixResp>, 123 | } 124 | 125 | impl MockMixClient { 126 | pub fn new() -> MockMixClient { 127 | MockMixClient { 128 | results: HashMap::new(), 129 | } 130 | } 131 | 132 | pub fn set_response(&mut self, onions: &Vec, r: MixResp) { 133 | self.results.insert(onions.clone(), r); 134 | } 135 | } 136 | 137 | #[async_trait] 138 | impl MixClient for MockMixClient { 139 | async fn mix_outputs(&self, onions: &Vec) -> Result { 140 | self.results 141 | .get(onions) 142 | .map(|r| Ok(r.clone())) 143 | .unwrap_or(Err(MixClientError::Custom( 144 | "No response set for input".into(), 145 | ))) 146 | } 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | pub mod test_util { 152 | use std::sync::Arc; 153 | 154 | use async_trait::async_trait; 155 | use grin_core::ser; 156 | use grin_core::ser::ProtocolVersion; 157 | 158 | use grin_onion::crypto::dalek::{self, DalekPublicKey}; 159 | use grin_onion::crypto::secp::SecretKey; 160 | use grin_onion::onion::Onion; 161 | 162 | use crate::servers::mix::MixServer; 163 | use crate::servers::mix_rpc::MixResp; 164 | 165 | use super::{MixClient, MixClientError}; 166 | 167 | /// Implementation of the 'MixClient' trait that calls a mix server implementation directly. 168 | /// No JSON-RPC serialization or socket communication occurs. 169 | #[derive(Clone)] 170 | pub struct DirectMixClient { 171 | pub key: SecretKey, 172 | pub mix_server: Arc, 173 | } 174 | 175 | #[async_trait] 176 | impl MixClient for DirectMixClient { 177 | async fn mix_outputs(&self, onions: &Vec) -> Result { 178 | let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); 179 | let sig = 180 | dalek::sign(&self.key, serialized.as_slice()).map_err(MixClientError::Dalek)?; 181 | 182 | sig.verify( 183 | &DalekPublicKey::from_secret(&self.key), 184 | serialized.as_slice(), 185 | ) 186 | .unwrap(); 187 | Ok(self.mix_server.mix_outputs(&onions, &sig).await.unwrap()) 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/tor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arti_client::config::TorClientConfigBuilder; 4 | use arti_client::{TorClient, TorClientConfig}; 5 | use arti_hyper::ArtiHttpConnector; 6 | use curve25519_dalek::digest::Digest; 7 | use ed25519_dalek::hazmat::ExpandedSecretKey; 8 | use futures::task::SpawnExt; 9 | use sha2::Sha512; 10 | use thiserror::Error; 11 | use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; 12 | use tls_api_native_tls::TlsConnector; 13 | use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; 14 | use tor_hsrproxy::config::{ 15 | Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr, 16 | }; 17 | use tor_hsrproxy::OnionServiceReverseProxy; 18 | use tor_hsservice::config::OnionServiceConfigBuilder; 19 | use tor_hsservice::{ 20 | HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService, 21 | }; 22 | use tor_keymgr::key_specifier_derive::internal; 23 | use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector}; 24 | use tor_llcrypto::pk::ed25519::ExpandedKeypair; 25 | use tor_rtcompat::Runtime; 26 | 27 | use secp256k1zkp::SecretKey; 28 | 29 | use crate::config::ServerConfig; 30 | 31 | /// Tor error types 32 | #[derive(Error, Debug)] 33 | pub enum TorError { 34 | #[error("Error generating config: {0:?}")] 35 | ConfigError(grin_wallet_impls::Error), 36 | #[error("Error starting process: {0:?}")] 37 | ProcessError(grin_wallet_impls::tor::process::Error), 38 | } 39 | 40 | pub struct TorService { 41 | tor_client: Option>, 42 | hidden_services: Vec>, 43 | } 44 | 45 | impl TorService { 46 | /// Builds a hyper::Client with an ArtiHttpConnector over the TorClient. 47 | /// The returned Client makes HTTP requests through the TorClient directly, eliminating the need for a socks proxy. 48 | pub fn new_hyper_client( 49 | &self, 50 | ) -> hyper::Client, hyper::Body> { 51 | let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); 52 | let tor_connector = ArtiHttpConnector::new(self.tor_client.clone().unwrap(), tls_connector); 53 | 54 | hyper::Client::builder().build::<_, hyper::Body>(tor_connector) 55 | } 56 | 57 | pub fn stop(&mut self) { 58 | self.tor_client = None; 59 | self.hidden_services.clear(); 60 | } 61 | } 62 | 63 | pub async fn async_init_tor( 64 | runtime: R, 65 | data_dir: &str, 66 | server_config: &ServerConfig, 67 | ) -> Result, TorError> 68 | where 69 | R: Runtime, 70 | { 71 | warn!("Initializing TOR"); 72 | 73 | let state_dir = format!("{}/tor/state", &data_dir); 74 | let cache_dir = format!("{}/tor/cache", &data_dir); 75 | let hs_nickname = HsNickname::new("listener".to_string()).unwrap(); 76 | 77 | let mut client_config_builder = 78 | TorClientConfigBuilder::from_directories(state_dir.clone(), cache_dir.clone()); 79 | client_config_builder 80 | .address_filter() 81 | .allow_onion_addrs(true); 82 | let client_config = client_config_builder.build().unwrap(); 83 | 84 | add_key_to_store(&client_config, &state_dir, &server_config.key, &hs_nickname)?; 85 | let tor_client = TorClient::with_runtime(runtime) 86 | .config(client_config) 87 | .create_bootstrapped() 88 | .await 89 | .unwrap(); 90 | 91 | let service = 92 | async_launch_hidden_service(hs_nickname.clone(), &tor_client, &server_config).await?; 93 | let tor_instance = TorService { 94 | tor_client: Some(tor_client), 95 | hidden_services: vec![service], 96 | }; 97 | Ok(tor_instance) 98 | } 99 | 100 | async fn async_launch_hidden_service( 101 | hs_nickname: HsNickname, 102 | tor_client: &TorClient, 103 | server_config: &ServerConfig, 104 | ) -> Result, TorError> 105 | where 106 | R: Runtime, 107 | { 108 | let svc_cfg = OnionServiceConfigBuilder::default() 109 | .nickname(hs_nickname.clone()) 110 | .build() 111 | .unwrap(); 112 | 113 | let (service, request_stream) = tor_client.launch_onion_service(svc_cfg).unwrap(); 114 | 115 | let proxy_rule = ProxyRule::new( 116 | ProxyPattern::one_port(80).unwrap(), 117 | ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(server_config.addr)), 118 | ); 119 | let mut proxy_cfg_builder = ProxyConfigBuilder::default(); 120 | proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]); 121 | let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap()); 122 | 123 | { 124 | let proxy = proxy.clone(); 125 | let runtime_clone = tor_client.runtime().clone(); 126 | tor_client 127 | .runtime() 128 | .spawn(async move { 129 | match proxy 130 | .handle_requests(runtime_clone, hs_nickname.clone(), request_stream) 131 | .await 132 | { 133 | Ok(()) => { 134 | debug!("Onion service {} exited cleanly.", hs_nickname); 135 | } 136 | Err(e) => { 137 | warn!("Onion service {} exited with an error: {}", hs_nickname, e); 138 | } 139 | } 140 | }) 141 | .unwrap(); 142 | } 143 | 144 | warn!( 145 | "Server listening at http://{}.onion", 146 | server_config.onion_address().to_ov3_str() 147 | ); 148 | Ok(service) 149 | } 150 | 151 | // TODO: Add proper error handling 152 | fn add_key_to_store( 153 | tor_config: &TorClientConfig, 154 | state_dir: &String, 155 | secret_key: &SecretKey, 156 | hs_nickname: &HsNickname, 157 | ) -> Result<(), TorError> { 158 | let key_store_dir = format!("{}/keystore", &state_dir); 159 | let arti_store = 160 | ArtiNativeKeystore::from_path_and_mistrust(&key_store_dir, &tor_config.fs_mistrust()) 161 | .unwrap(); 162 | info!("Using keystore from {key_store_dir:?}"); 163 | 164 | let key_manager = KeyMgrBuilder::default() 165 | .default_store(Box::new(arti_store)) 166 | .build() 167 | .map_err(|_| internal!("failed to build keymgr")) 168 | .unwrap(); 169 | 170 | let expanded_sk = ExpandedSecretKey::from_bytes( 171 | Sha512::default() 172 | .chain_update(secret_key) 173 | .finalize() 174 | .as_ref(), 175 | ); 176 | 177 | let mut sk_bytes = [0_u8; 64]; 178 | sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes()); 179 | sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix); 180 | let expanded_kp = ExpandedKeypair::from_secret_key_bytes(sk_bytes).unwrap(); 181 | 182 | key_manager 183 | .insert( 184 | HsIdKey::from(expanded_kp.public().clone()), 185 | &HsIdPublicKeySpecifier::new(hs_nickname.clone()), 186 | KeystoreSelector::Default, 187 | ) 188 | .unwrap(); 189 | 190 | key_manager 191 | .insert( 192 | HsIdKeypair::from(expanded_kp), 193 | &HsIdKeypairSpecifier::new(hs_nickname.clone()), 194 | KeystoreSelector::Default, 195 | ) 196 | .unwrap(); 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /src/wallet.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::net::ToSocketAddrs; 3 | 4 | use async_trait::async_trait; 5 | use grin_core::core::Output; 6 | use grin_core::libtx::secp_ser; 7 | use grin_keychain::BlindingFactor; 8 | use grin_util::{ToHex, ZeroingString}; 9 | use grin_wallet_api::Token; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::json; 12 | use thiserror::Error; 13 | 14 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 15 | use grin_onion::crypto::secp; 16 | use secp256k1zkp::{PublicKey, Secp256k1, SecretKey}; 17 | 18 | use crate::http; 19 | 20 | #[async_trait] 21 | pub trait Wallet: Send + Sync { 22 | /// Builds an output for the wallet with the provided amount. 23 | async fn async_build_output( 24 | &self, 25 | amount: u64, 26 | ) -> Result<(BlindingFactor, Output), WalletError>; 27 | } 28 | 29 | /// Error types for interacting with wallets 30 | #[derive(Error, Debug)] 31 | pub enum WalletError { 32 | #[error("Error communication with wallet: {0:?}")] 33 | WalletCommError(http::HttpError), 34 | } 35 | 36 | /// HTTP (JSONRPC) implementation of the 'Wallet' trait. 37 | #[derive(Clone)] 38 | pub struct HttpWallet { 39 | wallet_owner_url: SocketAddr, 40 | wallet_owner_secret: Option, 41 | shared_key: SecretKey, 42 | token: Token, 43 | } 44 | 45 | const ENDPOINT: &str = "/v3/owner"; 46 | 47 | /// Wrapper for ECDH Public keys 48 | #[derive(Serialize, Deserialize, Debug, Clone)] 49 | #[serde(transparent)] 50 | pub struct ECDHPubkey { 51 | /// public key, flattened 52 | #[serde(with = "secp_ser::pubkey_serde")] 53 | pub ecdh_pubkey: PublicKey, 54 | } 55 | 56 | impl HttpWallet { 57 | /// Calls the 'open_wallet' using the RPC API. 58 | pub async fn async_open_wallet( 59 | wallet_owner_url: &str, 60 | wallet_owner_secret: &Option, 61 | wallet_pass: &ZeroingString, 62 | ) -> Result { 63 | info!("Opening wallet at {}", wallet_owner_url); 64 | let mut addrs_iter = wallet_owner_url.to_socket_addrs().unwrap(); 65 | let wallet_owner_url = addrs_iter.next().unwrap(); 66 | let shared_key = 67 | HttpWallet::async_init_secure_api(&wallet_owner_url, &wallet_owner_secret).await?; 68 | let open_wallet_params = json!({ 69 | "name": null, 70 | "password": wallet_pass.to_string() 71 | }); 72 | let url = format!("http://{}{}", wallet_owner_url, ENDPOINT); 73 | let token: Token = http::async_send_enc_request( 74 | &url, 75 | &wallet_owner_secret, 76 | "open_wallet", 77 | &open_wallet_params, 78 | &shared_key, 79 | ) 80 | .await 81 | .map_err(WalletError::WalletCommError)?; 82 | info!("Connected to wallet"); 83 | 84 | Ok(HttpWallet { 85 | wallet_owner_url: wallet_owner_url.clone(), 86 | wallet_owner_secret: wallet_owner_secret.clone(), 87 | shared_key: shared_key.clone(), 88 | token: token.clone(), 89 | }) 90 | } 91 | 92 | async fn async_init_secure_api( 93 | wallet_owner_url: &SocketAddr, 94 | wallet_owner_secret: &Option, 95 | ) -> Result { 96 | let secp = Secp256k1::new(); 97 | let ephemeral_sk = secp::random_secret(); 98 | let ephemeral_pk = PublicKey::from_secret_key(&secp, &ephemeral_sk).unwrap(); 99 | let ephemeral_pk_bytes = ephemeral_pk.serialize_vec(&secp, true); 100 | let init_params = json!({ 101 | "ecdh_pubkey": ephemeral_pk_bytes.to_hex() 102 | }); 103 | 104 | let url = format!("http://{}{}", wallet_owner_url, ENDPOINT); 105 | let response_pk: ECDHPubkey = http::async_send_json_request( 106 | &url, 107 | &wallet_owner_secret, 108 | "init_secure_api", 109 | &init_params, 110 | ) 111 | .await 112 | .map_err(WalletError::WalletCommError)?; 113 | 114 | let shared_key = { 115 | let mut shared_pubkey = response_pk.ecdh_pubkey.clone(); 116 | shared_pubkey.mul_assign(&secp, &ephemeral_sk).unwrap(); 117 | 118 | let x_coord = shared_pubkey.serialize_vec(&secp, true); 119 | SecretKey::from_slice(&secp, &x_coord[1..]).unwrap() 120 | }; 121 | 122 | Ok(shared_key) 123 | } 124 | 125 | pub async fn async_perform_request( 126 | &self, 127 | method: &str, 128 | params: &serde_json::Value, 129 | ) -> Result { 130 | let url = format!("http://{}{}", self.wallet_owner_url, ENDPOINT); 131 | http::async_send_enc_request( 132 | &url, 133 | &self.wallet_owner_secret, 134 | method, 135 | params, 136 | &self.shared_key, 137 | ) 138 | .await 139 | .map_err(WalletError::WalletCommError) 140 | } 141 | 142 | pub fn get_token(&self) -> Token { 143 | self.token.clone() 144 | } 145 | } 146 | 147 | #[derive(Clone, Debug, Serialize, Deserialize)] 148 | pub struct OutputWithBlind { 149 | #[serde( 150 | serialize_with = "secp_ser::as_hex", 151 | deserialize_with = "secp_ser::blind_from_hex" 152 | )] 153 | blind: BlindingFactor, 154 | output: Output, 155 | } 156 | 157 | #[async_trait] 158 | impl Wallet for HttpWallet { 159 | /// Builds an 'Output' for the wallet using the 'build_output' RPC API. 160 | async fn async_build_output( 161 | &self, 162 | amount: u64, 163 | ) -> Result<(BlindingFactor, Output), WalletError> { 164 | let params = json!({ 165 | "token": self.token, 166 | "features": "Plain", 167 | "amount": amount 168 | }); 169 | 170 | let url = format!("http://{}{}", self.wallet_owner_url, ENDPOINT); 171 | let output: OutputWithBlind = http::async_send_enc_request( 172 | &url, 173 | &self.wallet_owner_secret, 174 | "build_output", 175 | ¶ms, 176 | &self.shared_key, 177 | ) 178 | .await 179 | .map_err(WalletError::WalletCommError)?; 180 | Ok((output.blind, output.output)) 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | pub mod mock { 186 | use std::borrow::BorrowMut; 187 | use std::sync::{Arc, Mutex}; 188 | 189 | use async_trait::async_trait; 190 | use grin_core::core::{Output, OutputFeatures}; 191 | use grin_keychain::BlindingFactor; 192 | 193 | use grin_onion::crypto::secp; 194 | use secp256k1zkp::pedersen::Commitment; 195 | use secp256k1zkp::Secp256k1; 196 | 197 | use super::{Wallet, WalletError}; 198 | 199 | /// Mock implementation of the 'Wallet' trait for unit-tests. 200 | #[derive(Clone)] 201 | pub struct MockWallet { 202 | built_outputs: Arc>>, 203 | } 204 | 205 | impl MockWallet { 206 | /// Creates a new, empty MockWallet. 207 | pub fn new() -> Self { 208 | MockWallet { 209 | built_outputs: Arc::new(Mutex::new(Vec::new())), 210 | } 211 | } 212 | 213 | /// Returns the commitments of all outputs built for the wallet. 214 | pub fn built_outputs(&self) -> Vec { 215 | self.built_outputs.lock().unwrap().clone() 216 | } 217 | } 218 | 219 | #[async_trait] 220 | impl Wallet for MockWallet { 221 | /// Builds an 'Output' for the wallet using the 'build_output' RPC API. 222 | async fn async_build_output( 223 | &self, 224 | amount: u64, 225 | ) -> Result<(BlindingFactor, Output), WalletError> { 226 | let secp = Secp256k1::new(); 227 | let blind = secp::random_secret(); 228 | let commit = secp::commit(amount, &blind).unwrap(); 229 | let proof = secp.bullet_proof( 230 | amount, 231 | blind.clone(), 232 | secp::random_secret(), 233 | secp::random_secret(), 234 | None, 235 | None, 236 | ); 237 | let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); 238 | 239 | let mut locked = self.built_outputs.lock().unwrap(); 240 | locked.borrow_mut().push(output.commitment().clone()); 241 | 242 | Ok((BlindingFactor::from_secret_key(blind), output)) 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /tests/common/server.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | use std::net::TcpListener; 3 | use std::sync::Arc; 4 | 5 | use grin_core::core::Transaction; 6 | use tor_rtcompat::PreferredRuntime; 7 | use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; 8 | 9 | use grin_onion::crypto::comsig::ComSignature; 10 | use grin_onion::crypto::dalek::DalekPublicKey; 11 | use grin_onion::onion::Onion; 12 | use mwixnet::mix_client::MixClientImpl; 13 | use mwixnet::tor::TorService; 14 | use mwixnet::{tor, SwapError, SwapServer, SwapStore}; 15 | use secp256k1zkp::SecretKey; 16 | 17 | use crate::common::node::IntegrationGrinNode; 18 | use crate::common::wallet::{GrinWalletManager, IntegrationGrinWallet}; 19 | 20 | pub struct IntegrationSwapServer { 21 | server_key: SecretKey, 22 | tor_instance: Arc>>, 23 | swap_server: Arc>, 24 | rpc_server: jsonrpc_http_server::Server, 25 | _wallet: Arc>, 26 | } 27 | 28 | impl IntegrationSwapServer { 29 | pub async fn async_swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> { 30 | self.swap_server.lock().await.swap(&onion, &comsig).await 31 | } 32 | 33 | pub async fn async_execute_round(&self) -> Result>, SwapError> { 34 | self.swap_server.lock().await.execute_round().await 35 | } 36 | } 37 | 38 | pub struct IntegrationMixServer { 39 | server_key: SecretKey, 40 | tor_instance: Arc>>, 41 | rpc_server: jsonrpc_http_server::Server, 42 | _wallet: Arc>, 43 | } 44 | 45 | async fn async_new_swap_server( 46 | data_dir: &str, 47 | rt_handle: &tokio::runtime::Handle, 48 | tor_runtime: R, 49 | wallets: &mut GrinWalletManager, 50 | server_key: &SecretKey, 51 | node: &Arc>, 52 | next_server: Option<&IntegrationMixServer>, 53 | ) -> IntegrationSwapServer 54 | where 55 | R: tor_rtcompat::Runtime, 56 | { 57 | let wallet = wallets.async_new_wallet(&node.lock().api_address()).await; 58 | 59 | let server_config = mwixnet::ServerConfig { 60 | key: server_key.clone(), 61 | interval_s: 15, 62 | addr: TcpListener::bind("127.0.0.1:0") 63 | .unwrap() 64 | .local_addr() 65 | .unwrap(), 66 | grin_node_url: node.lock().api_address().to_string(), 67 | grin_node_secret_path: None, 68 | wallet_owner_url: wallet.lock().owner_address().to_string(), 69 | wallet_owner_secret_path: None, 70 | prev_server: None, 71 | next_server: match next_server { 72 | Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)), 73 | None => None, 74 | }, 75 | }; 76 | 77 | // Open SwapStore 78 | let store = SwapStore::new(format!("{}/db", data_dir).as_str()).unwrap(); 79 | let tor_instance = tor::async_init_tor(tor_runtime, &data_dir, &server_config) 80 | .await 81 | .unwrap(); 82 | let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance)); 83 | 84 | let (swap_server, rpc_server) = mwixnet::swap_listen( 85 | rt_handle, 86 | &server_config, 87 | match next_server { 88 | Some(s) => Some(Arc::new(MixClientImpl::new( 89 | server_config.clone(), 90 | tor_instance.clone(), 91 | DalekPublicKey::from_secret(&s.server_key), 92 | ))), 93 | None => None, 94 | }, 95 | wallet.lock().get_client(), 96 | node.lock().to_client(), 97 | store, 98 | ) 99 | .unwrap(); 100 | 101 | IntegrationSwapServer { 102 | server_key: server_key.clone(), 103 | tor_instance, 104 | swap_server, 105 | rpc_server, 106 | _wallet: wallet, 107 | } 108 | } 109 | 110 | async fn async_new_mix_server( 111 | data_dir: &str, 112 | rt_handle: &tokio::runtime::Handle, 113 | tor_runtime: R, 114 | wallets: &mut GrinWalletManager, 115 | server_key: &SecretKey, 116 | node: &Arc>, 117 | prev_server: DalekPublicKey, 118 | next_server: Option<&IntegrationMixServer>, 119 | ) -> IntegrationMixServer 120 | where 121 | R: tor_rtcompat::Runtime, 122 | { 123 | let wallet = wallets.async_new_wallet(&node.lock().api_address()).await; 124 | let server_config = mwixnet::ServerConfig { 125 | key: server_key.clone(), 126 | interval_s: 15, 127 | addr: TcpListener::bind("127.0.0.1:0") 128 | .unwrap() 129 | .local_addr() 130 | .unwrap(), 131 | grin_node_url: node.lock().api_address().to_string(), 132 | grin_node_secret_path: None, 133 | wallet_owner_url: wallet.lock().owner_address().to_string(), 134 | wallet_owner_secret_path: None, 135 | prev_server: Some(prev_server), 136 | next_server: match next_server { 137 | Some(s) => Some(DalekPublicKey::from_secret(&s.server_key)), 138 | None => None, 139 | }, 140 | }; 141 | 142 | let tor_instance = tor::async_init_tor(tor_runtime, &data_dir, &server_config) 143 | .await 144 | .unwrap(); 145 | let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance)); 146 | 147 | let (_, rpc_server) = mwixnet::mix_listen( 148 | rt_handle, 149 | server_config.clone(), 150 | match next_server { 151 | Some(s) => Some(Arc::new(MixClientImpl::new( 152 | server_config.clone(), 153 | tor_instance.clone(), 154 | DalekPublicKey::from_secret(&s.server_key), 155 | ))), 156 | None => None, 157 | }, 158 | wallet.lock().get_client(), 159 | node.lock().to_client(), 160 | ) 161 | .unwrap(); 162 | 163 | IntegrationMixServer { 164 | server_key: server_key.clone(), 165 | tor_instance, 166 | rpc_server, 167 | _wallet: wallet, 168 | } 169 | } 170 | 171 | pub struct Servers { 172 | pub swapper: IntegrationSwapServer, 173 | 174 | pub mixers: Vec>, 175 | } 176 | 177 | impl Servers { 178 | pub async fn async_setup( 179 | test_dir: &str, 180 | rt_handle: &tokio::runtime::Handle, 181 | wallets: &mut GrinWalletManager, 182 | node: &Arc>, 183 | num_mixers: usize, 184 | ) -> Servers { 185 | // Pre-generate all server keys 186 | let server_keys: Vec = 187 | iter::repeat_with(|| grin_onion::crypto::secp::random_secret()) 188 | .take(num_mixers + 1) 189 | .collect(); 190 | 191 | // Setup mock tor network 192 | let tor_runtime = PreferredRuntime::current().unwrap(); 193 | 194 | // Build mixers in reverse order 195 | let mut mixers = Vec::new(); 196 | for i in (0..num_mixers).rev() { 197 | let mix_server = async_new_mix_server( 198 | format!("{}/mixers/{}", test_dir, i).as_str(), 199 | rt_handle, 200 | tor_runtime.clone(), 201 | wallets, 202 | &server_keys[i + 1], 203 | &node, 204 | DalekPublicKey::from_secret(&server_keys[i]), 205 | mixers.last(), 206 | ) 207 | .await; 208 | println!( 209 | "Mixer {}: server_key={}, prev_server={}, next_server={}", 210 | i, 211 | DalekPublicKey::from_secret(&server_keys[i + 1]).to_hex(), 212 | DalekPublicKey::from_secret(&server_keys[i]).to_hex(), 213 | match mixers.last() { 214 | Some(s) => DalekPublicKey::from_secret(&s.server_key).to_hex(), 215 | None => "NONE".to_string(), 216 | }, 217 | ); 218 | mixers.push(mix_server); 219 | } 220 | mixers.reverse(); 221 | 222 | let swapper = async_new_swap_server( 223 | format!("{}/swapper", test_dir).as_str(), 224 | rt_handle, 225 | tor_runtime.clone(), 226 | wallets, 227 | &server_keys[0], 228 | &node, 229 | mixers.first(), 230 | ) 231 | .await; 232 | println!( 233 | "Swapper: server_key={}", 234 | DalekPublicKey::from_secret(&server_keys[0]).to_hex() 235 | ); 236 | 237 | Servers { swapper, mixers } 238 | } 239 | 240 | pub fn get_pub_keys(&self) -> Vec { 241 | let mut pub_keys = vec![xPublicKey::from(&StaticSecret::from( 242 | self.swapper.server_key.0.clone(), 243 | ))]; 244 | for mixer in &self.mixers { 245 | pub_keys.push(xPublicKey::from(&StaticSecret::from( 246 | mixer.server_key.0.clone(), 247 | ))) 248 | } 249 | pub_keys 250 | } 251 | 252 | pub fn stop_all(&mut self) { 253 | self.swapper.rpc_server.close_handle().close(); 254 | self.swapper.tor_instance.lock().stop(); 255 | 256 | self.mixers.iter_mut().for_each(|mixer| { 257 | mixer.rpc_server.close_handle().close(); 258 | mixer.tor_instance.lock().stop(); 259 | }); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/tx.rs: -------------------------------------------------------------------------------- 1 | use crate::wallet::Wallet; 2 | 3 | use grin_core::core::{ 4 | FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel, 5 | }; 6 | use grin_keychain::BlindingFactor; 7 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 8 | use grin_onion::crypto::secp; 9 | use secp256k1zkp::{ContextFlag, Secp256k1, SecretKey}; 10 | use serde::{Deserialize, Serialize}; 11 | use std::sync::Arc; 12 | use thiserror::Error; 13 | 14 | /// Error types for interacting with wallets 15 | #[derive(Error, Debug)] 16 | pub enum TxError { 17 | #[error("Error computing transactions's offset: {0:?}")] 18 | OffsetError(secp256k1zkp::Error), 19 | #[error("Error building kernel's fee fields: {0:?}")] 20 | KernelFeeError(grin_core::core::transaction::Error), 21 | #[error("Error computing kernel's excess: {0:?}")] 22 | KernelExcessError(secp256k1zkp::Error), 23 | #[error("Error computing kernel's signature message: {0:?}")] 24 | KernelSigMessageError(grin_core::core::transaction::Error), 25 | #[error("Error signing kernel: {0:?}")] 26 | KernelSigError(secp256k1zkp::Error), 27 | #[error("Built kernel failed to verify: {0:?}")] 28 | KernelVerifyError(grin_core::core::transaction::Error), 29 | #[error("Output blinding factor is invalid: {0:?}")] 30 | OutputBlindError(secp256k1zkp::Error), 31 | #[error("Wallet error: {0:?}")] 32 | WalletError(crate::wallet::WalletError), 33 | } 34 | 35 | /// A collection of transaction components 36 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 37 | pub struct TxComponents { 38 | /// Transaction offset 39 | pub offset: SecretKey, 40 | /// Transaction kernels 41 | pub kernels: Vec, 42 | /// Transaction outputs 43 | pub outputs: Vec, 44 | } 45 | 46 | /// Builds and verifies the finalized swap 'Transaction' using the provided components. 47 | pub async fn async_assemble_tx( 48 | wallet: &Arc, 49 | inputs: &Vec, 50 | outputs: &Vec, 51 | kernels: &Vec, 52 | fee_base: u64, 53 | fees_paid: u64, 54 | prev_offset: &SecretKey, 55 | output_excesses: &Vec, 56 | ) -> Result { 57 | // calculate minimum fee required for the kernel 58 | let min_kernel_fee = 59 | TransactionBody::weight_by_iok(inputs.len() as u64, outputs.len() as u64, 1) * fee_base; 60 | 61 | let components = async_add_kernel_and_collect_fees( 62 | &wallet, 63 | &outputs, 64 | &kernels, 65 | fee_base, 66 | min_kernel_fee, 67 | fees_paid, 68 | &prev_offset, 69 | &output_excesses, 70 | ) 71 | .await?; 72 | 73 | // assemble the transaction 74 | let tx = Transaction::new( 75 | Inputs::from(inputs.as_slice()), 76 | &components.outputs, 77 | &components.kernels, 78 | ) 79 | .with_offset(BlindingFactor::from_secret_key(components.offset)); 80 | Ok(tx) 81 | } 82 | 83 | /// Adds a kernel and output to a collection of transaction components to consume fees and offset excesses. 84 | pub async fn async_assemble_components( 85 | wallet: &Arc, 86 | components: &TxComponents, 87 | output_excesses: &Vec, 88 | fee_base: u64, 89 | fees_paid: u64, 90 | ) -> Result { 91 | // calculate minimum fee required for the kernel 92 | let min_kernel_fee = TransactionBody::weight_by_iok(0, 0, 1) * fee_base; 93 | 94 | async_add_kernel_and_collect_fees( 95 | &wallet, 96 | &components.outputs, 97 | &components.kernels, 98 | fee_base, 99 | min_kernel_fee, 100 | fees_paid, 101 | &components.offset, 102 | &output_excesses, 103 | ) 104 | .await 105 | } 106 | 107 | async fn async_add_kernel_and_collect_fees( 108 | wallet: &Arc, 109 | outputs: &Vec, 110 | kernels: &Vec, 111 | fee_base: u64, 112 | min_kernel_fee: u64, 113 | fees_paid: u64, 114 | prev_offset: &SecretKey, 115 | output_excesses: &Vec, 116 | ) -> Result { 117 | let secp = Secp256k1::with_caps(ContextFlag::Commit); 118 | let mut txn_outputs = outputs.clone(); 119 | let mut txn_excesses = output_excesses.clone(); 120 | let mut txn_kernels = kernels.clone(); 121 | let mut kernel_fee = fees_paid; 122 | 123 | // calculate fee required if we add our own output 124 | let fee_to_collect = TransactionBody::weight_by_iok(0, 1, 0) * fee_base; 125 | 126 | // calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it 127 | let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base; 128 | 129 | // collect any leftover fees 130 | if fees_paid > min_kernel_fee + fee_to_collect + fee_to_spend { 131 | let amount = fees_paid - (min_kernel_fee + fee_to_collect); 132 | kernel_fee -= amount; 133 | 134 | let wallet_output = wallet 135 | .async_build_output(amount) 136 | .await 137 | .map_err(TxError::WalletError)?; 138 | txn_outputs.push(wallet_output.1); 139 | 140 | let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref()) 141 | .map_err(TxError::OutputBlindError)?; 142 | txn_excesses.push(output_excess); 143 | } 144 | 145 | // generate random transaction offset 146 | let our_offset = secp::random_secret(); 147 | let txn_offset = secp 148 | .blind_sum(vec![prev_offset.clone(), our_offset.clone()], Vec::new()) 149 | .map_err(TxError::OffsetError)?; 150 | 151 | // calculate kernel excess 152 | let kern_excess = secp 153 | .blind_sum(txn_excesses, vec![our_offset.clone()]) 154 | .map_err(TxError::KernelExcessError)?; 155 | 156 | // build and verify kernel 157 | let kernel = build_kernel(&kern_excess, kernel_fee)?; 158 | txn_kernels.push(kernel); 159 | 160 | // Sort outputs & kernels by commitment 161 | txn_kernels.sort_by(|a, b| a.excess.partial_cmp(&b.excess).unwrap()); 162 | txn_outputs.sort_by(|a, b| { 163 | a.identifier 164 | .commit 165 | .partial_cmp(&b.identifier.commit) 166 | .unwrap() 167 | }); 168 | 169 | Ok(TxComponents { 170 | offset: txn_offset, 171 | kernels: txn_kernels, 172 | outputs: txn_outputs, 173 | }) 174 | } 175 | 176 | /// Builds a transaction kernel for the Grin network. 177 | /// 178 | /// Transaction kernels are a critical part of the Grin transaction process. Each transaction contains a 179 | /// kernel. It includes features chosen for this transaction, a fee chosen for this transaction, and 180 | /// a proof that the total sum of outputs, transaction fees and block reward equals the total sum of inputs. 181 | /// The `build_kernel` function handles this process, building the kernel and handling any potential errors. 182 | /// 183 | /// # Arguments 184 | /// 185 | /// * `excess`: A reference to a `SecretKey`. This key is used as an excess value for the transaction. 186 | /// The excess is a kind of cryptographic proof that the total sum of outputs and fees equals the 187 | /// total sum of inputs. 188 | /// * `fee`: An unsigned 64-bit integer representing the transaction fee in nanogrin. This is the fee 189 | /// that will be paid to the miner who mines the block containing this transaction. 190 | /// 191 | /// # Returns 192 | /// 193 | /// The function returns a `Result` enum with `TxKernel` as the Ok variant and `TxError` as the Err variant. 194 | /// If the kernel is successfully built, it is returned as part of the Ok variant. If there is an error at any point 195 | /// during the process, it is returned as part of the Err variant. 196 | /// 197 | /// # Errors 198 | /// 199 | /// This function can return several types of errors, all defined in the `TxError` enum. These include: 200 | /// 201 | /// * `KernelFeeError`: There was an error building the kernel's fee fields. 202 | /// * `KernelExcessError`: There was an error computing the kernel's excess. 203 | /// * `KernelSigMessageError`: There was an error computing the kernel's signature message. 204 | /// * `KernelSigError`: There was an error signing the kernel. 205 | /// * `KernelVerifyError`: The built kernel failed to verify. 206 | /// 207 | /// # Example 208 | /// 209 | /// ```rust 210 | /// use secp256k1zkp::key::SecretKey; 211 | /// use secp256k1zkp::rand::thread_rng; 212 | /// use grin_onion::crypto::secp; 213 | /// 214 | /// let secret_key = secp::random_secret(); 215 | /// let fee = 10; // 10 nanogrin 216 | /// let kernel = mwixnet::tx::build_kernel(&secret_key, fee); 217 | /// ``` 218 | pub fn build_kernel(excess: &SecretKey, fee: u64) -> Result { 219 | let mut kernel = TxKernel::with_features(KernelFeatures::Plain { 220 | fee: FeeFields::new(0, fee).map_err(TxError::KernelFeeError)?, 221 | }); 222 | let msg = kernel 223 | .msg_to_sign() 224 | .map_err(TxError::KernelSigMessageError)?; 225 | kernel.excess = secp::commit(0, &excess).map_err(TxError::KernelExcessError)?; 226 | kernel.excess_sig = secp::sign(&excess, &msg).map_err(TxError::KernelSigError)?; 227 | kernel.verify().map_err(TxError::KernelVerifyError)?; 228 | 229 | Ok(kernel) 230 | } 231 | -------------------------------------------------------------------------------- /src/servers/swap_rpc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::FutureExt; 4 | use jsonrpc_core::{BoxFuture, Value}; 5 | use jsonrpc_derive::rpc; 6 | use jsonrpc_http_server::{DomainsValidation, ServerBuilder}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 10 | use grin_onion::crypto::comsig::{self, ComSignature}; 11 | use grin_onion::onion::Onion; 12 | 13 | use crate::config::ServerConfig; 14 | use crate::mix_client::MixClient; 15 | use crate::node::GrinNode; 16 | use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl}; 17 | use crate::store::SwapStore; 18 | use crate::wallet::Wallet; 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct SwapReq { 22 | onion: Onion, 23 | #[serde(with = "comsig::comsig_serde")] 24 | comsig: ComSignature, 25 | } 26 | 27 | #[rpc(server)] 28 | pub trait SwapAPI { 29 | #[rpc(name = "swap")] 30 | fn swap(&self, swap: SwapReq) -> BoxFuture>; 31 | } 32 | 33 | #[derive(Clone)] 34 | struct RPCSwapServer { 35 | server_config: ServerConfig, 36 | server: Arc>, 37 | } 38 | 39 | impl RPCSwapServer { 40 | /// Spin up an instance of the JSON-RPC HTTP server. 41 | fn start_http(&self, runtime_handle: tokio::runtime::Handle) -> jsonrpc_http_server::Server { 42 | let mut io = jsonrpc_core::IoHandler::new(); 43 | io.extend_with(RPCSwapServer::to_delegate(self.clone())); 44 | 45 | ServerBuilder::new(io) 46 | .event_loop_executor(runtime_handle) 47 | .cors(DomainsValidation::Disabled) 48 | .request_middleware(|request: hyper::Request| { 49 | if request.uri() == "/v1" { 50 | request.into() 51 | } else { 52 | jsonrpc_http_server::Response::bad_request("Only v1 supported").into() 53 | } 54 | }) 55 | .start_http(&self.server_config.addr) 56 | .expect("Unable to start RPC server") 57 | } 58 | } 59 | 60 | impl From for jsonrpc_core::Error { 61 | fn from(e: SwapError) -> Self { 62 | match e { 63 | SwapError::UnknownError(_) => jsonrpc_core::Error { 64 | message: e.to_string(), 65 | code: jsonrpc_core::ErrorCode::InternalError, 66 | data: None, 67 | }, 68 | _ => jsonrpc_core::Error::invalid_params(e.to_string()), 69 | } 70 | } 71 | } 72 | 73 | impl SwapAPI for RPCSwapServer { 74 | fn swap(&self, swap: SwapReq) -> BoxFuture> { 75 | let server = self.server.clone(); 76 | async move { 77 | server.lock().await.swap(&swap.onion, &swap.comsig).await?; 78 | Ok(Value::String("success".into())) 79 | } 80 | .boxed() 81 | } 82 | } 83 | 84 | /// Spin up the JSON-RPC web server 85 | pub fn listen( 86 | rt_handle: &tokio::runtime::Handle, 87 | server_config: &ServerConfig, 88 | next_server: Option>, 89 | wallet: Arc, 90 | node: Arc, 91 | store: SwapStore, 92 | ) -> std::result::Result< 93 | ( 94 | Arc>, 95 | jsonrpc_http_server::Server, 96 | ), 97 | Box, 98 | > { 99 | let server = SwapServerImpl::new( 100 | server_config.clone(), 101 | next_server, 102 | wallet.clone(), 103 | node.clone(), 104 | store, 105 | ); 106 | let server = Arc::new(tokio::sync::Mutex::new(server)); 107 | 108 | let rpc_server = RPCSwapServer { 109 | server_config: server_config.clone(), 110 | server: server.clone(), 111 | }; 112 | 113 | let http_server = rpc_server.start_http(rt_handle.clone()); 114 | 115 | Ok((server, http_server)) 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use std::net::TcpListener; 121 | use std::sync::Arc; 122 | 123 | use hyper::{Body, Client, Request, Response}; 124 | use tokio::sync::Mutex; 125 | 126 | use grin_onion::create_onion; 127 | use grin_onion::crypto::comsig::ComSignature; 128 | use grin_onion::crypto::secp; 129 | 130 | use crate::config::ServerConfig; 131 | use crate::servers::swap::{SwapError, SwapServer}; 132 | use crate::servers::swap::mock::MockSwapServer; 133 | use crate::servers::swap_rpc::{RPCSwapServer, SwapReq}; 134 | 135 | async fn body_to_string(req: Response) -> String { 136 | let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap(); 137 | String::from_utf8(body_bytes.to_vec()).unwrap() 138 | } 139 | 140 | /// Spin up a temporary web service, query the API, then cleanup and return response 141 | async fn async_make_request( 142 | server: Arc>, 143 | req: String, 144 | runtime_handle: &tokio::runtime::Handle, 145 | ) -> Result> { 146 | let server_config = ServerConfig { 147 | key: secp::random_secret(), 148 | interval_s: 1, 149 | addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, 150 | grin_node_url: "127.0.0.1:3413".parse()?, 151 | grin_node_secret_path: None, 152 | wallet_owner_url: "127.0.0.1:3420".parse()?, 153 | wallet_owner_secret_path: None, 154 | prev_server: None, 155 | next_server: None, 156 | }; 157 | 158 | let rpc_server = RPCSwapServer { 159 | server_config: server_config.clone(), 160 | server: server.clone(), 161 | }; 162 | 163 | // Start the JSON-RPC server 164 | let http_server = rpc_server.start_http(runtime_handle.clone()); 165 | 166 | let uri = format!("http://{}/v1", server_config.addr); 167 | 168 | let request = Request::post(uri) 169 | .header("Content-Type", "application/json") 170 | .body(Body::from(req)) 171 | .unwrap(); 172 | 173 | let response = Client::new().request(request).await?; 174 | 175 | let response_str: String = body_to_string(response).await; 176 | 177 | // Execute one round 178 | server.lock().await.execute_round().await?; 179 | 180 | // Stop the server 181 | http_server.close(); 182 | 183 | Ok(response_str) 184 | } 185 | 186 | // todo: Test all error types 187 | 188 | /// Demonstrates a successful swap response 189 | #[test] 190 | fn swap_success() -> Result<(), Box> { 191 | let rt = tokio::runtime::Builder::new_multi_thread() 192 | .enable_all() 193 | .build()?; 194 | let commitment = secp::commit(1234, &secp::random_secret())?; 195 | let onion = create_onion(&commitment, &vec![])?; 196 | let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?; 197 | let swap = SwapReq { 198 | onion: onion.clone(), 199 | comsig, 200 | }; 201 | 202 | let server: Arc> = Arc::new(Mutex::new(MockSwapServer::new())); 203 | 204 | let req = format!( 205 | "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", 206 | serde_json::json!(swap) 207 | ); 208 | let rt_handle = rt.handle().clone(); 209 | let response = rt.block_on(async_make_request(server, req, &rt_handle))?; 210 | let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n"; 211 | assert_eq!(response, expected); 212 | 213 | Ok(()) 214 | } 215 | 216 | #[test] 217 | fn swap_bad_request() -> Result<(), Box> { 218 | let rt = tokio::runtime::Builder::new_multi_thread() 219 | .enable_all() 220 | .build()?; 221 | let server: Arc> = Arc::new(Mutex::new(MockSwapServer::new())); 222 | 223 | let params = "{ \"param\": \"Not a valid Swap request\" }"; 224 | let req = format!( 225 | "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", 226 | params 227 | ); 228 | let rt_handle = rt.handle().clone(); 229 | let response = rt.block_on(async_make_request(server, req, &rt_handle))?; 230 | let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n"; 231 | assert_eq!(response, expected); 232 | Ok(()) 233 | } 234 | 235 | /// Returns "Commitment not found" when there's no matching output in the UTXO set. 236 | #[test] 237 | fn swap_utxo_missing() -> Result<(), Box> { 238 | let rt = tokio::runtime::Builder::new_multi_thread() 239 | .enable_all() 240 | .build()?; 241 | 242 | let commitment = secp::commit(1234, &secp::random_secret())?; 243 | let onion = create_onion(&commitment, &vec![])?; 244 | let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?; 245 | let swap = SwapReq { 246 | onion: onion.clone(), 247 | comsig, 248 | }; 249 | 250 | let mut server = MockSwapServer::new(); 251 | server.set_response( 252 | &onion, 253 | SwapError::CoinNotFound { 254 | commit: commitment.clone(), 255 | }, 256 | ); 257 | let server: Arc> = Arc::new(Mutex::new(server)); 258 | 259 | let req = format!( 260 | "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", 261 | serde_json::json!(swap) 262 | ); 263 | let rt_handle = rt.handle().clone(); 264 | let response = rt.block_on(async_make_request(server, req, &rt_handle))?; 265 | let expected = format!( 266 | "{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n", 267 | commitment 268 | ); 269 | assert_eq!(response, expected); 270 | Ok(()) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/common/miner.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Grin Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Mining service, gets a block to mine, and based on mining configuration 16 | //! chooses a version of the cuckoo miner to mine the block and produce a valid 17 | //! header with its proof-of-work. Any valid mined blocks are submitted to the 18 | //! network. 19 | 20 | use crate::common::types::BlockFees; 21 | use crate::common::wallet::IntegrationGrinWallet; 22 | use chrono::prelude::Utc; 23 | use chrono::{DateTime, NaiveDateTime}; 24 | use grin_chain::Chain; 25 | use grin_core::core::hash::{Hash, Hashed}; 26 | use grin_core::core::{Block, BlockHeader, Transaction}; 27 | use grin_core::{consensus, global}; 28 | use grin_keychain::Identifier; 29 | use grin_util::Mutex; 30 | use rand::{thread_rng, Rng}; 31 | use std::sync::Arc; 32 | use std::time::Duration; 33 | 34 | pub struct Miner { 35 | chain: Arc, 36 | } 37 | 38 | impl Miner { 39 | // Creates a new Miner. Needs references to the chain state and its 40 | /// storage. 41 | pub fn new(chain: Arc) -> Miner { 42 | Miner { chain } 43 | } 44 | 45 | pub async fn async_mine_empty_blocks( 46 | &self, 47 | wallet: &Arc>, 48 | num_blocks: usize, 49 | ) { 50 | for _ in 0..num_blocks { 51 | self.async_mine_next_block(wallet, &vec![]).await; 52 | } 53 | } 54 | 55 | /// Builds a new block on top of the existing chain. 56 | pub async fn async_mine_next_block( 57 | &self, 58 | wallet: &Arc>, 59 | txs: &Vec, 60 | ) { 61 | info!("Starting test miner loop."); 62 | 63 | // iteration, we keep the returned derivation to provide it back when 64 | // nothing has changed. We only want to create a new key_id for each new block. 65 | let mut key_id = None; 66 | 67 | loop { 68 | // get the latest chain state and build a block on top of it 69 | let head = self.chain.head_header().unwrap(); 70 | let mut latest_hash = self.chain.head().unwrap().last_block_h; 71 | 72 | let (mut b, block_fees) = self.async_get_block(wallet, txs, key_id.clone()).await; 73 | let sol = self.inner_mining_loop(&mut b, &head, &mut latest_hash); 74 | 75 | // we found a solution, push our block through the chain processing pipeline 76 | if sol { 77 | info!( 78 | "Found valid proof of work, adding block {} (prev_root {}).", 79 | b.hash(), 80 | b.header.prev_root, 81 | ); 82 | let res = self.chain.process_block(b, grin_chain::Options::MINE); 83 | if let Err(e) = res { 84 | error!("Error validating mined block: {:?}", e); 85 | } else { 86 | return; 87 | } 88 | key_id = None; 89 | } else { 90 | key_id = block_fees.key_id(); 91 | } 92 | } 93 | } 94 | 95 | /// The inner part of mining loop for the internal miner 96 | /// kept around mostly for automated testing purposes 97 | fn inner_mining_loop(&self, b: &mut Block, head: &BlockHeader, latest_hash: &mut Hash) -> bool { 98 | while head.hash() == *latest_hash { 99 | let mut ctx = global::create_pow_context::( 100 | head.height, 101 | global::min_edge_bits(), 102 | global::proofsize(), 103 | 10, 104 | ) 105 | .unwrap(); 106 | ctx.set_header_nonce(b.header.pre_pow(), None, true) 107 | .unwrap(); 108 | if let Ok(proofs) = ctx.find_cycles() { 109 | b.header.pow.proof = proofs[0].clone(); 110 | let proof_diff = b.header.pow.to_difficulty(b.header.height); 111 | if proof_diff >= (b.header.total_difficulty() - head.total_difficulty()) { 112 | return true; 113 | } 114 | } 115 | 116 | b.header.pow.nonce += 1; 117 | *latest_hash = self.chain.head().unwrap().last_block_h; 118 | } 119 | 120 | false 121 | } 122 | 123 | // Ensure a block suitable for mining is built and returned 124 | // If a wallet listener URL is not provided the reward will be "burnt" 125 | // Warning: This call does not return until/unless a new block can be built 126 | async fn async_get_block( 127 | &self, 128 | wallet: &Arc>, 129 | txs: &Vec, 130 | key_id: Option, 131 | ) -> (Block, BlockFees) { 132 | let wallet_retry_interval = 5; 133 | // get the latest chain state and build a block on top of it 134 | let mut result = self.async_build_block(wallet, txs, key_id.clone()).await; 135 | while let Err(e) = result { 136 | println!("Error: {:?}", &e); 137 | let mut new_key_id = key_id.to_owned(); 138 | match e { 139 | grin_servers::common::types::Error::Chain(c) => match c { 140 | grin_chain::Error::DuplicateCommitment(_) => { 141 | debug!( 142 | "Duplicate commit for potential coinbase detected. Trying next derivation." 143 | ); 144 | // use the next available key to generate a different coinbase commitment 145 | new_key_id = None; 146 | } 147 | _ => { 148 | error!("Chain Error: {}", c); 149 | } 150 | }, 151 | grin_servers::common::types::Error::WalletComm(_) => { 152 | error!( 153 | "Error building new block: Can't connect to wallet listener; will retry" 154 | ); 155 | async_std::task::sleep(Duration::from_secs(wallet_retry_interval)).await; 156 | } 157 | ae => { 158 | warn!("Error building new block: {:?}. Retrying.", ae); 159 | } 160 | } 161 | 162 | // only wait if we are still using the same key: a different coinbase commitment is unlikely 163 | // to have duplication 164 | if new_key_id.is_some() { 165 | async_std::task::sleep(Duration::from_millis(100)).await; 166 | } 167 | 168 | result = self.async_build_block(wallet, txs, new_key_id).await; 169 | } 170 | return result.unwrap(); 171 | } 172 | 173 | /// Builds a new block with the chain head as previous and eligible 174 | /// transactions from the pool. 175 | async fn async_build_block( 176 | &self, 177 | wallet: &Arc>, 178 | txs: &Vec, 179 | key_id: Option, 180 | ) -> Result<(Block, BlockFees), grin_servers::common::types::Error> { 181 | let head = self.chain.head_header()?; 182 | 183 | // prepare the block header timestamp 184 | let mut now_sec = Utc::now().timestamp(); 185 | let head_sec = head.timestamp.timestamp(); 186 | if now_sec <= head_sec { 187 | now_sec = head_sec + 1; 188 | } 189 | 190 | // Determine the difficulty our block should be at. 191 | // Note: do not keep the difficulty_iter in scope (it has an active batch). 192 | let difficulty = consensus::next_difficulty(head.height + 1, self.chain.difficulty_iter()?); 193 | 194 | // build the coinbase and the block itself 195 | let fees = txs.iter().map(|tx| tx.fee()).sum(); 196 | let height = head.height + 1; 197 | let block_fees = BlockFees { 198 | fees, 199 | key_id, 200 | height, 201 | }; 202 | 203 | let res = wallet.lock().async_create_coinbase(&block_fees).await?; 204 | let output = res.output; 205 | let kernel = res.kernel; 206 | let block_fees = BlockFees { 207 | key_id: res.key_id, 208 | ..block_fees 209 | }; 210 | let mut b = Block::from_reward(&head, &txs, output, kernel, difficulty.difficulty)?; 211 | 212 | // making sure we're not spending time mining a useless block 213 | b.validate(&head.total_kernel_offset)?; 214 | 215 | b.header.pow.nonce = thread_rng().gen(); 216 | b.header.pow.secondary_scaling = difficulty.secondary_scaling; 217 | b.header.timestamp = DateTime::::from_naive_utc_and_offset( 218 | NaiveDateTime::from_timestamp_opt(now_sec, 0).unwrap(), 219 | Utc, 220 | ); 221 | 222 | debug!( 223 | "Built new block with {} inputs and {} outputs, block difficulty: {}, cumulative difficulty {}", 224 | b.inputs().len(), 225 | b.outputs().len(), 226 | difficulty.difficulty, 227 | b.header.total_difficulty().to_num(), 228 | ); 229 | 230 | // Now set txhashset roots and sizes on the header of the block being built. 231 | match self.chain.set_txhashset_roots(&mut b) { 232 | Ok(_) => Ok((b, block_fees)), 233 | Err(e) => { 234 | match e { 235 | // If this is a duplicate commitment then likely trying to use 236 | // a key that hass already been derived but not in the wallet 237 | // for some reason, allow caller to retry. 238 | grin_chain::Error::DuplicateCommitment(e) => { 239 | Err(grin_servers::common::types::Error::Chain( 240 | grin_chain::Error::DuplicateCommitment(e), 241 | )) 242 | } 243 | 244 | // Some other issue, possibly duplicate kernel 245 | _ => { 246 | error!("Error setting txhashset root to build a block: {:?}", e); 247 | Err(grin_servers::common::types::Error::Chain( 248 | grin_chain::Error::Other(format!("{:?}", e)), 249 | )) 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::net::ToSocketAddrs; 3 | use std::sync::Arc; 4 | 5 | use async_trait::async_trait; 6 | use grin_api::LocatedTxKernel; 7 | use grin_api::{OutputPrintable, OutputType, Tip}; 8 | use grin_core::consensus::COINBASE_MATURITY; 9 | use grin_core::core::hash::Hash; 10 | use grin_core::core::{Committed, Input, OutputFeatures, Transaction}; 11 | use grin_util::ToHex; 12 | use serde_json::json; 13 | use thiserror::Error; 14 | 15 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 16 | use grin_onion::crypto::secp::Commitment; 17 | 18 | use crate::http; 19 | 20 | #[async_trait] 21 | pub trait GrinNode: Send + Sync { 22 | /// Retrieves the unspent output with a matching commitment 23 | async fn async_get_utxo( 24 | &self, 25 | output_commit: &Commitment, 26 | ) -> Result, NodeError>; 27 | 28 | /// Gets the height and hash of the chain tip 29 | async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>; 30 | 31 | /// Posts a transaction to the grin node 32 | async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>; 33 | 34 | /// Returns a LocatedTxKernel based on the kernel excess. 35 | /// The min_height and max_height parameters are both optional. 36 | /// If not supplied, min_height will be set to 0 and max_height will be set to the head of the chain. 37 | /// The method will start at the block height max_height and traverse the kernel MMR backwards, until either the kernel is found or min_height is reached. 38 | async fn async_get_kernel( 39 | &self, 40 | excess: &Commitment, 41 | min_height: Option, 42 | max_height: Option, 43 | ) -> Result, NodeError>; 44 | } 45 | 46 | /// Error types for interacting with nodes 47 | #[derive(Error, Debug)] 48 | pub enum NodeError { 49 | #[error("Error decoding JSON response: {0:?}")] 50 | DecodeResponseError(serde_json::Error), 51 | #[error("JSON-RPC API communication error: {0:?}")] 52 | ApiCommError(grin_api::Error), 53 | #[error("Client error: {0:?}")] 54 | NodeCommError(http::HttpError), 55 | #[error("Error decoding JSON-RPC response: {0:?}")] 56 | ResponseParseError(grin_api::json_rpc::Error), 57 | } 58 | 59 | /// Checks if a commitment is in the UTXO set 60 | pub async fn async_is_unspent( 61 | node: &Arc, 62 | commit: &Commitment, 63 | ) -> Result { 64 | let utxo = node.async_get_utxo(&commit).await?; 65 | Ok(utxo.is_some()) 66 | } 67 | 68 | /// Checks whether a commitment is spendable at the block height provided 69 | pub async fn async_is_spendable( 70 | node: &Arc, 71 | commit: &Commitment, 72 | next_block_height: u64, 73 | ) -> Result { 74 | let output = node.async_get_utxo(&commit).await?; 75 | if let Some(out) = output { 76 | let is_coinbase = match out.output_type { 77 | OutputType::Coinbase => true, 78 | OutputType::Transaction => false, 79 | }; 80 | 81 | if is_coinbase { 82 | if let Some(block_height) = out.block_height { 83 | if block_height + COINBASE_MATURITY < next_block_height { 84 | return Ok(false); 85 | } 86 | } else { 87 | return Ok(false); 88 | } 89 | } 90 | 91 | return Ok(true); 92 | } 93 | 94 | Ok(false) 95 | } 96 | 97 | /// Builds an input for an unspent output commitment 98 | pub async fn async_build_input( 99 | node: &Arc, 100 | output_commit: &Commitment, 101 | ) -> Result, NodeError> { 102 | let output = node.async_get_utxo(&output_commit).await?; 103 | 104 | if let Some(out) = output { 105 | let features = match out.output_type { 106 | OutputType::Coinbase => OutputFeatures::Coinbase, 107 | OutputType::Transaction => OutputFeatures::Plain, 108 | }; 109 | 110 | let input = Input::new(features, out.commit); 111 | return Ok(Some(input)); 112 | } 113 | 114 | Ok(None) 115 | } 116 | 117 | pub async fn async_is_tx_valid( 118 | node: &Arc, 119 | tx: &Transaction, 120 | ) -> Result { 121 | let next_block_height = node.async_get_chain_tip().await?.0 + 1; 122 | for input_commit in &tx.inputs_committed() { 123 | if !async_is_spendable(&node, &input_commit, next_block_height).await? { 124 | return Ok(false); 125 | } 126 | } 127 | 128 | for output_commit in &tx.outputs_committed() { 129 | if async_is_unspent(&node, &output_commit).await? { 130 | return Ok(false); 131 | } 132 | } 133 | 134 | Ok(true) 135 | } 136 | 137 | /// HTTP (JSON-RPC) implementation of the 'GrinNode' trait 138 | #[derive(Clone)] 139 | pub struct HttpGrinNode { 140 | node_url: SocketAddr, 141 | node_api_secret: Option, 142 | } 143 | 144 | const ENDPOINT: &str = "/v2/foreign"; 145 | 146 | impl HttpGrinNode { 147 | pub fn new(node_url: &str, node_api_secret: &Option) -> HttpGrinNode { 148 | let mut addrs_iter = node_url.to_socket_addrs().unwrap(); 149 | let node_url = addrs_iter.next().unwrap(); 150 | HttpGrinNode { 151 | node_url, 152 | node_api_secret: node_api_secret.to_owned(), 153 | } 154 | } 155 | 156 | async fn async_send_request( 157 | &self, 158 | method: &str, 159 | params: &serde_json::Value, 160 | ) -> Result { 161 | let url = format!("http://{}{}", self.node_url, ENDPOINT); 162 | let parsed = http::async_send_json_request(&url, &self.node_api_secret, &method, ¶ms) 163 | .await 164 | .map_err(NodeError::NodeCommError)?; 165 | Ok(parsed) 166 | } 167 | } 168 | 169 | #[async_trait] 170 | impl GrinNode for HttpGrinNode { 171 | async fn async_get_utxo( 172 | &self, 173 | output_commit: &Commitment, 174 | ) -> Result, NodeError> { 175 | let commits: Vec = vec![output_commit.to_hex()]; 176 | let start_height: Option = None; 177 | let end_height: Option = None; 178 | let include_proof: Option = Some(false); 179 | let include_merkle_proof: Option = Some(false); 180 | 181 | let params = json!([ 182 | Some(commits), 183 | start_height, 184 | end_height, 185 | include_proof, 186 | include_merkle_proof 187 | ]); 188 | let outputs = self 189 | .async_send_request::>("get_outputs", ¶ms) 190 | .await?; 191 | if outputs.is_empty() { 192 | return Ok(None); 193 | } 194 | 195 | Ok(Some(outputs[0].clone())) 196 | } 197 | 198 | async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> { 199 | let params = json!([]); 200 | let tip_json = self 201 | .async_send_request::("get_tip", ¶ms) 202 | .await?; 203 | let tip = 204 | serde_json::from_value::(tip_json).map_err(NodeError::DecodeResponseError)?; 205 | 206 | Ok(( 207 | tip.height, 208 | Hash::from_hex(tip.last_block_pushed.as_str()).unwrap(), 209 | )) 210 | } 211 | 212 | async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> { 213 | let params = json!([tx, true]); 214 | self.async_send_request::("push_transaction", ¶ms) 215 | .await?; 216 | Ok(()) 217 | } 218 | 219 | async fn async_get_kernel( 220 | &self, 221 | excess: &Commitment, 222 | min_height: Option, 223 | max_height: Option, 224 | ) -> Result, NodeError> { 225 | let params = json!([excess.0.as_ref().to_hex(), min_height, max_height]); 226 | let value = self 227 | .async_send_request::("get_kernel", ¶ms) 228 | .await?; 229 | 230 | let contents = format!("{:?}", value); 231 | if contents.contains("NotFound") { 232 | return Ok(None); 233 | } 234 | 235 | let located_kernel = serde_json::from_value::(value) 236 | .map_err(NodeError::DecodeResponseError)?; 237 | Ok(Some(located_kernel)) 238 | } 239 | } 240 | 241 | #[cfg(test)] 242 | pub mod mock { 243 | use std::collections::HashMap; 244 | use std::sync::RwLock; 245 | 246 | use async_trait::async_trait; 247 | use grin_api::{LocatedTxKernel, OutputPrintable, OutputType}; 248 | use grin_core::core::hash::Hash; 249 | use grin_core::core::Transaction; 250 | 251 | use grin_onion::crypto::secp::Commitment; 252 | 253 | use super::{GrinNode, NodeError}; 254 | 255 | /// Implementation of 'GrinNode' trait that mocks a grin node instance. 256 | /// Use only for testing purposes. 257 | pub struct MockGrinNode { 258 | utxos: HashMap, 259 | txns_posted: RwLock>, 260 | kernels: HashMap, 261 | } 262 | 263 | impl MockGrinNode { 264 | pub fn new() -> Self { 265 | MockGrinNode { 266 | utxos: HashMap::new(), 267 | txns_posted: RwLock::new(Vec::new()), 268 | kernels: HashMap::new(), 269 | } 270 | } 271 | 272 | pub fn new_with_utxos(utxos: &Vec<&Commitment>) -> Self { 273 | let mut node = MockGrinNode { 274 | utxos: HashMap::new(), 275 | txns_posted: RwLock::new(Vec::new()), 276 | kernels: HashMap::new(), 277 | }; 278 | for utxo in utxos { 279 | node.add_default_utxo(utxo); 280 | } 281 | node 282 | } 283 | 284 | pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) { 285 | self.utxos.insert(output_commit.clone(), utxo.clone()); 286 | } 287 | 288 | pub fn add_default_utxo(&mut self, output_commit: &Commitment) { 289 | let utxo = OutputPrintable { 290 | output_type: OutputType::Transaction, 291 | commit: output_commit.to_owned(), 292 | spent: false, 293 | proof: None, 294 | proof_hash: String::from(""), 295 | block_height: None, 296 | merkle_proof: None, 297 | mmr_index: 0, 298 | }; 299 | 300 | self.add_utxo(&output_commit, &utxo); 301 | } 302 | 303 | pub fn get_posted_txns(&self) -> Vec { 304 | let read = self.txns_posted.read().unwrap(); 305 | read.clone() 306 | } 307 | 308 | pub fn add_kernel(&mut self, kernel: &LocatedTxKernel) { 309 | self.kernels 310 | .insert(kernel.tx_kernel.excess.clone(), kernel.clone()); 311 | } 312 | } 313 | 314 | #[async_trait] 315 | impl GrinNode for MockGrinNode { 316 | async fn async_get_utxo( 317 | &self, 318 | output_commit: &Commitment, 319 | ) -> Result, NodeError> { 320 | if let Some(utxo) = self.utxos.get(&output_commit) { 321 | return Ok(Some(utxo.clone())); 322 | } 323 | 324 | Ok(None) 325 | } 326 | 327 | async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> { 328 | Ok((100, Hash::default())) 329 | } 330 | 331 | async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> { 332 | let mut write = self.txns_posted.write().unwrap(); 333 | write.push(tx.clone()); 334 | Ok(()) 335 | } 336 | 337 | async fn async_get_kernel( 338 | &self, 339 | excess: &Commitment, 340 | _min_height: Option, 341 | _max_height: Option, 342 | ) -> Result, NodeError> { 343 | if let Some(kernel) = self.kernels.get(&excess) { 344 | return Ok(Some(kernel.clone())); 345 | } 346 | 347 | Ok(None) 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use core::num::NonZeroU32; 2 | use std::fs::File; 3 | use std::io::prelude::*; 4 | use std::net::SocketAddr; 5 | use std::path::PathBuf; 6 | use std::result::Result; 7 | 8 | use grin_core::global::ChainTypes; 9 | use grin_util::{file, ToHex, ZeroingString}; 10 | use grin_wallet_util::OnionV3Address; 11 | use rand::{Rng, thread_rng}; 12 | use ring::{aead, pbkdf2}; 13 | use serde_derive::{Deserialize, Serialize}; 14 | use thiserror::Error; 15 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 16 | 17 | use grin_onion::crypto::dalek::DalekPublicKey; 18 | use grin_onion::crypto::secp::SecretKey; 19 | 20 | const GRIN_HOME: &str = ".grin"; 21 | const NODE_API_SECRET_FILE_NAME: &str = ".api_secret"; 22 | const WALLET_OWNER_API_SECRET_FILE_NAME: &str = ".owner_api_secret"; 23 | 24 | /// The decrypted server config to be passed around and used by the rest of the mwixnet code 25 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 26 | pub struct ServerConfig { 27 | /// private key used by the server to decrypt onion packets 28 | pub key: SecretKey, 29 | /// interval (in seconds) to wait before each mixing round 30 | pub interval_s: u32, 31 | /// socket address the server listener should bind to 32 | pub addr: SocketAddr, 33 | /// foreign api address of the grin node 34 | pub grin_node_url: String, 35 | /// path to file containing api secret for the grin node 36 | pub grin_node_secret_path: Option, 37 | /// owner api address of the grin wallet 38 | pub wallet_owner_url: String, 39 | /// path to file containing secret for the grin wallet's owner api 40 | pub wallet_owner_secret_path: Option, 41 | /// public key of the previous mix/swap server (e.g. N_1 if this is N_2) 42 | #[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)] 43 | pub prev_server: Option, 44 | /// public key of the next mix server 45 | #[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)] 46 | pub next_server: Option, 47 | } 48 | 49 | impl ServerConfig { 50 | pub fn onion_address(&self) -> OnionV3Address { 51 | OnionV3Address::from_private(&self.key.0).unwrap() 52 | } 53 | 54 | pub fn server_pubkey(&self) -> DalekPublicKey { 55 | DalekPublicKey::from_secret(&self.key) 56 | } 57 | 58 | pub fn node_api_secret(&self) -> Option { 59 | file::get_first_line(self.grin_node_secret_path.clone()) 60 | } 61 | 62 | pub fn wallet_owner_api_secret(&self) -> Option { 63 | file::get_first_line(self.wallet_owner_secret_path.clone()) 64 | } 65 | } 66 | 67 | /// Error types for saving or loading configs 68 | #[derive(Error, Debug)] 69 | pub enum ConfigError { 70 | #[error("Error while writing config to file: {0:?}")] 71 | FileWriteError(std::io::Error), 72 | #[error("Error while encoding config as toml: {0:?}")] 73 | EncodingError(toml::ser::Error), 74 | #[error("Error while decoding toml config: {0:?}")] 75 | DecodingError(toml::de::Error), 76 | #[error("{0} not valid hex")] 77 | InvalidHex(String), 78 | #[error("Error decrypting seed: {0:?}")] 79 | DecryptionError(ring::error::Unspecified), 80 | #[error("Decrypted server key is invalid")] 81 | InvalidServerKey, 82 | #[error( 83 | "Unable to read server config. Perform init-config or pass in config path.\nError: {0:?}" 84 | )] 85 | ReadConfigError(std::io::Error), 86 | } 87 | 88 | /// Encrypted server key, for storing on disk and decrypting with a password. 89 | /// Includes a salt used by key derivation and a nonce used when sealing the encrypted data. 90 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 91 | struct EncryptedServerKey { 92 | encrypted_key: String, 93 | salt: String, 94 | nonce: String, 95 | } 96 | 97 | impl EncryptedServerKey { 98 | /// Generates a random salt for pbkdf2 key derivation and a random nonce for aead sealing. 99 | /// Then derives an encryption key from the password and salt. Finally, it encrypts and seals 100 | /// the server key with chacha20-poly1305 using the derived key and random nonce. 101 | pub fn from_secret_key(server_key: &SecretKey, password: &ZeroingString) -> EncryptedServerKey { 102 | let salt: [u8; 8] = thread_rng().gen(); 103 | let password = password.as_bytes(); 104 | let mut key = [0; 32]; 105 | pbkdf2::derive( 106 | pbkdf2::PBKDF2_HMAC_SHA512, 107 | NonZeroU32::new(100).unwrap(), 108 | &salt, 109 | password, 110 | &mut key, 111 | ); 112 | let content = server_key.0.to_vec(); 113 | let mut enc_bytes = content; 114 | 115 | let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); 116 | let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); 117 | let nonce: [u8; 12] = thread_rng().gen(); 118 | let aad = aead::Aad::from(&[]); 119 | let _ = sealing_key 120 | .seal_in_place_append_tag( 121 | aead::Nonce::assume_unique_for_key(nonce), 122 | aad, 123 | &mut enc_bytes, 124 | ) 125 | .unwrap(); 126 | 127 | EncryptedServerKey { 128 | encrypted_key: enc_bytes.to_hex(), 129 | salt: salt.to_hex(), 130 | nonce: nonce.to_hex(), 131 | } 132 | } 133 | 134 | /// Decrypt the server secret key using the provided password. 135 | pub fn decrypt(&self, password: &str) -> Result { 136 | let mut encrypted_seed = grin_util::from_hex(&self.encrypted_key.clone()) 137 | .map_err(|_| ConfigError::InvalidHex("Seed".to_string()))?; 138 | let salt = grin_util::from_hex(&self.salt.clone()) 139 | .map_err(|_| ConfigError::InvalidHex("Salt".to_string()))?; 140 | let nonce = grin_util::from_hex(&self.nonce.clone()) 141 | .map_err(|_| ConfigError::InvalidHex("Nonce".to_string()))?; 142 | let password = password.as_bytes(); 143 | let mut key = [0; 32]; 144 | pbkdf2::derive( 145 | pbkdf2::PBKDF2_HMAC_SHA512, 146 | NonZeroU32::new(100).unwrap(), 147 | &salt, 148 | password, 149 | &mut key, 150 | ); 151 | 152 | let mut n = [0u8; 12]; 153 | n.copy_from_slice(&nonce[0..12]); 154 | let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); 155 | let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); 156 | let aad = aead::Aad::from(&[]); 157 | let _ = opening_key 158 | .open_in_place( 159 | aead::Nonce::assume_unique_for_key(n), 160 | aad, 161 | &mut encrypted_seed, 162 | ) 163 | .map_err(|e| ConfigError::DecryptionError(e))?; 164 | 165 | for _ in 0..aead::AES_256_GCM.tag_len() { 166 | encrypted_seed.pop(); 167 | } 168 | 169 | let secp = secp256k1zkp::Secp256k1::new(); 170 | let decrypted = SecretKey::from_slice(&secp, &encrypted_seed) 171 | .map_err(|_| ConfigError::InvalidServerKey)?; 172 | Ok(decrypted) 173 | } 174 | } 175 | 176 | /// The config attributes saved to disk 177 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 178 | struct RawConfig { 179 | encrypted_key: String, 180 | salt: String, 181 | nonce: String, 182 | interval_s: u32, 183 | addr: SocketAddr, 184 | grin_node_url: String, 185 | grin_node_secret_path: Option, 186 | wallet_owner_url: String, 187 | wallet_owner_secret_path: Option, 188 | #[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)] 189 | prev_server: Option, 190 | #[serde(with = "grin_onion::crypto::dalek::option_dalek_pubkey_serde", default)] 191 | next_server: Option, 192 | } 193 | 194 | /// Writes the server config to the config_path given, encrypting the server_key first. 195 | pub fn write_config( 196 | config_path: &PathBuf, 197 | server_config: &ServerConfig, 198 | password: &ZeroingString, 199 | ) -> Result<(), ConfigError> { 200 | let encrypted = EncryptedServerKey::from_secret_key(&server_config.key, &password); 201 | 202 | let raw_config = RawConfig { 203 | encrypted_key: encrypted.encrypted_key, 204 | salt: encrypted.salt, 205 | nonce: encrypted.nonce, 206 | interval_s: server_config.interval_s, 207 | addr: server_config.addr, 208 | grin_node_url: server_config.grin_node_url.clone(), 209 | grin_node_secret_path: server_config.grin_node_secret_path.clone(), 210 | wallet_owner_url: server_config.wallet_owner_url.clone(), 211 | wallet_owner_secret_path: server_config.wallet_owner_secret_path.clone(), 212 | prev_server: server_config.prev_server.clone(), 213 | next_server: server_config.next_server.clone(), 214 | }; 215 | let encoded: String = 216 | toml::to_string(&raw_config).map_err(|e| ConfigError::EncodingError(e))?; 217 | 218 | let mut file = File::create(config_path).map_err(|e| ConfigError::FileWriteError(e))?; 219 | file.write_all(encoded.as_bytes()) 220 | .map_err(|e| ConfigError::FileWriteError(e))?; 221 | 222 | Ok(()) 223 | } 224 | 225 | /// Reads the server config from the config_path given and decrypts it with the provided password. 226 | pub fn load_config( 227 | config_path: &PathBuf, 228 | password: &ZeroingString, 229 | ) -> Result { 230 | let contents = std::fs::read_to_string(config_path).map_err(ConfigError::ReadConfigError)?; 231 | let raw_config: RawConfig = toml::from_str(&contents).map_err(ConfigError::DecodingError)?; 232 | 233 | let encrypted_key = EncryptedServerKey { 234 | encrypted_key: raw_config.encrypted_key, 235 | salt: raw_config.salt, 236 | nonce: raw_config.nonce, 237 | }; 238 | let secret_key = encrypted_key.decrypt(&password)?; 239 | 240 | Ok(ServerConfig { 241 | key: secret_key, 242 | interval_s: raw_config.interval_s, 243 | addr: raw_config.addr, 244 | grin_node_url: raw_config.grin_node_url, 245 | grin_node_secret_path: raw_config.grin_node_secret_path, 246 | wallet_owner_url: raw_config.wallet_owner_url, 247 | wallet_owner_secret_path: raw_config.wallet_owner_secret_path, 248 | prev_server: raw_config.prev_server, 249 | next_server: raw_config.next_server, 250 | }) 251 | } 252 | 253 | pub fn get_grin_path(chain_type: &ChainTypes) -> PathBuf { 254 | let mut grin_path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); 255 | grin_path.push(GRIN_HOME); 256 | grin_path.push(chain_type.shortname()); 257 | grin_path 258 | } 259 | 260 | pub fn node_secret_path(chain_type: &ChainTypes) -> PathBuf { 261 | let mut path = get_grin_path(chain_type); 262 | path.push(NODE_API_SECRET_FILE_NAME); 263 | path 264 | } 265 | 266 | pub fn wallet_owner_secret_path(chain_type: &ChainTypes) -> PathBuf { 267 | let mut path = get_grin_path(chain_type); 268 | path.push(WALLET_OWNER_API_SECRET_FILE_NAME); 269 | path 270 | } 271 | 272 | pub fn grin_node_url(chain_type: &ChainTypes) -> String { 273 | if *chain_type == ChainTypes::Testnet { 274 | "127.0.0.1:13413".into() 275 | } else { 276 | "127.0.0.1:3413".into() 277 | } 278 | } 279 | 280 | pub fn wallet_owner_url(_chain_type: &ChainTypes) -> String { 281 | "127.0.0.1:3420".into() 282 | } 283 | 284 | #[cfg(test)] 285 | pub mod test_util { 286 | use std::net::TcpListener; 287 | 288 | use grin_onion::crypto::dalek::DalekPublicKey; 289 | use secp256k1zkp::SecretKey; 290 | 291 | use crate::config::ServerConfig; 292 | 293 | pub fn local_config( 294 | server_key: &SecretKey, 295 | prev_server: &Option, 296 | next_server: &Option, 297 | ) -> Result> { 298 | let config = ServerConfig { 299 | key: server_key.clone(), 300 | interval_s: 1, 301 | addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, 302 | grin_node_url: "127.0.0.1:3413".parse()?, 303 | grin_node_secret_path: None, 304 | wallet_owner_url: "127.0.0.1:3420".parse()?, 305 | wallet_owner_secret_path: None, 306 | prev_server: prev_server.clone(), 307 | next_server: next_server.clone(), 308 | }; 309 | Ok(config) 310 | } 311 | } 312 | 313 | #[cfg(test)] 314 | mod tests { 315 | use grin_onion::crypto::secp; 316 | 317 | use super::*; 318 | 319 | #[test] 320 | fn server_key_encrypt() { 321 | let password = ZeroingString::from("password"); 322 | let server_key = secp::random_secret(); 323 | let mut enc_key = EncryptedServerKey::from_secret_key(&server_key, &password); 324 | let decrypted_key = enc_key.decrypt(&password).unwrap(); 325 | assert_eq!(server_key, decrypted_key); 326 | 327 | // Wrong password 328 | let decrypted_key = enc_key.decrypt("wrongpass"); 329 | assert!(decrypted_key.is_err()); 330 | 331 | // Wrong nonce 332 | enc_key.nonce = "wrongnonce".to_owned(); 333 | let decrypted_key = enc_key.decrypt(&password); 334 | assert!(decrypted_key.is_err()); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use grin_core::core::{Input, Transaction}; 2 | use grin_core::core::hash::Hash; 3 | use grin_core::ser::{ 4 | self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer, 5 | }; 6 | use grin_store::{self as store, Store}; 7 | use grin_util::ToHex; 8 | use thiserror::Error; 9 | 10 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 11 | use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey}; 12 | use grin_onion::onion::Onion; 13 | use grin_onion::util::{read_optional, write_optional}; 14 | 15 | const DB_NAME: &str = "swap"; 16 | const STORE_SUBPATH: &str = "swaps"; 17 | 18 | const CURRENT_SWAP_VERSION: u8 = 0; 19 | const SWAP_PREFIX: u8 = b'S'; 20 | 21 | const CURRENT_TX_VERSION: u8 = 0; 22 | const TX_PREFIX: u8 = b'T'; 23 | 24 | /// Swap statuses 25 | #[derive(Clone, Debug, PartialEq)] 26 | pub enum SwapStatus { 27 | Unprocessed, 28 | InProcess { 29 | kernel_commit: Commitment, 30 | }, 31 | Completed { 32 | kernel_commit: Commitment, 33 | block_hash: Hash, 34 | }, 35 | Failed, 36 | } 37 | 38 | impl Writeable for SwapStatus { 39 | fn write(&self, writer: &mut W) -> Result<(), ser::Error> { 40 | match self { 41 | SwapStatus::Unprocessed => { 42 | writer.write_u8(0)?; 43 | } 44 | SwapStatus::InProcess { kernel_commit } => { 45 | writer.write_u8(1)?; 46 | kernel_commit.write(writer)?; 47 | } 48 | SwapStatus::Completed { 49 | kernel_commit, 50 | block_hash, 51 | } => { 52 | writer.write_u8(2)?; 53 | kernel_commit.write(writer)?; 54 | block_hash.write(writer)?; 55 | } 56 | SwapStatus::Failed => { 57 | writer.write_u8(3)?; 58 | } 59 | }; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | impl Readable for SwapStatus { 66 | fn read(reader: &mut R) -> Result { 67 | let status = match reader.read_u8()? { 68 | 0 => SwapStatus::Unprocessed, 69 | 1 => { 70 | let kernel_commit = Commitment::read(reader)?; 71 | SwapStatus::InProcess { kernel_commit } 72 | } 73 | 2 => { 74 | let kernel_commit = Commitment::read(reader)?; 75 | let block_hash = Hash::read(reader)?; 76 | SwapStatus::Completed { 77 | kernel_commit, 78 | block_hash, 79 | } 80 | } 81 | 3 => SwapStatus::Failed, 82 | _ => { 83 | return Err(ser::Error::CorruptedData); 84 | } 85 | }; 86 | Ok(status) 87 | } 88 | } 89 | 90 | /// Data needed to swap a single output. 91 | #[derive(Clone, Debug, PartialEq)] 92 | pub struct SwapData { 93 | /// The total excess for the output commitment 94 | pub excess: SecretKey, 95 | /// The derived output commitment after applying excess and fee 96 | pub output_commit: Commitment, 97 | /// The rangeproof, included only for the final hop (node N) 98 | pub rangeproof: Option, 99 | /// Transaction input being spent 100 | pub input: Input, 101 | /// Transaction fee 102 | pub fee: u64, 103 | /// The remaining onion after peeling off our layer 104 | pub onion: Onion, 105 | /// The status of the swap 106 | pub status: SwapStatus, 107 | } 108 | 109 | impl Writeable for SwapData { 110 | fn write(&self, writer: &mut W) -> Result<(), ser::Error> { 111 | writer.write_u8(CURRENT_SWAP_VERSION)?; 112 | writer.write_fixed_bytes(&self.excess)?; 113 | writer.write_fixed_bytes(&self.output_commit)?; 114 | write_optional(writer, &self.rangeproof)?; 115 | self.input.write(writer)?; 116 | writer.write_u64(self.fee.into())?; 117 | self.onion.write(writer)?; 118 | self.status.write(writer)?; 119 | 120 | Ok(()) 121 | } 122 | } 123 | 124 | impl Readable for SwapData { 125 | fn read(reader: &mut R) -> Result { 126 | let version = reader.read_u8()?; 127 | if version != CURRENT_SWAP_VERSION { 128 | return Err(ser::Error::UnsupportedProtocolVersion); 129 | } 130 | 131 | let excess = secp::read_secret_key(reader)?; 132 | let output_commit = Commitment::read(reader)?; 133 | let rangeproof = read_optional(reader)?; 134 | let input = Input::read(reader)?; 135 | let fee = reader.read_u64()?; 136 | let onion = Onion::read(reader)?; 137 | let status = SwapStatus::read(reader)?; 138 | Ok(SwapData { 139 | excess, 140 | output_commit, 141 | rangeproof, 142 | input, 143 | fee, 144 | onion, 145 | status, 146 | }) 147 | } 148 | } 149 | 150 | /// A transaction created as part of a swap round. 151 | #[derive(Clone, Debug, PartialEq)] 152 | pub struct SwapTx { 153 | pub tx: Transaction, 154 | pub chain_tip: (u64, Hash), 155 | // TODO: Include status 156 | } 157 | 158 | impl Writeable for SwapTx { 159 | fn write(&self, writer: &mut W) -> Result<(), ser::Error> { 160 | writer.write_u8(CURRENT_TX_VERSION)?; 161 | self.tx.write(writer)?; 162 | writer.write_u64(self.chain_tip.0)?; 163 | self.chain_tip.1.write(writer)?; 164 | Ok(()) 165 | } 166 | } 167 | 168 | impl Readable for SwapTx { 169 | fn read(reader: &mut R) -> Result { 170 | let version = reader.read_u8()?; 171 | if version != CURRENT_TX_VERSION { 172 | return Err(ser::Error::UnsupportedProtocolVersion); 173 | } 174 | 175 | let tx = Transaction::read(reader)?; 176 | let height = reader.read_u64()?; 177 | let block_hash = Hash::read(reader)?; 178 | Ok(SwapTx { 179 | tx, 180 | chain_tip: (height, block_hash), 181 | }) 182 | } 183 | } 184 | 185 | /// Storage facility for swap data. 186 | pub struct SwapStore { 187 | db: Store, 188 | } 189 | 190 | /// Store error types 191 | #[derive(Clone, Error, Debug, PartialEq)] 192 | pub enum StoreError { 193 | #[error("Swap entry already exists for '{0:?}'")] 194 | AlreadyExists(Commitment), 195 | #[error("Entry does not exist for '{0:?}'")] 196 | NotFound(Commitment), 197 | #[error("Error occurred while attempting to open db: {0}")] 198 | OpenError(store::lmdb::Error), 199 | #[error("Serialization error occurred: {0}")] 200 | SerializationError(ser::Error), 201 | #[error("Error occurred while attempting to read from db: {0}")] 202 | ReadError(store::lmdb::Error), 203 | #[error("Error occurred while attempting to write to db: {0}")] 204 | WriteError(store::lmdb::Error), 205 | } 206 | 207 | impl From for StoreError { 208 | fn from(e: ser::Error) -> StoreError { 209 | StoreError::SerializationError(e) 210 | } 211 | } 212 | 213 | impl SwapStore { 214 | /// Create new chain store 215 | pub fn new(db_root: &str) -> Result { 216 | let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) 217 | .map_err(StoreError::OpenError)?; 218 | Ok(SwapStore { db }) 219 | } 220 | 221 | /// Writes a single key-value pair to the database 222 | fn write>( 223 | &self, 224 | prefix: u8, 225 | k: K, 226 | value: &Vec, 227 | overwrite: bool, 228 | ) -> Result { 229 | let batch = self.db.batch()?; 230 | let key = store::to_key(prefix, k); 231 | if !overwrite && batch.exists(&key[..])? { 232 | Ok(false) 233 | } else { 234 | batch.put(&key[..], &value[..])?; 235 | batch.commit()?; 236 | Ok(true) 237 | } 238 | } 239 | 240 | /// Reads a single value by key 241 | fn read + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result { 242 | store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k), None), || { 243 | format!("{}:{}", prefix, k.to_hex()) 244 | }) 245 | .map_err(StoreError::ReadError) 246 | } 247 | 248 | /// Saves a swap to the database 249 | pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> { 250 | let data = ser::ser_vec(&s, ProtocolVersion::local())?; 251 | let saved = self 252 | .write(SWAP_PREFIX, &s.input.commit, &data, overwrite) 253 | .map_err(StoreError::WriteError)?; 254 | if !saved { 255 | Err(StoreError::AlreadyExists(s.input.commit.clone())) 256 | } else { 257 | Ok(()) 258 | } 259 | } 260 | 261 | /// Iterator over all swaps. 262 | pub fn swaps_iter(&self) -> Result, StoreError> { 263 | let key = store::to_key(SWAP_PREFIX, ""); 264 | let protocol_version = self.db.protocol_version(); 265 | self.db 266 | .iter(&key[..], move |_, mut v| { 267 | ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) 268 | .map_err(From::from) 269 | }) 270 | .map_err(|e| StoreError::ReadError(e)) 271 | } 272 | 273 | /// Checks if a matching swap exists in the database 274 | #[allow(dead_code)] 275 | pub fn swap_exists(&self, input_commit: &Commitment) -> Result { 276 | let key = store::to_key(SWAP_PREFIX, input_commit); 277 | self.db 278 | .batch() 279 | .map_err(StoreError::ReadError)? 280 | .exists(&key[..]) 281 | .map_err(StoreError::ReadError) 282 | } 283 | 284 | /// Reads a swap from the database 285 | pub fn get_swap(&self, input_commit: &Commitment) -> Result { 286 | self.read(SWAP_PREFIX, input_commit) 287 | } 288 | 289 | /// Saves a swap transaction to the database 290 | pub fn save_swap_tx(&self, s: &SwapTx) -> Result<(), StoreError> { 291 | let data = ser::ser_vec(&s, ProtocolVersion::local())?; 292 | self.write( 293 | TX_PREFIX, 294 | &s.tx.kernels().first().unwrap().excess, 295 | &data, 296 | true, 297 | ) 298 | .map_err(StoreError::WriteError)?; 299 | 300 | Ok(()) 301 | } 302 | 303 | /// Reads a swap tx from the database 304 | pub fn get_swap_tx(&self, kernel_excess: &Commitment) -> Result { 305 | self.read(TX_PREFIX, kernel_excess) 306 | } 307 | } 308 | 309 | #[cfg(test)] 310 | mod tests { 311 | use std::cmp::Ordering; 312 | 313 | use grin_core::core::{Input, OutputFeatures}; 314 | use grin_core::global::{self, ChainTypes}; 315 | use rand::RngCore; 316 | 317 | use grin_onion::crypto::secp; 318 | use grin_onion::test_util as onion_test_util; 319 | 320 | use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; 321 | 322 | fn new_store(test_name: &str) -> SwapStore { 323 | global::set_local_chain_type(ChainTypes::AutomatedTesting); 324 | let db_root = format!("./target/tmp/.{}", test_name); 325 | let _ = std::fs::remove_dir_all(db_root.as_str()); 326 | SwapStore::new(db_root.as_str()).unwrap() 327 | } 328 | 329 | fn rand_swap_with_status(status: SwapStatus) -> SwapData { 330 | SwapData { 331 | excess: secp::random_secret(), 332 | output_commit: onion_test_util::rand_commit(), 333 | rangeproof: Some(onion_test_util::rand_proof()), 334 | input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()), 335 | fee: rand::thread_rng().next_u64(), 336 | onion: onion_test_util::rand_onion(), 337 | status, 338 | } 339 | } 340 | 341 | fn rand_swap() -> SwapData { 342 | let s = rand::thread_rng().next_u64() % 3; 343 | let status = if s == 0 { 344 | SwapStatus::Unprocessed 345 | } else if s == 1 { 346 | SwapStatus::InProcess { 347 | kernel_commit: onion_test_util::rand_commit(), 348 | } 349 | } else { 350 | SwapStatus::Completed { 351 | kernel_commit: onion_test_util::rand_commit(), 352 | block_hash: onion_test_util::rand_hash(), 353 | } 354 | }; 355 | rand_swap_with_status(status) 356 | } 357 | 358 | #[test] 359 | fn swap_iter() -> Result<(), Box> { 360 | let store = new_store("swap_iter"); 361 | let mut swaps: Vec = Vec::new(); 362 | for _ in 0..5 { 363 | let swap = rand_swap(); 364 | store.save_swap(&swap, false)?; 365 | swaps.push(swap); 366 | } 367 | 368 | swaps.sort_by(|a, b| { 369 | if a.input.commit < b.input.commit { 370 | Ordering::Less 371 | } else if a.input.commit == b.input.commit { 372 | Ordering::Equal 373 | } else { 374 | Ordering::Greater 375 | } 376 | }); 377 | 378 | let mut i: usize = 0; 379 | for swap in store.swaps_iter()? { 380 | assert_eq!(swap, *swaps.get(i).unwrap()); 381 | i += 1; 382 | } 383 | 384 | Ok(()) 385 | } 386 | 387 | #[test] 388 | fn save_swap() -> Result<(), Box> { 389 | let store = new_store("save_swap"); 390 | 391 | let mut swap = rand_swap_with_status(SwapStatus::Unprocessed); 392 | assert!(!store.swap_exists(&swap.input.commit)?); 393 | 394 | store.save_swap(&swap, false)?; 395 | assert_eq!(swap, store.get_swap(&swap.input.commit)?); 396 | assert!(store.swap_exists(&swap.input.commit)?); 397 | 398 | swap.status = SwapStatus::InProcess { 399 | kernel_commit: onion_test_util::rand_commit(), 400 | }; 401 | let result = store.save_swap(&swap, false); 402 | assert_eq!( 403 | Err(StoreError::AlreadyExists(swap.input.commit.clone())), 404 | result 405 | ); 406 | 407 | store.save_swap(&swap, true)?; 408 | assert_eq!(swap, store.get_swap(&swap.input.commit)?); 409 | 410 | Ok(()) 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /tests/common/wallet.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 2 | use std::sync::Arc; 3 | use std::thread; 4 | 5 | use grin_core::core::{FeeFields, Output, OutputFeatures, Transaction, TxKernel}; 6 | use grin_core::global::ChainTypes; 7 | use grin_core::libtx::tx_fee; 8 | use grin_keychain::{BlindingFactor, ExtKeychain, Identifier, Keychain, SwitchCommitmentType}; 9 | use grin_util::{Mutex, ZeroingString}; 10 | use grin_wallet_api::Owner; 11 | use grin_wallet_config::WalletConfig; 12 | use grin_wallet_controller::controller; 13 | use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; 14 | use grin_wallet_libwallet::{InitTxArgs, Slate, VersionedSlate, WalletInfo, WalletInst}; 15 | use log::error; 16 | use serde_derive::{Deserialize, Serialize}; 17 | use serde_json::json; 18 | use x25519_dalek::PublicKey as xPublicKey; 19 | 20 | use grin_onion::crypto::comsig::ComSignature; 21 | use grin_onion::onion::Onion; 22 | use grin_onion::Hop; 23 | use mwixnet::http; 24 | use mwixnet::wallet::HttpWallet; 25 | use secp256k1zkp::pedersen::Commitment; 26 | use secp256k1zkp::{Secp256k1, SecretKey}; 27 | 28 | use crate::common::types::BlockFees; 29 | 30 | /// Response to build a coinbase output. 31 | #[derive(Serialize, Deserialize, Debug, Clone)] 32 | pub struct CbData { 33 | /// Output 34 | pub output: Output, 35 | /// Kernel 36 | pub kernel: TxKernel, 37 | /// Key Id 38 | pub key_id: Option, 39 | } 40 | 41 | pub struct IntegrationGrinWallet { 42 | wallet: Arc< 43 | Mutex< 44 | Box< 45 | dyn WalletInst< 46 | 'static, 47 | DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, 48 | HTTPNodeClient, 49 | ExtKeychain, 50 | >, 51 | >, 52 | >, 53 | >, 54 | api_listen_port: u16, 55 | owner_api: Arc< 56 | Owner, HTTPNodeClient, ExtKeychain>, 57 | >, 58 | http_client: Arc, 59 | } 60 | 61 | impl IntegrationGrinWallet { 62 | pub async fn async_new_wallet( 63 | wallet_dir: String, 64 | api_listen_port: u16, 65 | node_api: String, 66 | ) -> IntegrationGrinWallet { 67 | let node_client = HTTPNodeClient::new(&node_api, None).unwrap(); 68 | let mut wallet = Box::new( 69 | DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(), 70 | ) 71 | as Box< 72 | dyn WalletInst< 73 | 'static, 74 | DefaultLCProvider, 75 | HTTPNodeClient, 76 | ExtKeychain, 77 | >, 78 | >; 79 | 80 | // Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc... 81 | let lc = wallet.lc_provider().unwrap(); 82 | 83 | let mut wallet_config = WalletConfig::default(); 84 | wallet_config.check_node_api_http_addr = node_api.clone(); 85 | wallet_config.owner_api_listen_port = Some(api_listen_port); 86 | wallet_config.api_secret_path = None; 87 | wallet_config.data_file_dir = wallet_dir.clone(); 88 | 89 | // The top level wallet directory should be set manually (in the reference implementation, 90 | // this is provided in the WalletConfig) 91 | let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); 92 | 93 | lc.create_config( 94 | &ChainTypes::AutomatedTesting, 95 | "grin-wallet.toml", 96 | Some(wallet_config.clone()), 97 | None, 98 | None, 99 | ) 100 | .unwrap(); 101 | 102 | lc.create_wallet(None, None, 12, ZeroingString::from("pass"), false) 103 | .unwrap(); 104 | 105 | // Start owner API 106 | let km = Arc::new(Mutex::new(None)); 107 | let wallet = Arc::new(Mutex::new(wallet)); 108 | let owner_api = Arc::new(Owner::new(wallet.clone(), None)); 109 | 110 | let address_str = format!("127.0.0.1:{}", api_listen_port); 111 | let address_str_2 = format!("127.0.0.1:{}", api_listen_port); 112 | let thr_wallet = wallet.clone(); 113 | let _thread_handle = thread::spawn(move || { 114 | controller::owner_listener( 115 | thr_wallet, 116 | km, 117 | address_str.as_str(), 118 | None, 119 | None, 120 | Some(true), 121 | None, 122 | false, 123 | ) 124 | .unwrap() 125 | }); 126 | 127 | let http_client = Arc::new( 128 | HttpWallet::async_open_wallet(&address_str_2, &None, &ZeroingString::from("pass")) 129 | .await 130 | .unwrap(), 131 | ); 132 | 133 | IntegrationGrinWallet { 134 | wallet, 135 | api_listen_port, 136 | owner_api, 137 | http_client, 138 | } 139 | } 140 | 141 | pub async fn async_retrieve_summary_info(&self) -> Result { 142 | let params = json!({ 143 | "token": self.http_client.clone().get_token(), 144 | "refresh_from_node": true, 145 | "minimum_confirmations": 1 146 | }); 147 | let (_, wallet_info): (bool, WalletInfo) = self 148 | .http_client 149 | .clone() 150 | .async_perform_request("retrieve_summary_info", ¶ms) 151 | .await?; 152 | Ok(wallet_info) 153 | } 154 | 155 | pub async fn async_send( 156 | &self, 157 | receiving_wallet: &IntegrationGrinWallet, 158 | amount: u64, 159 | ) -> Result { 160 | let slate = self.async_init_send_tx(amount).await.unwrap(); 161 | let slate = receiving_wallet.async_receive_tx(&slate).await.unwrap(); 162 | let slate = self.async_finalize_tx(&slate).await.unwrap(); 163 | let tx = Slate::from(slate).tx_or_err().unwrap().clone(); 164 | Ok(tx) 165 | } 166 | 167 | async fn async_init_send_tx( 168 | &self, 169 | amount: u64, 170 | ) -> Result { 171 | let args = InitTxArgs { 172 | src_acct_name: None, 173 | amount, 174 | minimum_confirmations: 0, 175 | max_outputs: 10, 176 | num_change_outputs: 1, 177 | selection_strategy_is_use_all: false, 178 | ..Default::default() 179 | }; 180 | let params = json!({ 181 | "token": self.http_client.clone().get_token(), 182 | "args": args 183 | }); 184 | 185 | let slate: VersionedSlate = self 186 | .http_client 187 | .clone() 188 | .async_perform_request("init_send_tx", ¶ms) 189 | .await?; 190 | 191 | let params = json!({ 192 | "token": self.http_client.clone().get_token(), 193 | "slate": &slate 194 | }); 195 | self.http_client 196 | .clone() 197 | .async_perform_request("tx_lock_outputs", ¶ms) 198 | .await?; 199 | 200 | Ok(slate) 201 | } 202 | 203 | pub async fn async_receive_tx( 204 | &self, 205 | slate: &VersionedSlate, 206 | ) -> Result { 207 | let params = json!([slate, null, null]); 208 | let response = 209 | http::async_send_json_request(&self.foreign_api(), &None, "receive_tx", ¶ms) 210 | .await 211 | .map_err(|e| { 212 | let report = format!("Failed to receive tx. Is the wallet listening? {}", e); 213 | error!("{}", report); 214 | grin_servers::common::types::Error::WalletComm(report) 215 | })?; 216 | 217 | Ok(response) 218 | } 219 | 220 | async fn async_finalize_tx( 221 | &self, 222 | slate: &VersionedSlate, 223 | ) -> Result { 224 | let params = json!({ 225 | "token": self.http_client.clone().get_token(), 226 | "slate": slate 227 | }); 228 | 229 | self.http_client 230 | .clone() 231 | .async_perform_request("finalize_tx", ¶ms) 232 | .await 233 | } 234 | 235 | #[allow(dead_code)] 236 | async fn async_post_tx( 237 | &self, 238 | finalized_slate: &VersionedSlate, 239 | fluff: bool, 240 | ) -> Result { 241 | let params = json!({ 242 | "token": self.http_client.clone().get_token(), 243 | "slate": finalized_slate, 244 | "fluff": fluff 245 | }); 246 | 247 | self.http_client 248 | .clone() 249 | .async_perform_request("post_tx", ¶ms) 250 | .await 251 | } 252 | 253 | /// Call the wallet API to create a coinbase output for the given block_fees. 254 | /// Will retry based on default "retry forever with backoff" behavior. 255 | pub async fn async_create_coinbase( 256 | &self, 257 | block_fees: &BlockFees, 258 | ) -> Result { 259 | let params = json!({ 260 | "block_fees": block_fees 261 | }); 262 | let response = 263 | http::async_send_json_request(&self.foreign_api(), &None, "build_coinbase", ¶ms) 264 | .await 265 | .map_err(|e| { 266 | let report = format!("Failed to get coinbase. Is the wallet listening? {}", e); 267 | error!("{}", report); 268 | grin_servers::common::types::Error::WalletComm(report) 269 | })?; 270 | 271 | Ok(response) 272 | } 273 | 274 | pub fn build_onion( 275 | &self, 276 | commitment: &Commitment, 277 | server_pubkeys: &Vec, 278 | ) -> Result<(Onion, ComSignature), grin_wallet_libwallet::Error> { 279 | let keychain = self 280 | .wallet 281 | .lock() 282 | .lc_provider()? 283 | .wallet_inst()? 284 | .keychain(self.keychain_mask().as_ref())?; 285 | let (_, outputs) = 286 | self.owner_api 287 | .retrieve_outputs(self.keychain_mask().as_ref(), false, false, None)?; 288 | 289 | let mut output = None; 290 | for o in &outputs { 291 | if o.commit == *commitment { 292 | output = Some(o.output.clone()); 293 | break; 294 | } 295 | } 296 | 297 | if output.is_none() { 298 | return Err(grin_wallet_libwallet::Error::GenericError(String::from( 299 | "output not found", 300 | ))); 301 | } 302 | 303 | let amount = output.clone().unwrap().value; 304 | let input_blind = keychain.derive_key( 305 | amount, 306 | &output.clone().unwrap().key_id, 307 | SwitchCommitmentType::Regular, 308 | )?; 309 | 310 | let fee = tx_fee(1, 1, 1); 311 | let new_amount = amount - (fee * server_pubkeys.len() as u64); 312 | let new_output = self.owner_api.build_output( 313 | self.keychain_mask().as_ref(), 314 | OutputFeatures::Plain, 315 | new_amount, 316 | )?; 317 | 318 | let secp = Secp256k1::new(); 319 | let mut blind_sum = new_output 320 | .blind 321 | .split(&BlindingFactor::from_secret_key(input_blind.clone()), &secp)?; 322 | 323 | let hops = server_pubkeys 324 | .iter() 325 | .enumerate() 326 | .map(|(i, &p)| { 327 | if (i + 1) == server_pubkeys.len() { 328 | Hop { 329 | server_pubkey: p.clone(), 330 | excess: blind_sum.secret_key(&secp).unwrap(), 331 | fee: FeeFields::from(fee as u32), 332 | rangeproof: Some(new_output.output.proof.clone()), 333 | } 334 | } else { 335 | let hop_excess = BlindingFactor::rand(&secp); 336 | blind_sum = blind_sum.split(&hop_excess, &secp).unwrap(); 337 | Hop { 338 | server_pubkey: p.clone(), 339 | excess: hop_excess.secret_key(&secp).unwrap(), 340 | fee: FeeFields::from(fee as u32), 341 | rangeproof: None, 342 | } 343 | } 344 | }) 345 | .collect(); 346 | 347 | let onion = grin_onion::create_onion(&commitment, &hops).unwrap(); 348 | let comsig = ComSignature::sign(amount, &input_blind, &onion.serialize().unwrap()).unwrap(); 349 | 350 | Ok((onion, comsig)) 351 | } 352 | 353 | pub fn owner_api( 354 | &self, 355 | ) -> Arc< 356 | Owner, HTTPNodeClient, ExtKeychain>, 357 | > { 358 | self.owner_api.clone() 359 | } 360 | 361 | pub fn foreign_api(&self) -> String { 362 | format!("http://127.0.0.1:{}/v2/foreign", self.api_listen_port) 363 | } 364 | 365 | pub fn owner_address(&self) -> SocketAddr { 366 | SocketAddr::new( 367 | IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 368 | self.api_listen_port, 369 | ) 370 | } 371 | 372 | pub fn keychain_mask(&self) -> Option { 373 | self.http_client.as_ref().get_token().keychain_mask.clone() 374 | } 375 | 376 | pub fn get_client(&self) -> Arc { 377 | self.http_client.clone() 378 | } 379 | } 380 | 381 | #[allow(dead_code)] 382 | pub struct GrinWalletManager { 383 | // base directory for the server instance 384 | working_dir: String, 385 | 386 | wallets: Vec>>, 387 | } 388 | 389 | impl GrinWalletManager { 390 | pub fn new(test_dir: &str) -> GrinWalletManager { 391 | GrinWalletManager { 392 | working_dir: String::from(test_dir), 393 | wallets: vec![], 394 | } 395 | } 396 | 397 | pub async fn async_new_wallet( 398 | &mut self, 399 | node_api_addr: &SocketAddr, 400 | ) -> Arc> { 401 | let wallet_dir = format!("{}/wallets/{}", self.working_dir, self.wallets.len()); 402 | let wallet = Arc::new(Mutex::new( 403 | IntegrationGrinWallet::async_new_wallet( 404 | wallet_dir, 405 | 21000 + self.wallets.len() as u16, 406 | format!("http://{}", node_api_addr), 407 | ) 408 | .await, 409 | )); 410 | self.wallets.push(wallet.clone()); 411 | wallet 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/bin/mwixnet.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | 4 | use std::path::PathBuf; 5 | use std::sync::{Arc, Mutex}; 6 | use std::thread::{sleep, spawn}; 7 | use std::time::Duration; 8 | 9 | use clap::App; 10 | use grin_core::global; 11 | use grin_core::global::ChainTypes; 12 | use grin_util::{StopState, ZeroingString}; 13 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 14 | use rand::{Rng, thread_rng}; 15 | use rpassword; 16 | use tor_rtcompat::PreferredRuntime; 17 | 18 | use grin_onion::crypto; 19 | use grin_onion::crypto::dalek::DalekPublicKey; 20 | use mwixnet::config::{self, ServerConfig}; 21 | use mwixnet::mix_client::{MixClient, MixClientImpl}; 22 | use mwixnet::node::GrinNode; 23 | use mwixnet::node::HttpGrinNode; 24 | use mwixnet::servers; 25 | use mwixnet::store::StoreError; 26 | use mwixnet::store::SwapStore; 27 | use mwixnet::tor; 28 | use mwixnet::wallet::HttpWallet; 29 | 30 | const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; 31 | 32 | fn main() -> Result<(), Box> { 33 | real_main()?; 34 | std::process::exit(0); 35 | } 36 | 37 | fn real_main() -> Result<(), Box> { 38 | let yml = load_yaml!("mwixnet.yml"); 39 | let args = App::from_yaml(yml).get_matches(); 40 | let chain_type = if args.is_present("testnet") { 41 | ChainTypes::Testnet 42 | } else { 43 | ChainTypes::Mainnet 44 | }; 45 | global::set_local_chain_type(chain_type); 46 | 47 | let config_path = match args.value_of("config_file") { 48 | Some(path) => PathBuf::from(path), 49 | None => { 50 | let mut grin_path = config::get_grin_path(&chain_type); 51 | grin_path.push("mwixnet-config.toml"); 52 | grin_path 53 | } 54 | }; 55 | 56 | let round_time = args 57 | .value_of("round_time") 58 | .map(|t| t.parse::().unwrap()); 59 | let bind_addr = args.value_of("bind_addr"); 60 | let grin_node_url = args.value_of("grin_node_url"); 61 | let grin_node_secret_path = args.value_of("grin_node_secret_path"); 62 | let wallet_owner_url = args.value_of("wallet_owner_url"); 63 | let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path"); 64 | let prev_server = args 65 | .value_of("prev_server") 66 | .map(|p| DalekPublicKey::from_hex(&p).unwrap()); 67 | let next_server = args 68 | .value_of("next_server") 69 | .map(|p| DalekPublicKey::from_hex(&p).unwrap()); 70 | 71 | // Write a new config file if init-config command is supplied 72 | if let ("init-config", Some(_)) = args.subcommand() { 73 | if config_path.exists() { 74 | panic!( 75 | "Config file already exists at {}", 76 | config_path.to_string_lossy() 77 | ); 78 | } 79 | 80 | let server_config = ServerConfig { 81 | key: crypto::secp::random_secret(), 82 | interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), 83 | addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?, 84 | grin_node_url: match grin_node_url { 85 | Some(u) => u.parse()?, 86 | None => config::grin_node_url(&chain_type), 87 | }, 88 | grin_node_secret_path: match grin_node_secret_path { 89 | Some(p) => Some(p.to_owned()), 90 | None => config::node_secret_path(&chain_type) 91 | .to_str() 92 | .map(|p| p.to_owned()), 93 | }, 94 | wallet_owner_url: match wallet_owner_url { 95 | Some(u) => u.parse()?, 96 | None => config::wallet_owner_url(&chain_type), 97 | }, 98 | wallet_owner_secret_path: match wallet_owner_secret_path { 99 | Some(p) => Some(p.to_owned()), 100 | None => config::wallet_owner_secret_path(&chain_type) 101 | .to_str() 102 | .map(|p| p.to_owned()), 103 | }, 104 | prev_server, 105 | next_server, 106 | }; 107 | 108 | let password = prompt_password_confirm(); 109 | config::write_config(&config_path, &server_config, &password)?; 110 | println!( 111 | "Config file written to {:?}. Please back this file up in a safe place.", 112 | config_path 113 | ); 114 | return Ok(()); 115 | } 116 | 117 | let password = prompt_password(); 118 | let mut server_config = config::load_config(&config_path, &password)?; 119 | 120 | // Write a new config file if init-config command is supplied 121 | if let ("pubkey", Some(_)) = args.subcommand() { 122 | if !config_path.exists() { 123 | panic!( 124 | "No public address configured (Config file not found). {}", 125 | config_path.to_string_lossy() 126 | ); 127 | } 128 | let sub_args = args.subcommand_matches("pubkey").unwrap(); 129 | let server_pubkey = server_config.server_pubkey(); 130 | if sub_args.is_present("output_file") 131 | { 132 | //output server pubkey to file 133 | let output_file = sub_args.value_of("output_file").unwrap(); 134 | std::fs::write(output_file, format!("{}", server_pubkey.to_hex()))?; 135 | println!("Server pubkey written to file: {}", output_file); 136 | } else { 137 | println!("{}", server_pubkey.to_hex()); 138 | } 139 | return Ok(()) 140 | } 141 | 142 | // Override grin_node_url, if supplied 143 | if let Some(grin_node_url) = grin_node_url { 144 | server_config.grin_node_url = grin_node_url.parse()?; 145 | } 146 | 147 | // Override grin_node_secret_path, if supplied 148 | if let Some(grin_node_secret_path) = grin_node_secret_path { 149 | server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned()); 150 | } 151 | 152 | // Override wallet_owner_url, if supplied 153 | if let Some(wallet_owner_url) = wallet_owner_url { 154 | server_config.wallet_owner_url = wallet_owner_url.parse()?; 155 | } 156 | 157 | // Override wallet_owner_secret_path, if supplied 158 | if let Some(wallet_owner_secret_path) = wallet_owner_secret_path { 159 | server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned()); 160 | } 161 | 162 | // Override bind_addr, if supplied 163 | if let Some(bind_addr) = bind_addr { 164 | server_config.addr = bind_addr.parse()?; 165 | } 166 | 167 | // Override prev_server, if supplied 168 | if let Some(prev_server) = prev_server { 169 | server_config.prev_server = Some(prev_server); 170 | } 171 | 172 | // Override next_server, if supplied 173 | if let Some(next_server) = next_server { 174 | server_config.next_server = Some(next_server); 175 | } 176 | 177 | // Create GrinNode 178 | let node = HttpGrinNode::new( 179 | &server_config.grin_node_url, 180 | &server_config.node_api_secret(), 181 | ); 182 | 183 | // Node API health check 184 | let rt = tokio::runtime::Builder::new_multi_thread() 185 | .enable_all() 186 | .build()?; 187 | 188 | let rt_handle = rt.handle().clone(); 189 | 190 | if let Err(e) = rt_handle.block_on(node.async_get_chain_tip()) { 191 | eprintln!("Node communication failure. Is node listening?"); 192 | return Err(e.into()); 193 | }; 194 | 195 | // Open wallet 196 | let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass")); 197 | let wallet = rt_handle.block_on(HttpWallet::async_open_wallet( 198 | &server_config.wallet_owner_url, 199 | &server_config.wallet_owner_api_secret(), 200 | &wallet_pass, 201 | )); 202 | let wallet = match wallet { 203 | Ok(w) => w, 204 | Err(e) => { 205 | eprintln!("Wallet communication failure. Is wallet listening?"); 206 | return Err(e.into()); 207 | } 208 | }; 209 | 210 | let tor_runtime = if let Ok(r) = PreferredRuntime::create() { 211 | r 212 | } else { 213 | return Err("No runtime found".into()); 214 | }; 215 | 216 | let tor_instance = rt_handle.block_on(tor::async_init_tor( 217 | tor_runtime, 218 | &config::get_grin_path(&chain_type).to_str().unwrap(), 219 | &server_config, 220 | ))?; 221 | let tor_instance = Arc::new(grin_util::Mutex::new(tor_instance)); 222 | let tor_clone = tor_instance.clone(); 223 | 224 | let stop_state = Arc::new(StopState::new()); 225 | let stop_state_clone = stop_state.clone(); 226 | 227 | rt_handle.spawn(async move { 228 | futures::executor::block_on(build_signals_fut()); 229 | tor_clone.lock().stop(); 230 | stop_state_clone.stop(); 231 | }); 232 | 233 | let next_mixer: Option> = server_config.next_server.clone().map(|pk| { 234 | let client: Arc = Arc::new(MixClientImpl::new( 235 | server_config.clone(), 236 | tor_instance.clone(), 237 | pk.clone(), 238 | )); 239 | client 240 | }); 241 | 242 | if server_config.prev_server.is_some() { 243 | // Start the JSON-RPC HTTP 'mix' server 244 | println!( 245 | "Starting MIX server with public key {:?}", 246 | server_config.server_pubkey().to_hex() 247 | ); 248 | 249 | let (_, http_server) = servers::mix_rpc::listen( 250 | &rt_handle, 251 | server_config, 252 | next_mixer, 253 | Arc::new(wallet), 254 | Arc::new(node), 255 | )?; 256 | 257 | let close_handle = http_server.close_handle(); 258 | let round_handle = spawn(move || loop { 259 | if stop_state.is_stopped() { 260 | close_handle.close(); 261 | break; 262 | } 263 | 264 | sleep(Duration::from_millis(100)); 265 | }); 266 | 267 | http_server.wait(); 268 | round_handle.join().unwrap(); 269 | } else { 270 | println!( 271 | "Starting SWAP server with public key {:?}", 272 | server_config.server_pubkey().to_hex() 273 | ); 274 | 275 | // Open SwapStore 276 | let store = SwapStore::new( 277 | config::get_grin_path(&chain_type) 278 | .join("db") 279 | .to_str() 280 | .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( 281 | "db_root path error".to_string(), 282 | )))?, 283 | )?; 284 | 285 | // Start the mwixnet JSON-RPC HTTP 'swap' server 286 | let (swap_server, http_server) = servers::swap_rpc::listen( 287 | rt.handle(), 288 | &server_config, 289 | next_mixer, 290 | Arc::new(wallet), 291 | Arc::new(node), 292 | store, 293 | )?; 294 | 295 | let close_handle = http_server.close_handle(); 296 | let round_handle = spawn(move || { 297 | let mut rng = thread_rng(); 298 | let mut secs = 0u32; 299 | let mut reorg_secs = 0u32; 300 | let mut reorg_window = rng.gen_range(900u32, 3600u32); 301 | let prev_tx = Arc::new(Mutex::new(None)); 302 | let server = swap_server.clone(); 303 | 304 | loop { 305 | if stop_state.is_stopped() { 306 | close_handle.close(); 307 | break; 308 | } 309 | 310 | sleep(Duration::from_secs(1)); 311 | secs = (secs + 1) % server_config.interval_s; 312 | reorg_secs = (reorg_secs + 1) % reorg_window; 313 | 314 | if secs == 0 { 315 | let prev_tx_clone = prev_tx.clone(); 316 | let server_clone = server.clone(); 317 | rt.spawn(async move { 318 | let result = server_clone.lock().await.execute_round().await; 319 | let mut prev_tx_lock = prev_tx_clone.lock().unwrap(); 320 | *prev_tx_lock = match result { 321 | Ok(Some(tx)) => Some(tx), 322 | _ => None, 323 | }; 324 | }); 325 | reorg_secs = 0; 326 | reorg_window = rng.gen_range(900u32, 3600u32); 327 | } else if reorg_secs == 0 { 328 | let prev_tx_clone = prev_tx.clone(); 329 | let server_clone = server.clone(); 330 | rt.spawn(async move { 331 | let tx_option = { 332 | let prev_tx_lock = prev_tx_clone.lock().unwrap(); 333 | prev_tx_lock.clone() 334 | }; // Lock is dropped here 335 | 336 | if let Some(tx) = tx_option { 337 | let result = server_clone.lock().await.check_reorg(&tx).await; 338 | let mut prev_tx_lock = prev_tx_clone.lock().unwrap(); 339 | *prev_tx_lock = match result { 340 | Ok(Some(tx)) => Some(tx), 341 | _ => None, 342 | }; 343 | } 344 | }); 345 | reorg_window = rng.gen_range(900u32, 3600u32); 346 | } 347 | } 348 | }); 349 | 350 | http_server.wait(); 351 | round_handle.join().unwrap(); 352 | } 353 | 354 | Ok(()) 355 | } 356 | 357 | #[cfg(unix)] 358 | async fn build_signals_fut() { 359 | use tokio::signal::unix::{signal, SignalKind}; 360 | 361 | // Listen for SIGINT, SIGQUIT, and SIGTERM 362 | let mut terminate_signal = 363 | signal(SignalKind::terminate()).expect("failed to create terminate signal"); 364 | let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal"); 365 | let mut interrupt_signal = 366 | signal(SignalKind::interrupt()).expect("failed to create interrupt signal"); 367 | 368 | futures::future::select_all(vec![ 369 | Box::pin(terminate_signal.recv()), 370 | Box::pin(quit_signal.recv()), 371 | Box::pin(interrupt_signal.recv()), 372 | ]) 373 | .await; 374 | } 375 | 376 | #[cfg(not(unix))] 377 | async fn build_signals_fut() { 378 | tokio::signal::ctrl_c() 379 | .await 380 | .expect("failed to install CTRL+C signal handler"); 381 | } 382 | 383 | fn prompt_password() -> ZeroingString { 384 | ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap()) 385 | } 386 | 387 | fn prompt_password_confirm() -> ZeroingString { 388 | let mut first = "first".to_string(); 389 | let mut second = "second".to_string(); 390 | while first != second { 391 | first = rpassword::prompt_password_stdout("Server password: ").unwrap(); 392 | second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap(); 393 | } 394 | ZeroingString::from(first) 395 | } 396 | 397 | fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString { 398 | match *wallet_pass { 399 | Some(wallet_pass) => ZeroingString::from(wallet_pass), 400 | None => { 401 | ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap()) 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/servers/mix.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::sync::Arc; 3 | 4 | use async_trait::async_trait; 5 | use futures::stream::{self, StreamExt}; 6 | use grin_core::core::{Output, OutputFeatures, TransactionBody}; 7 | use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; 8 | use grin_core::ser; 9 | use grin_core::ser::ProtocolVersion; 10 | use itertools::Itertools; 11 | use thiserror::Error; 12 | 13 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 14 | use grin_onion::crypto::dalek::{self, DalekSignature}; 15 | use grin_onion::onion::{Onion, OnionError, PeeledOnion}; 16 | use secp256k1zkp::key::ZERO_KEY; 17 | use secp256k1zkp::Secp256k1; 18 | 19 | use crate::config::ServerConfig; 20 | use crate::mix_client::MixClient; 21 | use crate::node::{self, GrinNode}; 22 | use crate::servers::mix_rpc::MixResp; 23 | use crate::tx::{self, TxComponents}; 24 | use crate::wallet::Wallet; 25 | 26 | /// Mixer error types 27 | #[derive(Error, Debug)] 28 | pub enum MixError { 29 | #[error("Invalid number of payloads provided")] 30 | InvalidPayloadLength, 31 | #[error("Signature is invalid")] 32 | InvalidSignature, 33 | #[error("Rangeproof is invalid")] 34 | InvalidRangeproof, 35 | #[error("Rangeproof is required but was not supplied")] 36 | MissingRangeproof, 37 | #[error("Failed to peel onion layer: {0:?}")] 38 | PeelOnionFailure(OnionError), 39 | #[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")] 40 | FeeTooLow { minimum_fee: u64, actual_fee: u64 }, 41 | #[error("None of the outputs could be mixed")] 42 | NoValidOutputs, 43 | #[error("Dalek error: {0:?}")] 44 | Dalek(dalek::DalekError), 45 | #[error("Secp error: {0:?}")] 46 | Secp(grin_util::secp::Error), 47 | #[error("Error building transaction: {0:?}")] 48 | TxError(tx::TxError), 49 | #[error("Wallet error: {0:?}")] 50 | WalletError(crate::wallet::WalletError), 51 | #[error("Client comm error: {0:?}")] 52 | Client(crate::mix_client::MixClientError), 53 | } 54 | 55 | /// An internal MWixnet server - a "Mixer" 56 | #[async_trait] 57 | pub trait MixServer: Send + Sync { 58 | /// Swaps the outputs provided and returns the final swapped outputs and kernels. 59 | async fn mix_outputs( 60 | &self, 61 | onions: &Vec, 62 | sig: &DalekSignature, 63 | ) -> Result; 64 | } 65 | 66 | /// The standard MWixnet "Mixer" implementation 67 | #[derive(Clone)] 68 | pub struct MixServerImpl { 69 | secp: Secp256k1, 70 | server_config: ServerConfig, 71 | mix_client: Option>, 72 | wallet: Arc, 73 | node: Arc, 74 | } 75 | 76 | impl MixServerImpl { 77 | /// Create a new 'Mix' server 78 | pub fn new( 79 | server_config: ServerConfig, 80 | mix_client: Option>, 81 | wallet: Arc, 82 | node: Arc, 83 | ) -> Self { 84 | MixServerImpl { 85 | secp: Secp256k1::new(), 86 | server_config, 87 | mix_client, 88 | wallet, 89 | node, 90 | } 91 | } 92 | 93 | /// The fee base to use. For now, just using the default. 94 | fn get_fee_base(&self) -> u64 { 95 | DEFAULT_ACCEPT_FEE_BASE 96 | } 97 | 98 | /// Minimum fee to perform a mix. 99 | /// Requires enough fee for the mixer's kernel. 100 | fn get_minimum_mix_fee(&self) -> u64 { 101 | TransactionBody::weight_by_iok(0, 0, 1) * self.get_fee_base() 102 | } 103 | 104 | fn peel_onion(&self, onion: &Onion) -> Result { 105 | // Verify that more than 1 payload exists when there's a next server, 106 | // or that exactly 1 payload exists when this is the final server 107 | if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1 108 | || self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1 109 | { 110 | return Err(MixError::InvalidPayloadLength); 111 | } 112 | 113 | // Peel the top layer 114 | let peeled = onion 115 | .peel_layer(&self.server_config.key) 116 | .map_err(|e| MixError::PeelOnionFailure(e))?; 117 | 118 | // Verify the fee meets the minimum 119 | let fee: u64 = peeled.payload.fee.into(); 120 | if fee < self.get_minimum_mix_fee() { 121 | return Err(MixError::FeeTooLow { 122 | minimum_fee: self.get_minimum_mix_fee(), 123 | actual_fee: fee, 124 | }); 125 | } 126 | 127 | if let Some(r) = peeled.payload.rangeproof { 128 | // Verify the bullet proof 129 | self.secp 130 | .verify_bullet_proof(peeled.onion.commit, r, None) 131 | .map_err(|_| MixError::InvalidRangeproof)?; 132 | } else if peeled.onion.enc_payloads.is_empty() { 133 | // A rangeproof is required in the last payload 134 | return Err(MixError::MissingRangeproof); 135 | } 136 | 137 | Ok(peeled) 138 | } 139 | 140 | async fn async_build_final_outputs( 141 | &self, 142 | peeled: &Vec<(usize, PeeledOnion)>, 143 | ) -> Result { 144 | // Filter out commitments that already exist in the UTXO set 145 | let filtered: Vec<&(usize, PeeledOnion)> = stream::iter(peeled.iter()) 146 | .filter(|(_, p)| async { 147 | !node::async_is_unspent(&self.node, &p.onion.commit) 148 | .await 149 | .unwrap_or(true) 150 | }) 151 | .collect() 152 | .await; 153 | 154 | // Build plain outputs for each mix entry 155 | let outputs: Vec = filtered 156 | .iter() 157 | .map(|(_, p)| { 158 | Output::new( 159 | OutputFeatures::Plain, 160 | p.onion.commit, 161 | p.payload.rangeproof.unwrap(), 162 | ) 163 | }) 164 | .collect(); 165 | 166 | let fees_paid = filtered.iter().map(|(_, p)| p.payload.fee.fee()).sum(); 167 | let output_excesses = filtered 168 | .iter() 169 | .map(|(_, p)| p.payload.excess.clone()) 170 | .collect(); 171 | 172 | let components = tx::async_assemble_components( 173 | &self.wallet, 174 | &TxComponents { 175 | offset: ZERO_KEY, 176 | kernels: Vec::new(), 177 | outputs, 178 | }, 179 | &output_excesses, 180 | self.get_fee_base(), 181 | fees_paid, 182 | ) 183 | .await 184 | .map_err(MixError::TxError)?; 185 | 186 | let indices = filtered.iter().map(|(i, _)| *i).collect(); 187 | 188 | Ok(MixResp { 189 | indices, 190 | components, 191 | }) 192 | } 193 | 194 | async fn call_next_mixer( 195 | &self, 196 | peeled: &Vec<(usize, PeeledOnion)>, 197 | ) -> Result { 198 | // Sort by commitment 199 | let mut onions_with_index = peeled.clone(); 200 | onions_with_index 201 | .sort_by(|(_, a), (_, b)| a.onion.commit.partial_cmp(&b.onion.commit).unwrap()); 202 | 203 | // Create map of prev indices to next indices 204 | let map_indices: HashMap = 205 | HashMap::from_iter(onions_with_index.iter().enumerate().map(|(i, j)| (j.0, i))); 206 | 207 | // Call next server 208 | let onions = peeled.iter().map(|(_, p)| p.onion.clone()).collect(); 209 | let mixed = self 210 | .mix_client 211 | .as_ref() 212 | .unwrap() 213 | .mix_outputs(&onions) 214 | .await 215 | .map_err(MixError::Client)?; 216 | 217 | // Remove filtered entries 218 | let kept_next_indices = HashSet::<_>::from_iter(mixed.indices.clone()); 219 | let filtered_onions: Vec<&(usize, PeeledOnion)> = onions_with_index 220 | .iter() 221 | .filter(|(i, _)| { 222 | map_indices.contains_key(i) 223 | && kept_next_indices.contains(map_indices.get(i).unwrap()) 224 | }) 225 | .collect(); 226 | 227 | // Calculate excess of entries kept 228 | let excesses = filtered_onions 229 | .iter() 230 | .map(|(_, p)| p.payload.excess.clone()) 231 | .collect(); 232 | 233 | // Calculate total fee of entries kept 234 | let fees_paid = filtered_onions 235 | .iter() 236 | .fold(0, |f, (_, p)| f + p.payload.fee.fee()); 237 | 238 | let indices = kept_next_indices.into_iter().sorted().collect(); 239 | 240 | let components = tx::async_assemble_components( 241 | &self.wallet, 242 | &mixed.components, 243 | &excesses, 244 | self.get_fee_base(), 245 | fees_paid, 246 | ) 247 | .await 248 | .map_err(MixError::TxError)?; 249 | 250 | Ok(MixResp { 251 | indices, 252 | components, 253 | }) 254 | } 255 | } 256 | 257 | #[async_trait] 258 | impl MixServer for MixServerImpl { 259 | async fn mix_outputs( 260 | &self, 261 | onions: &Vec, 262 | sig: &DalekSignature, 263 | ) -> Result { 264 | // Verify Signature 265 | let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); 266 | sig.verify( 267 | self.server_config.prev_server.as_ref().unwrap(), 268 | serialized.as_slice(), 269 | ) 270 | .map_err(|_| MixError::InvalidSignature)?; 271 | 272 | // Peel onions and filter 273 | let mut peeled: Vec<(usize, PeeledOnion)> = onions 274 | .iter() 275 | .enumerate() 276 | .filter_map(|(i, o)| match self.peel_onion(&o) { 277 | Ok(p) => Some((i, p)), 278 | Err(e) => { 279 | println!("Error peeling onion: {:?}", e); 280 | None 281 | } 282 | }) 283 | .collect(); 284 | 285 | // Remove duplicate commitments 286 | peeled.sort_by_key(|(_, o)| o.onion.commit); 287 | peeled.dedup_by_key(|(_, o)| o.onion.commit); 288 | peeled.sort_by_key(|(i, _)| *i); 289 | 290 | if peeled.is_empty() { 291 | return Err(MixError::NoValidOutputs); 292 | } 293 | 294 | if self.server_config.next_server.is_some() { 295 | self.call_next_mixer(&peeled).await 296 | } else { 297 | self.async_build_final_outputs(&peeled).await 298 | } 299 | } 300 | } 301 | 302 | #[cfg(test)] 303 | mod test_util { 304 | use std::sync::Arc; 305 | 306 | use grin_onion::crypto::dalek::DalekPublicKey; 307 | use secp256k1zkp::SecretKey; 308 | 309 | use crate::config; 310 | use crate::mix_client::MixClient; 311 | use crate::mix_client::test_util::DirectMixClient; 312 | use crate::node::mock::MockGrinNode; 313 | use crate::servers::mix::MixServerImpl; 314 | use crate::wallet::mock::MockWallet; 315 | 316 | pub fn new_mixer( 317 | server_key: &SecretKey, 318 | prev_server: (&SecretKey, &DalekPublicKey), 319 | next_server: &Option<(DalekPublicKey, Arc)>, 320 | node: &Arc, 321 | ) -> (Arc, Arc) { 322 | let config = config::test_util::local_config( 323 | &server_key, 324 | &Some(prev_server.1.clone()), 325 | &next_server.as_ref().map(|(k, _)| k.clone()), 326 | ) 327 | .unwrap(); 328 | 329 | let wallet = Arc::new(MockWallet::new()); 330 | let mix_server = Arc::new(MixServerImpl::new( 331 | config, 332 | next_server.as_ref().map(|(_, c)| c.clone()), 333 | wallet.clone(), 334 | node.clone(), 335 | )); 336 | let client = Arc::new(DirectMixClient { 337 | key: prev_server.0.clone(), 338 | mix_server: mix_server.clone(), 339 | }); 340 | 341 | (client, wallet) 342 | } 343 | } 344 | 345 | #[cfg(test)] 346 | mod tests { 347 | use std::collections::HashSet; 348 | use std::sync::Arc; 349 | 350 | use ::function_name::named; 351 | 352 | use grin_onion::{create_onion, Hop, new_hop}; 353 | use grin_onion::crypto::dalek::DalekPublicKey; 354 | use grin_onion::crypto::secp::{self, Commitment}; 355 | use grin_onion::test_util as onion_test_util; 356 | use secp256k1zkp::pedersen::RangeProof; 357 | use secp256k1zkp::SecretKey; 358 | 359 | use crate::mix_client::MixClient; 360 | use crate::node::mock::MockGrinNode; 361 | 362 | macro_rules! init_test { 363 | () => {{ 364 | grin_core::global::set_local_chain_type( 365 | grin_core::global::ChainTypes::AutomatedTesting, 366 | ); 367 | let db_root = concat!("./target/tmp/.", function_name!()); 368 | let _ = std::fs::remove_dir_all(db_root); 369 | () 370 | }}; 371 | } 372 | 373 | struct ServerVars { 374 | fee: u32, 375 | sk: SecretKey, 376 | pk: DalekPublicKey, 377 | excess: SecretKey, 378 | } 379 | 380 | impl ServerVars { 381 | fn new(fee: u32) -> Self { 382 | let (sk, pk) = onion_test_util::rand_keypair(); 383 | let excess = secp::random_secret(); 384 | ServerVars { 385 | fee, 386 | sk, 387 | pk, 388 | excess, 389 | } 390 | } 391 | 392 | fn build_hop(&self, proof: Option) -> Hop { 393 | new_hop(&self.sk, &self.excess, self.fee, proof) 394 | } 395 | } 396 | 397 | /// Tests the happy path for a 3 server setup. 398 | /// 399 | /// Servers: 400 | /// * Swap Server - Simulated by test 401 | /// * Mixer 1 - Internal MixServerImpl directly called by test 402 | /// * Mixer 2 - Final MixServerImpl called by Mixer 1 403 | #[tokio::test] 404 | #[named] 405 | async fn mix_lifecycle() -> Result<(), Box> { 406 | init_test!(); 407 | 408 | // Setup Input(s) 409 | let input1_value: u64 = 200_000_000; 410 | let input1_blind = secp::random_secret(); 411 | let input1_commit = secp::commit(input1_value, &input1_blind)?; 412 | let input_commits = vec![&input1_commit]; 413 | 414 | // Setup Servers 415 | let (swap_vars, mix1_vars, mix2_vars) = ( 416 | ServerVars::new(50_000_000), 417 | ServerVars::new(50_000_000), 418 | ServerVars::new(50_000_000), 419 | ); 420 | 421 | let node = Arc::new(MockGrinNode::new_with_utxos(&input_commits)); 422 | let (mixer2_client, mixer2_wallet) = super::test_util::new_mixer( 423 | &mix2_vars.sk, 424 | (&mix1_vars.sk, &mix1_vars.pk), 425 | &None, 426 | &node, 427 | ); 428 | 429 | let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer( 430 | &mix1_vars.sk, 431 | (&swap_vars.sk, &swap_vars.pk), 432 | &Some((mix2_vars.pk.clone(), mixer2_client.clone())), 433 | &node, 434 | ); 435 | 436 | // Build rangeproof 437 | let (output_commit, proof) = onion_test_util::proof( 438 | input1_value, 439 | swap_vars.fee + mix1_vars.fee + mix2_vars.fee, 440 | &input1_blind, 441 | &vec![&swap_vars.excess, &mix1_vars.excess, &mix2_vars.excess], 442 | ); 443 | 444 | // Create Onion 445 | let onion = create_onion( 446 | &input1_commit, 447 | &vec![ 448 | swap_vars.build_hop(None), 449 | mix1_vars.build_hop(None), 450 | mix2_vars.build_hop(Some(proof)), 451 | ], 452 | )?; 453 | 454 | // Simulate the swap server peeling the onion and then calling mix1 455 | let mix1_onion = onion.peel_layer(&swap_vars.sk)?; 456 | let mixed = mixer1_client 457 | .mix_outputs(&vec![mix1_onion.onion.clone()]) 458 | .await?; 459 | 460 | // Verify 3 outputs are returned: mixed output, mixer1's output, and mixer2's output 461 | assert_eq!(mixed.indices, vec![0 as usize]); 462 | assert_eq!(mixed.components.outputs.len(), 3); 463 | let output_commits: HashSet = mixed 464 | .components 465 | .outputs 466 | .iter() 467 | .map(|o| o.identifier.commit.clone()) 468 | .collect(); 469 | assert!(output_commits.contains(&output_commit)); 470 | 471 | assert_eq!(mixer1_wallet.built_outputs().len(), 1); 472 | assert!(output_commits.contains(mixer1_wallet.built_outputs().get(0).unwrap())); 473 | 474 | assert_eq!(mixer2_wallet.built_outputs().len(), 1); 475 | assert!(output_commits.contains(mixer2_wallet.built_outputs().get(0).unwrap())); 476 | 477 | Ok(()) 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /doc/onion.md: -------------------------------------------------------------------------------- 1 | # Onion Routing 2 | 3 | ## Overview 4 | 5 | The Onion Routing scheme in this context is a method of encrypting and routing transactions in a privacy-preserving manner. At each step, a server peels off a layer of encryption, revealing information intended for it, and passes on the rest of the data to the next server. 6 | 7 | The main component in this encryption scheme is an `Onion`, which contains encrypted payloads, an ephemeral public key and an output commitment. 8 | 9 | ## Data Structures 10 | 11 | ### `Hop` structure 12 | 13 | Each `Hop` represents a step in the routing process, which has its own unique encryption parameters. A `Hop` consists of: 14 | 15 | - `server_pubkey`: The public key of the server for this hop. 16 | - `excess`: An additional blinding factor to add to the commitment. 17 | - `fee`: The transaction fee for this hop. 18 | - `rangeproof`: An optional rangeproof, included only for the final hop. 19 | 20 | ### `Onion` structure 21 | 22 | An `Onion` represents a complete route at a particular stage. It contains: 23 | 24 | - `ephemeral_pubkey`: The ephemeral public key for the server that is next in line to peel off a layer of the onion. 25 | - `commit`: The modified commitment at this stage in the routing process, which will be the original commitment for the first server in the chain, and then will be a recalculated commitment at each following stage. 26 | - `enc_payloads`: The list of encrypted payloads for each remaining hop in the route. 27 | 28 | The `commit` in an `Onion` will be the original unspent output's commitment for the very first `Onion` object sent to the swap server, but then for each peeled layer (i.e., after each hop), a new `Onion` object will be created with a recalculated commitment. This new commitment reflects the additional blinding factor and subtracted fee at each stage. The `Onion` passed from one server to the next then contains this adjusted commitment, not the original one. 29 | 30 | ### `Payload` structure 31 | 32 | Each encrypted payload contains the information needed by a server to process the hop. This includes: 33 | 34 | - `next_ephemeral_pk`: The ephemeral public key for the next hop. 35 | - `excess`: The additional blinding factor for the commitment at this hop. 36 | - `fee`: The transaction fee for this hop. 37 | - `rangeproof`: A rangeproof if the payload is for the final hop. 38 | Absolutely, let's go into more detail on the cryptographic methods utilized during the creation and peeling of the Onion. 39 | 40 | ### Creating an Onion 41 | 42 | The creation of the Onion involves both symmetric and asymmetric encryption techniques: 43 | 44 | 1. **Ephemeral keys:** For each hop (server) in the network, an ephemeral secret key is randomly generated. These ephemeral keys are used to create shared secrets with the server's public key through the Diffie-Hellman key exchange. The first ephemeral public key is included in the Onion, and each subsequent ephemeral public key is encrypted and included in the payload for the previous server. 45 | 46 | 2. **Shared secrets:** A shared secret is generated between the sender (the client) and each server (hop) in the path. This is done using the Elliptic Curve Diffie-Hellman (ECDH) method. The shared secret for each server is calculated from the server's public key and the client's ephemeral secret key. 47 | 48 | 3. **Payload encryption:** Detailed in the next section. 49 | 50 | ### Payload Encryption with ChaCha20 Cipher 51 | 52 | After the shared secrets are created, they are used to derive keys for symmetric encryption with the ChaCha20 cipher. Here is the process: 53 | 54 | 1. **Key derivation:** An HMAC-SHA-256 is used as a key derivation function (KDF). The shared secret is fed into this HMAC function with a constant key of "MWIXNET". The HMAC is used here as a pseudo-random function (PRF) to derive a 256-bit key from the shared secret. The purpose of using HMAC in this manner is to ensure that the output key is indistinguishable from random data, assuming the shared secret is also indistinguishable from random data. The output of the HMAC function is a 256-bit key. 55 | 56 | 2. **Nonce:** A nonce is a random or pseudo-random value that is meant to prevent replay attacks. For the ChaCha20 cipher, a 12-byte nonce is used. In this case, a static nonce of "NONCE1234567" is used. This means that the security of the cipher relies solely on the never-reusing any key more than once, since the nonce is not being used as an input of randomness. 57 | 58 | 3. **ChaCha20 Initialization**: The derived key and static nonce are used to initialize the ChaCha20 cipher. 59 | 60 | 4. **Payload Encryption** Each server's payload is encrypted with all the shared secrets of that server and all previous servers, in reverse order. This means the payload for the first server is encrypted once, the second server's payload is encrypted twice, and so on, creating the layered "onion" encryption. 61 | 62 | ```rust 63 | fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { 64 | let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; 65 | mu_hmac.update(shared_secret.as_bytes()); 66 | let mukey = mu_hmac.finalize().into_bytes(); 67 | 68 | let key = Key::from_slice(&mukey[0..32]); 69 | let nonce = Nonce::from_slice(b"NONCE1234567"); 70 | 71 | Ok(ChaCha20::new(&key, &nonce)) 72 | } 73 | ``` 74 | 75 | ### Peeling an Onion 76 | 77 | The peeling of the Onion is basically the decryption process that happens at each server: 78 | 79 | 1. **Shared secret:** The server creates a shared secret using its own private key and the client's ephemeral public key. 80 | 81 | 2. **Decryption:** The server uses the shared secret to derive a key for the ChaCha20 cipher. It then uses this to decrypt the payloads. Because of the layered encryption, each server can only decrypt the first remaining payload in the Onion, which reveals the payload intended for that server, while leaving the other payloads still encrypted. 82 | 83 | 3. **Payload extraction:** After decryption, the server extracts its fee, excess, and the next server's ephemeral public key from its payload. If the server is the last one in the chain, it also receives a rangeproof. 84 | 85 | 4. **Commitment recalculation:** Using the excess and fee from the decrypted payload, the server recalculates the commitment. The new commitment is `C' = (C - fee*H + excess*G)`, where `C` is the previous commitment, `H` is the hash of a known value and `G` is the generator of the elliptic curve group. 86 | 87 | 5. **New Onion creation:** The server creates a new Onion with the recalculated commitment, the next server's ephemeral public key, and the remaining still-encrypted payloads (minus the server's own decrypted payload). 88 | 89 | This process repeats until the Onion reaches the final server in the path. The final server, after peeling its layer, should find the commitment matches the provided rangeproof, thus ensuring the integrity of the transaction and the anonymity of the involved parties. 90 | 91 | ### 2-Hop Example 92 | 93 | Initial output: 94 | 95 | ```json 96 | { 97 | "value": 1000, 98 | "blind": "c2df4d2331659e8e9c780d27309dba453e34ef48f6e38aab1be50545a0431f95", 99 | "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1" 100 | } 101 | 102 | ``` 103 | 104 | Server keys: 105 | 106 | ```json 107 | [ 108 | { 109 | "sk": "a129111d283b13bf93957c06bf6605c3417b4b89db4b5cb2e7dab2c15e36e0a4", 110 | "pk": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b" 111 | }, 112 | { 113 | "sk": "2231414c56488b3596bb56b555ce1b4f8f6ed6b128914760ff89cd42c3d38ad6", 114 | "pk": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718" 115 | } 116 | ] 117 | ``` 118 | 119 | For the first hop, the user provides: 120 | ```json 121 | { 122 | "server_pubkey": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b", 123 | "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4", 124 | "fee": 5, 125 | "rangeproof": "None" 126 | } 127 | ``` 128 | The ephemeral key is randomly generated as 129 | ```json 130 | { 131 | "sk": "e8debf70567d3240f5d8e7743e3d986962de4efdd8e638e9989a3afbbafaa85f", 132 | "pk": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26" 133 | } 134 | ``` 135 | 136 | For the second hop, the user provides: 137 | ```json 138 | { 139 | "server_pubkey": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718", 140 | "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663", 141 | "fee": 5, 142 | "rangeproof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa" 143 | } 144 | ``` 145 | 146 | The generated onion becomes: 147 | ```json 148 | { 149 | "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1", 150 | "data": [ 151 | "d19f914e7a7b5ba1c0ab36d982bd0bc59aa651458a6ee86c2015b57d96424da4a41559069be5ba59e0d2b688f95f1dfb20648e21f9b01ac400cb4879f2ba434c8eb0337d3f58e9e643aa", 152 | "a5663659b17f48fba8a335e774f9968e7039db4ce67f3a9d11419e3cf24d96b1c1338d574ba8c6ac36e74b08aef0e61f0dacb5bb769aa76227f894cc49e5ac0f55800130f2a49509123963734ddf21db0fa4f662a3d37c7a0850f8c0c0dd31b26be9cce1e264d60b2240e52f465c7c2b14b11b77fbe3b2fa9bbe78e71df4083d554aa80bcfb1266bd2132ea5c5b356d54a166c16e38c66a58c19c85bb72d11e2097ab2cb13efb4f1de62b3fdf5ee90093de04e8e31e5ef036e155c3db4d7912240abd8807d26f6e9e116bfe90958a8f4347377ed774b1408adb217847efc9dc1bd0067d283a180757f5d23bcdb9e2a87dba903191ecf13ec9ff2e9bed2e774f68fa06f6f51b8f90895058ad3c699d44b9917d2b4b56096bd7885ca8d44d4b635bd975db07780006000faa40b59280aef2bf99088677f0d24efdddd670b7d0b713a9bec34f3c47cc74f594675174508061c7957fcbcf4a5f27ae9fc92f1e9d56f64a3cbcb1492c6845fd08ea04990234ea3cebf62c17f79d3a93fc6ab076fb02563579c903673759b89cfaffef68bf86daf7cb42939ee7fbc8a92e832fb0d7f8071d46323c95676d7e7821c40595d2db8dd7e29bf988eaca8f4ef6ed71a4084a01beea45497d0a5e06476c7092d7774fb4c6f9c52a0ab8bd4cc0d1569696f58deb521c2a11b774f4934fb171f3c2cf0cbfadb02c32a93a70895c5388f04824c486b5075cadf143594f46cb792145932d6a67845b5b744451517728df77f194fd5cbfda7dab160c329d7d340a2cbe3cd2accdecbd32494f75aed1892d65248124aaf9c82951edf49e46de1d80fa465ee70552d76b4f5e5f68d8bbac534f98454adc396050c9eaa7a782c3a27d6f2116a831e75cd1726b8b543738a084d7c1ee592aad80798461eb7a88ba5ea3ecf1a3329ffb7bdc882644efea1d97ceb10206356678e05aa555dd090a695da43e193ecaa239116ba1df350a86a508feb4e57696ec66f17864c394c06de614fe35c7417d51be837dfd2a1eabe135bb985be8d11847990dff17ba3f7b74a68cdfad6d83fec0700fc5fd5" 153 | ], 154 | "pubkey": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26" 155 | } 156 | ``` 157 | 158 | This is provided to the swap (first) server, which uses its server secret key to peel one layer, resulting in: 159 | ```json 160 | { 161 | "payload": { 162 | "next_ephemeral_pk": "5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442", 163 | "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4", 164 | "fee": 5, 165 | "proof": "None" 166 | }, 167 | "onion": { 168 | "commit":"08b045d9f160fd2528feb50e134a0873ae91a5ab7c44eb2a73ae246eee426bdbde", 169 | "data":[ 170 | "1df9a17573e657575b3fe17a458adc83907a9c643f1121fc5e93ca06467adc1c26fab89974d4b906683625e78ea8b778d6f48628220515699e921e8ec8059f09f4bc81cf84ace0e01fe542b9181e3cb79e4242845cf2b8c0d9a23654bede0cd149e92a6be92fa039602f5e81be2bdaaf7580e1102dd7dfec3df9dbfd4cd6977321bb7212b60810c337c1f83cce1fe9d8a1d8780cbc650dd77082b427e21cae914745f3563557f21315ed16332d09bdeee1cd5b1981433449533e515bfa223202fe8343989f57b9dc783e99b03750be23fa3ba87d973b907b173fb8d8c0790e3db3689f560eaf95c7073e9b71c453261c2e5598cdfed2503200b527724cb1a7dec033bf48e220f1a46b8cf3dc6c961d0173d10b487154fefd850c4e04923ce924a743a8ff403699ca319756e09106d5af5005e214bb02d23d9df99d6ee01fc578ecd82334d3bfdb18eeb3592d98d66a232bc1eecee972cbdf7e2f9dbc60b8fed1767afbb94220efed6a7f7ad51bfd10bc81089e650ca175c0ff0c4b0f5d592fa3166a2689871a89665c17d94a2ce9a4d25f4d174befc0a4b66e72ae6b559ae5250c23806d002a51713800190c25e310aae57167803de7413783f607b8d6cfbb3071dcb6fec6a2bfacd7b0656e8e24060c1a20c9b201ab5d5875455098770d0c4d48ceac73c9d5d6d357fb8fe24d9de27f9bd461e7076c7a28dd1e961d1e373e6890b8d4cf697a8bc11c5b252370ce2be403306390c1bf0d2aaeb6ef5f62064fb87e7fccca5e8a503c8d35651a4fbcfce89e44bd8595dac54e45d11861ca075af49cfdd1dd0bc56085548c8605c6b1706cdbcdfca0d37a77732039cfb9f28b4e216d3cc996d0e69b184d33c54d162d63efa0d7d2738dbcd09690d99277be25ce758d3a90880565d3a03e7c6308a8eb0fbbb450259bf916e1802c72f1226ccd1444503a8ce95a4e296eec4ffbba47e6a41d94b5672499b98f77e72cbed7660e2a0d66598ccc81de1055130393158d4a04805797444b0d8cc713184120a554a130ed4179e51f98db094fab5b1e27accd4b3d2351ebad62" 171 | ], 172 | "pubkey":"5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442" 173 | } 174 | } 175 | ``` 176 | 177 | This is passed to the second server, which uses its server secret key to peel the last layer, resulting in: 178 | ```json 179 | { 180 | "payload": { 181 | "next_ephemeral_pk": "0000000000000000000000000000000000000000000000000000000000000000", 182 | "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663", 183 | "fee": 5, 184 | "proof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa" 185 | }, 186 | "onion": { 187 | "commit": "0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0", 188 | "data": [], 189 | "pubkey": "0000000000000000000000000000000000000000000000000000000000000000" 190 | } 191 | } 192 | ``` 193 | 194 | The final commitment is returned as part of the last onion packet: `0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0` 195 | 196 | The final payload contains a valid rangeproof for it: `b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa` 197 | 198 | ## Security Considerations 199 | 200 | The security of this scheme comes from the use of ephemeral keys and the double encryption of payloads. Each server only has the keys to decrypt its own layer, and cannot derive the keys for any other layers. 201 | 202 | This means that a server can only see the data intended for it, and has no information about the rest of the route or the details of any previous hops. This provides strong privacy guarantees for the sender of the transaction. -------------------------------------------------------------------------------- /src/servers/swap.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::result::Result; 3 | use std::sync::Arc; 4 | 5 | use async_trait::async_trait; 6 | use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, TransactionBody}; 7 | use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; 8 | use itertools::Itertools; 9 | use secp256k1zkp::key::ZERO_KEY; 10 | use thiserror::Error; 11 | 12 | use grin_wallet_libwallet::mwixnet::onion as grin_onion; 13 | use grin_onion::crypto::comsig::ComSignature; 14 | use grin_onion::crypto::secp::{Commitment, Secp256k1, SecretKey}; 15 | use grin_onion::onion::{Onion, OnionError}; 16 | 17 | use crate::config::ServerConfig; 18 | use crate::mix_client::MixClient; 19 | use crate::node::{self, GrinNode}; 20 | use crate::store::{StoreError, SwapData, SwapStatus, SwapStore, SwapTx}; 21 | use crate::tx; 22 | use crate::wallet::Wallet; 23 | 24 | /// Swap error types 25 | #[derive(Clone, Error, Debug, PartialEq)] 26 | pub enum SwapError { 27 | #[error("Invalid number of payloads provided")] 28 | InvalidPayloadLength, 29 | #[error("Commitment Signature is invalid")] 30 | InvalidComSignature, 31 | #[error("Rangeproof is invalid")] 32 | InvalidRangeproof, 33 | #[error("Rangeproof is required but was not supplied")] 34 | MissingRangeproof, 35 | #[error("Output {commit:?} does not exist, or is already spent.")] 36 | CoinNotFound { commit: Commitment }, 37 | #[error("Output {commit:?} is already in the swap list.")] 38 | AlreadySwapped { commit: Commitment }, 39 | #[error("Failed to peel onion layer: {0:?}")] 40 | PeelOnionFailure(OnionError), 41 | #[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")] 42 | FeeTooLow { minimum_fee: u64, actual_fee: u64 }, 43 | #[error("Error saving swap to data store: {0}")] 44 | StoreError(StoreError), 45 | #[error("Error building transaction: {0}")] 46 | TxError(String), 47 | #[error("Node communication error: {0}")] 48 | NodeError(String), 49 | #[error("Client communication error: {0:?}")] 50 | ClientError(String), 51 | #[error("Swap transaction not found: {0:?}")] 52 | SwapTxNotFound(Commitment), 53 | #[error("{0}")] 54 | UnknownError(String), 55 | } 56 | 57 | impl From for SwapError { 58 | fn from(e: StoreError) -> SwapError { 59 | SwapError::StoreError(e) 60 | } 61 | } 62 | 63 | impl From for SwapError { 64 | fn from(e: tx::TxError) -> SwapError { 65 | SwapError::TxError(e.to_string()) 66 | } 67 | } 68 | 69 | impl From for SwapError { 70 | fn from(e: node::NodeError) -> SwapError { 71 | SwapError::NodeError(e.to_string()) 72 | } 73 | } 74 | 75 | /// A public MWixnet server - the "Swap Server" 76 | #[async_trait] 77 | pub trait SwapServer: Send + Sync { 78 | /// Submit a new output to be swapped. 79 | async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>; 80 | 81 | /// Iterate through all saved submissions, filter out any inputs that are no longer spendable, 82 | /// and assemble the coinswap transaction, posting the transaction to the configured node. 83 | async fn execute_round(&self) -> Result>, SwapError>; 84 | 85 | /// Verify the previous swap transaction is in the active chain or mempool. 86 | /// If it's not, rebroacast the transaction if it's still valid. 87 | /// If the transaction is no longer valid, perform the swap again. 88 | async fn check_reorg( 89 | &self, 90 | tx: &Arc, 91 | ) -> Result>, SwapError>; 92 | } 93 | 94 | /// The standard MWixnet server implementation 95 | #[derive(Clone)] 96 | pub struct SwapServerImpl { 97 | server_config: ServerConfig, 98 | next_server: Option>, 99 | wallet: Arc, 100 | node: Arc, 101 | store: Arc>, 102 | } 103 | 104 | impl SwapServerImpl { 105 | /// Create a new MWixnet server 106 | pub fn new( 107 | server_config: ServerConfig, 108 | next_server: Option>, 109 | wallet: Arc, 110 | node: Arc, 111 | store: SwapStore, 112 | ) -> Self { 113 | SwapServerImpl { 114 | server_config, 115 | next_server, 116 | wallet, 117 | node, 118 | store: Arc::new(tokio::sync::Mutex::new(store)), 119 | } 120 | } 121 | 122 | /// The fee base to use. For now, just using the default. 123 | fn get_fee_base(&self) -> u64 { 124 | DEFAULT_ACCEPT_FEE_BASE 125 | } 126 | 127 | /// Minimum fee to perform a swap. 128 | /// Requires enough fee for the swap server's kernel, 1 input and its output to swap. 129 | fn get_minimum_swap_fee(&self) -> u64 { 130 | TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() 131 | } 132 | 133 | async fn async_is_spendable(&self, next_block_height: u64, swap: &SwapData) -> bool { 134 | if let SwapStatus::Unprocessed = swap.status { 135 | if node::async_is_spendable(&self.node, &swap.input.commit, next_block_height) 136 | .await 137 | .unwrap_or(false) 138 | { 139 | if !node::async_is_unspent(&self.node, &swap.output_commit) 140 | .await 141 | .unwrap_or(true) 142 | { 143 | return true; 144 | } 145 | } 146 | } 147 | 148 | false 149 | } 150 | 151 | async fn async_execute_round( 152 | &self, 153 | store: &SwapStore, 154 | mut swaps: Vec, 155 | ) -> Result>, SwapError> { 156 | swaps.sort_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap()); 157 | 158 | if swaps.len() == 0 { 159 | return Ok(None); 160 | } 161 | 162 | let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server { 163 | // Call next mix server 164 | let onions = swaps.iter().map(|s| s.onion.clone()).collect(); 165 | let mixed = client 166 | .mix_outputs(&onions) 167 | .await 168 | .map_err(|e| SwapError::ClientError(e.to_string()))?; 169 | 170 | // Filter out failed entries 171 | let kept_indices = HashSet::<_>::from_iter(mixed.indices.clone()); 172 | let filtered = swaps 173 | .iter() 174 | .enumerate() 175 | .filter(|(i, _)| kept_indices.contains(i)) 176 | .map(|(_, j)| j.clone()) 177 | .collect(); 178 | 179 | let failed = swaps 180 | .iter() 181 | .enumerate() 182 | .filter(|(i, _)| !kept_indices.contains(i)) 183 | .map(|(_, j)| j.clone()) 184 | .collect(); 185 | 186 | ( 187 | filtered, 188 | failed, 189 | mixed.components.offset, 190 | mixed.components.outputs, 191 | mixed.components.kernels, 192 | ) 193 | } else { 194 | // Build plain outputs for each swap entry 195 | let outputs: Vec = swaps 196 | .iter() 197 | .map(|s| { 198 | Output::new( 199 | OutputFeatures::Plain, 200 | s.output_commit, 201 | s.rangeproof.unwrap(), 202 | ) 203 | }) 204 | .collect(); 205 | 206 | (swaps, Vec::new(), ZERO_KEY, outputs, Vec::new()) 207 | }; 208 | 209 | let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum(); 210 | let inputs: Vec = filtered.iter().map(|s| s.input).collect(); 211 | let output_excesses: Vec = filtered.iter().map(|s| s.excess.clone()).collect(); 212 | 213 | let tx = tx::async_assemble_tx( 214 | &self.wallet, 215 | &inputs, 216 | &outputs, 217 | &kernels, 218 | self.get_fee_base(), 219 | fees_paid, 220 | &offset, 221 | &output_excesses, 222 | ) 223 | .await?; 224 | 225 | let chain_tip = self.node.async_get_chain_tip().await?; 226 | self.node.async_post_tx(&tx).await?; 227 | 228 | store.save_swap_tx(&SwapTx { 229 | tx: tx.clone(), 230 | chain_tip, 231 | })?; 232 | 233 | // Update status to in process 234 | let kernel_commit = tx.kernels().first().unwrap().excess; 235 | for mut swap in filtered { 236 | swap.status = SwapStatus::InProcess { kernel_commit }; 237 | store.save_swap(&swap, true)?; 238 | } 239 | 240 | // Update status of failed swaps 241 | for mut swap in failed { 242 | swap.status = SwapStatus::Failed; 243 | store.save_swap(&swap, true)?; 244 | } 245 | 246 | Ok(Some(Arc::new(tx))) 247 | } 248 | } 249 | 250 | #[async_trait] 251 | impl SwapServer for SwapServerImpl { 252 | async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> { 253 | // Verify that more than 1 payload exists when there's a next server, 254 | // or that exactly 1 payload exists when this is the final server 255 | if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1 256 | || self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1 257 | { 258 | return Err(SwapError::InvalidPayloadLength); 259 | } 260 | 261 | // Verify commitment signature to ensure caller owns the output 262 | let serialized_onion = onion 263 | .serialize() 264 | .map_err(|e| SwapError::UnknownError(e.to_string()))?; 265 | let _ = comsig 266 | .verify(&onion.commit, &serialized_onion) 267 | .map_err(|_| SwapError::InvalidComSignature)?; 268 | 269 | // Verify that commitment is unspent 270 | let input = node::async_build_input(&self.node, &onion.commit) 271 | .await 272 | .map_err(|e| SwapError::UnknownError(e.to_string()))?; 273 | let input = input.ok_or(SwapError::CoinNotFound { 274 | commit: onion.commit.clone(), 275 | })?; 276 | 277 | // Peel off top layer of encryption 278 | let peeled = onion 279 | .peel_layer(&self.server_config.key) 280 | .map_err(|e| SwapError::PeelOnionFailure(e))?; 281 | 282 | // Verify the fee meets the minimum 283 | let fee: u64 = peeled.payload.fee.into(); 284 | if fee < self.get_minimum_swap_fee() { 285 | return Err(SwapError::FeeTooLow { 286 | minimum_fee: self.get_minimum_swap_fee(), 287 | actual_fee: fee, 288 | }); 289 | } 290 | 291 | // Verify the rangeproof 292 | if let Some(r) = peeled.payload.rangeproof { 293 | let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); 294 | secp.verify_bullet_proof(peeled.onion.commit, r, None) 295 | .map_err(|_| SwapError::InvalidRangeproof)?; 296 | } else if peeled.onion.enc_payloads.is_empty() { 297 | // A rangeproof is required in the last payload 298 | return Err(SwapError::MissingRangeproof); 299 | } 300 | 301 | let locked = self.store.lock().await; 302 | 303 | locked 304 | .save_swap( 305 | &SwapData { 306 | excess: peeled.payload.excess, 307 | output_commit: peeled.onion.commit, 308 | rangeproof: peeled.payload.rangeproof, 309 | input, 310 | fee, 311 | onion: peeled.onion, 312 | status: SwapStatus::Unprocessed, 313 | }, 314 | false, 315 | ) 316 | .map_err(|e| match e { 317 | StoreError::AlreadyExists(_) => SwapError::AlreadySwapped { 318 | commit: onion.commit.clone(), 319 | }, 320 | _ => SwapError::StoreError(e), 321 | })?; 322 | Ok(()) 323 | } 324 | 325 | async fn execute_round(&self) -> Result>, SwapError> { 326 | let next_block_height = self.node.async_get_chain_tip().await?.0 + 1; 327 | 328 | let locked_store = self.store.lock().await; 329 | let swaps: Vec = locked_store 330 | .swaps_iter()? 331 | .unique_by(|s| s.output_commit) 332 | .collect(); 333 | let mut spendable: Vec = vec![]; 334 | for swap in &swaps { 335 | if self.async_is_spendable(next_block_height, &swap).await { 336 | spendable.push(swap.clone()); 337 | } 338 | } 339 | 340 | self.async_execute_round(&locked_store, swaps).await 341 | } 342 | 343 | async fn check_reorg( 344 | &self, 345 | tx: &Arc, 346 | ) -> Result>, SwapError> { 347 | let excess = tx.kernels().first().unwrap().excess; 348 | let locked_store = self.store.lock().await; 349 | if let Ok(swap_tx) = locked_store.get_swap_tx(&excess) { 350 | // If kernel is in active chain, return tx 351 | if self 352 | .node 353 | .async_get_kernel(&excess, Some(swap_tx.chain_tip.0), None) 354 | .await? 355 | .is_some() 356 | { 357 | return Ok(Some(tx.clone())); 358 | } 359 | 360 | // If transaction is still valid, rebroadcast and return tx 361 | if node::async_is_tx_valid(&self.node, &tx).await? { 362 | self.node.async_post_tx(&tx).await?; 363 | return Ok(Some(tx.clone())); 364 | } 365 | 366 | // Collect all swaps based on tx's inputs, and execute_round with those swaps 367 | let next_block_height = self.node.async_get_chain_tip().await?.0 + 1; 368 | let mut swaps = Vec::new(); 369 | for input_commit in &tx.inputs_committed() { 370 | if let Ok(swap) = locked_store.get_swap(&input_commit) { 371 | if self.async_is_spendable(next_block_height, &swap).await { 372 | swaps.push(swap); 373 | } 374 | } 375 | } 376 | 377 | self.async_execute_round(&locked_store, swaps).await 378 | } else { 379 | Err(SwapError::SwapTxNotFound(excess)) 380 | } 381 | } 382 | } 383 | 384 | #[cfg(test)] 385 | pub mod mock { 386 | use std::collections::HashMap; 387 | use std::sync::Arc; 388 | 389 | use async_trait::async_trait; 390 | use grin_core::core::Transaction; 391 | 392 | use grin_onion::crypto::comsig::ComSignature; 393 | use grin_onion::onion::Onion; 394 | 395 | use super::{SwapError, SwapServer}; 396 | 397 | pub struct MockSwapServer { 398 | errors: HashMap, 399 | } 400 | 401 | impl MockSwapServer { 402 | pub fn new() -> MockSwapServer { 403 | MockSwapServer { 404 | errors: HashMap::new(), 405 | } 406 | } 407 | 408 | pub fn set_response(&mut self, onion: &Onion, e: SwapError) { 409 | self.errors.insert(onion.clone(), e); 410 | } 411 | } 412 | 413 | #[async_trait] 414 | impl SwapServer for MockSwapServer { 415 | async fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> { 416 | if let Some(e) = self.errors.get(&onion) { 417 | return Err(e.clone()); 418 | } 419 | 420 | Ok(()) 421 | } 422 | 423 | async fn execute_round(&self) -> Result>, SwapError> { 424 | Ok(None) 425 | } 426 | 427 | async fn check_reorg( 428 | &self, 429 | tx: &Arc, 430 | ) -> Result>, SwapError> { 431 | Ok(Some(tx.clone())) 432 | } 433 | } 434 | } 435 | 436 | #[cfg(test)] 437 | pub mod test_util { 438 | use std::sync::Arc; 439 | 440 | use grin_onion::crypto::dalek::DalekPublicKey; 441 | use grin_onion::crypto::secp::SecretKey; 442 | 443 | use crate::config; 444 | use crate::mix_client::MixClient; 445 | use crate::node::GrinNode; 446 | use crate::servers::swap::SwapServerImpl; 447 | use crate::store::SwapStore; 448 | use crate::wallet::mock::MockWallet; 449 | 450 | pub fn new_swapper( 451 | test_dir: &str, 452 | server_key: &SecretKey, 453 | next_server: Option<(&DalekPublicKey, &Arc)>, 454 | node: Arc, 455 | ) -> (Arc, Arc) { 456 | let config = 457 | config::test_util::local_config(&server_key, &None, &next_server.map(|n| n.0.clone())) 458 | .unwrap(); 459 | 460 | let wallet = Arc::new(MockWallet::new()); 461 | let store = SwapStore::new(test_dir).unwrap(); 462 | let swap_server = Arc::new(SwapServerImpl::new( 463 | config, 464 | next_server.map(|n| n.1.clone()), 465 | wallet.clone(), 466 | node, 467 | store, 468 | )); 469 | 470 | (swap_server, wallet) 471 | } 472 | } 473 | 474 | #[cfg(test)] 475 | mod tests { 476 | use std::sync::Arc; 477 | 478 | use ::function_name::named; 479 | use grin_core::core::{ 480 | Committed, Input, Inputs, Output, OutputFeatures, Transaction, Weighting, 481 | }; 482 | use secp256k1zkp::key::ZERO_KEY; 483 | use x25519_dalek::PublicKey as xPublicKey; 484 | 485 | use grin_onion::crypto::comsig::ComSignature; 486 | use grin_onion::crypto::secp; 487 | use grin_onion::onion::Onion; 488 | use grin_onion::test_util as onion_test_util; 489 | use grin_onion::{create_onion, new_hop, Hop}; 490 | 491 | use crate::mix_client::{self, MixClient}; 492 | use crate::node::mock::MockGrinNode; 493 | use crate::servers::mix_rpc::MixResp; 494 | use crate::servers::swap::{SwapError, SwapServer}; 495 | use crate::store::{SwapData, SwapStatus}; 496 | use crate::tx; 497 | use crate::tx::TxComponents; 498 | 499 | macro_rules! assert_error_type { 500 | ($result:expr, $error_type:pat) => { 501 | assert!($result.is_err()); 502 | assert!(if let $error_type = $result.unwrap_err() { 503 | true 504 | } else { 505 | false 506 | }); 507 | }; 508 | } 509 | 510 | macro_rules! init_test { 511 | () => {{ 512 | grin_core::global::set_local_chain_type( 513 | grin_core::global::ChainTypes::AutomatedTesting, 514 | ); 515 | let test_dir = concat!("./target/tmp/.", function_name!()); 516 | let _ = std::fs::remove_dir_all(test_dir); 517 | test_dir 518 | }}; 519 | } 520 | 521 | /// Standalone swap server to demonstrate request validation and onion unwrapping. 522 | #[tokio::test] 523 | #[named] 524 | async fn swap_standalone() -> Result<(), Box> { 525 | let test_dir = init_test!(); 526 | 527 | let value: u64 = 200_000_000; 528 | let fee: u32 = 50_000_000; 529 | let blind = secp::random_secret(); 530 | let input_commit = secp::commit(value, &blind)?; 531 | 532 | let server_key = secp::random_secret(); 533 | let hop_excess = secp::random_secret(); 534 | let (output_commit, proof) = onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 535 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 536 | 537 | let onion = create_onion(&input_commit, &vec![hop.clone()])?; 538 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 539 | 540 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 541 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 542 | server.swap(&onion, &comsig).await?; 543 | 544 | // Make sure entry is added to server. 545 | let expected = SwapData { 546 | excess: hop_excess.clone(), 547 | output_commit: output_commit.clone(), 548 | rangeproof: Some(proof), 549 | input: Input::new(OutputFeatures::Plain, input_commit.clone()), 550 | fee: fee as u64, 551 | onion: Onion { 552 | ephemeral_pubkey: xPublicKey::from([0u8; 32]), 553 | commit: output_commit.clone(), 554 | enc_payloads: vec![], 555 | }, 556 | status: SwapStatus::Unprocessed, 557 | }; 558 | 559 | { 560 | let store = server.store.lock().await; 561 | assert_eq!(1, store.swaps_iter().unwrap().count()); 562 | assert!(store.swap_exists(&input_commit).unwrap()); 563 | assert_eq!(expected, store.get_swap(&input_commit).unwrap()); 564 | } 565 | 566 | let tx = server.execute_round().await?; 567 | assert!(tx.is_some()); 568 | 569 | { 570 | // check that status was updated 571 | let store = server.store.lock().await; 572 | assert!(match store.get_swap(&input_commit)?.status { 573 | SwapStatus::InProcess { kernel_commit } => 574 | kernel_commit == tx.unwrap().kernels().first().unwrap().excess, 575 | _ => false, 576 | }); 577 | } 578 | 579 | // check that the transaction was posted 580 | let posted_txns = node.get_posted_txns(); 581 | assert_eq!(posted_txns.len(), 1); 582 | let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); 583 | assert!(posted_txn.inputs_committed().contains(&input_commit)); 584 | assert!(posted_txn.outputs_committed().contains(&output_commit)); 585 | // todo: check that outputs also contain the commitment generated by our wallet 586 | 587 | posted_txn.validate(Weighting::AsTransaction)?; 588 | 589 | Ok(()) 590 | } 591 | 592 | /// Multi-server test to verify proper MixClient communication. 593 | #[tokio::test] 594 | #[named] 595 | async fn swap_multiserver() -> Result<(), Box> { 596 | let test_dir = init_test!(); 597 | 598 | // Setup input 599 | let value: u64 = 200_000_000; 600 | let blind = secp::random_secret(); 601 | let input_commit = secp::commit(value, &blind)?; 602 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 603 | 604 | // Swapper data 605 | let swap_fee: u32 = 50_000_000; 606 | let (swap_sk, _swap_pk) = onion_test_util::rand_keypair(); 607 | let swap_hop_excess = secp::random_secret(); 608 | let swap_hop = new_hop(&swap_sk, &swap_hop_excess, swap_fee, None); 609 | 610 | // Mixer data 611 | let mixer_fee: u32 = 30_000_000; 612 | let (mixer_sk, mixer_pk) = onion_test_util::rand_keypair(); 613 | let mixer_hop_excess = secp::random_secret(); 614 | let (output_commit, proof) = onion_test_util::proof( 615 | value, 616 | swap_fee + mixer_fee, 617 | &blind, 618 | &vec![&swap_hop_excess, &mixer_hop_excess], 619 | ); 620 | let mixer_hop = new_hop(&mixer_sk, &mixer_hop_excess, mixer_fee, Some(proof)); 621 | 622 | // Create onion 623 | let onion = create_onion(&input_commit, &vec![swap_hop, mixer_hop])?; 624 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 625 | 626 | // Mock mixer 627 | let mixer_onion = onion.peel_layer(&swap_sk)?.onion; 628 | let mut mock_mixer = mix_client::mock::MockMixClient::new(); 629 | let mixer_response = TxComponents { 630 | offset: ZERO_KEY, 631 | outputs: vec![Output::new( 632 | OutputFeatures::Plain, 633 | output_commit.clone(), 634 | proof.clone(), 635 | )], 636 | kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?], 637 | }; 638 | mock_mixer.set_response( 639 | &vec![mixer_onion.clone()], 640 | MixResp { 641 | indices: vec![0 as usize], 642 | components: mixer_response, 643 | }, 644 | ); 645 | 646 | let mixer: Arc = Arc::new(mock_mixer); 647 | let (swapper, _) = super::test_util::new_swapper( 648 | &test_dir, 649 | &swap_sk, 650 | Some((&mixer_pk, &mixer)), 651 | node.clone(), 652 | ); 653 | swapper.swap(&onion, &comsig).await?; 654 | 655 | let tx = swapper.execute_round().await?; 656 | assert!(tx.is_some()); 657 | 658 | // check that the transaction was posted 659 | let posted_txns = node.get_posted_txns(); 660 | assert_eq!(posted_txns.len(), 1); 661 | let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); 662 | assert!(posted_txn.inputs_committed().contains(&input_commit)); 663 | assert!(posted_txn.outputs_committed().contains(&output_commit)); 664 | // todo: check that outputs also contain the commitment generated by our wallet 665 | 666 | posted_txn.validate(Weighting::AsTransaction)?; 667 | 668 | Ok(()) 669 | } 670 | 671 | /// Returns InvalidPayloadLength when too many payloads are provided. 672 | #[tokio::test] 673 | #[named] 674 | async fn swap_too_many_payloads() -> Result<(), Box> { 675 | let test_dir = init_test!(); 676 | 677 | let value: u64 = 200_000_000; 678 | let fee: u32 = 50_000_000; 679 | let blind = secp::random_secret(); 680 | let input_commit = secp::commit(value, &blind)?; 681 | 682 | let server_key = secp::random_secret(); 683 | let hop_excess = secp::random_secret(); 684 | let (_output_commit, proof) = 685 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 686 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 687 | 688 | let hops: Vec = vec![hop.clone(), hop.clone()]; // Multiple payloads 689 | let onion = create_onion(&input_commit, &hops)?; 690 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 691 | 692 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 693 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 694 | let result = server.swap(&onion, &comsig).await; 695 | assert_eq!(Err(SwapError::InvalidPayloadLength), result); 696 | 697 | // Make sure no entry is added to the store 698 | assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); 699 | 700 | Ok(()) 701 | } 702 | 703 | /// Returns InvalidComSignature when ComSignature fails to verify. 704 | #[tokio::test] 705 | #[named] 706 | async fn swap_invalid_com_signature() -> Result<(), Box> { 707 | let test_dir = init_test!(); 708 | 709 | let value: u64 = 200_000_000; 710 | let fee: u32 = 50_000_000; 711 | let blind = secp::random_secret(); 712 | let input_commit = secp::commit(value, &blind)?; 713 | 714 | let server_key = secp::random_secret(); 715 | let hop_excess = secp::random_secret(); 716 | let (_output_commit, proof) = 717 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 718 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 719 | 720 | let onion = create_onion(&input_commit, &vec![hop])?; 721 | 722 | let wrong_blind = secp::random_secret(); 723 | let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; 724 | 725 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 726 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 727 | let result = server.swap(&onion, &comsig).await; 728 | assert_eq!(Err(SwapError::InvalidComSignature), result); 729 | 730 | // Make sure no entry is added to the store 731 | assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); 732 | 733 | Ok(()) 734 | } 735 | 736 | /// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment. 737 | #[tokio::test] 738 | #[named] 739 | async fn swap_invalid_rangeproof() -> Result<(), Box> { 740 | let test_dir = init_test!(); 741 | 742 | let value: u64 = 200_000_000; 743 | let fee: u32 = 50_000_000; 744 | let blind = secp::random_secret(); 745 | let input_commit = secp::commit(value, &blind)?; 746 | 747 | let server_key = secp::random_secret(); 748 | let hop_excess = secp::random_secret(); 749 | let wrong_value = value + 10_000_000; 750 | let (_output_commit, proof) = 751 | onion_test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]); 752 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 753 | 754 | let onion = create_onion(&input_commit, &vec![hop])?; 755 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 756 | 757 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 758 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 759 | let result = server.swap(&onion, &comsig).await; 760 | assert_eq!(Err(SwapError::InvalidRangeproof), result); 761 | 762 | // Make sure no entry is added to the store 763 | assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); 764 | 765 | Ok(()) 766 | } 767 | 768 | /// Returns MissingRangeproof when no rangeproof is provided. 769 | #[tokio::test] 770 | #[named] 771 | async fn swap_missing_rangeproof() -> Result<(), Box> { 772 | let test_dir = init_test!(); 773 | 774 | let value: u64 = 200_000_000; 775 | let fee: u32 = 50_000_000; 776 | let blind = secp::random_secret(); 777 | let input_commit = secp::commit(value, &blind)?; 778 | 779 | let server_key = secp::random_secret(); 780 | let hop_excess = secp::random_secret(); 781 | let hop = new_hop(&server_key, &hop_excess, fee, None); 782 | 783 | let onion = create_onion(&input_commit, &vec![hop])?; 784 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 785 | 786 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 787 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 788 | let result = server.swap(&onion, &comsig).await; 789 | assert_eq!(Err(SwapError::MissingRangeproof), result); 790 | 791 | // Make sure no entry is added to the store 792 | assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); 793 | 794 | Ok(()) 795 | } 796 | 797 | /// Returns CoinNotFound when there's no matching output in the UTXO set. 798 | #[tokio::test] 799 | #[named] 800 | async fn swap_utxo_missing() -> Result<(), Box> { 801 | let test_dir = init_test!(); 802 | 803 | let value: u64 = 200_000_000; 804 | let fee: u32 = 50_000_000; 805 | let blind = secp::random_secret(); 806 | let input_commit = secp::commit(value, &blind)?; 807 | 808 | let server_key = secp::random_secret(); 809 | let hop_excess = secp::random_secret(); 810 | let (_output_commit, proof) = 811 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 812 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 813 | 814 | let onion = create_onion(&input_commit, &vec![hop])?; 815 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 816 | 817 | let node: Arc = Arc::new(MockGrinNode::new()); 818 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 819 | let result = server.swap(&onion, &comsig).await; 820 | assert_eq!( 821 | Err(SwapError::CoinNotFound { 822 | commit: input_commit.clone() 823 | }), 824 | result 825 | ); 826 | 827 | // Make sure no entry is added to the store 828 | assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); 829 | 830 | Ok(()) 831 | } 832 | 833 | /// Returns AlreadySwapped when trying to swap the same commitment multiple times. 834 | #[tokio::test] 835 | #[named] 836 | async fn swap_already_swapped() -> Result<(), Box> { 837 | let test_dir = init_test!(); 838 | 839 | let value: u64 = 200_000_000; 840 | let fee: u32 = 50_000_000; 841 | let blind = secp::random_secret(); 842 | let input_commit = secp::commit(value, &blind)?; 843 | 844 | let server_key = secp::random_secret(); 845 | let hop_excess = secp::random_secret(); 846 | let (_output_commit, proof) = 847 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 848 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 849 | 850 | let onion = create_onion(&input_commit, &vec![hop])?; 851 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 852 | 853 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 854 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 855 | server.swap(&onion, &comsig).await?; 856 | 857 | // Call swap a second time 858 | let result = server.swap(&onion, &comsig).await; 859 | assert_eq!( 860 | Err(SwapError::AlreadySwapped { 861 | commit: input_commit.clone() 862 | }), 863 | result 864 | ); 865 | 866 | Ok(()) 867 | } 868 | 869 | /// Returns SwapTxNotFound when trying to check_reorg with a transaction not found in the store. 870 | #[tokio::test] 871 | #[named] 872 | async fn swap_tx_not_found() -> Result<(), Box> { 873 | let test_dir = init_test!(); 874 | 875 | let server_key = secp::random_secret(); 876 | let node: Arc = Arc::new(MockGrinNode::new()); 877 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 878 | let kern = tx::build_kernel(&secp::random_secret(), 1000u64)?; 879 | let tx: Arc = 880 | Arc::new(Transaction::new(Inputs::default(), &[], &[kern.clone()])); 881 | let result = server.check_reorg(&tx).await; 882 | assert_eq!(Err(SwapError::SwapTxNotFound(kern.excess())), result); 883 | 884 | Ok(()) 885 | } 886 | 887 | /// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload. 888 | #[tokio::test] 889 | #[named] 890 | async fn swap_peel_onion_failure() -> Result<(), Box> { 891 | let test_dir = init_test!(); 892 | 893 | let value: u64 = 200_000_000; 894 | let fee: u32 = 50_000_000; 895 | let blind = secp::random_secret(); 896 | let input_commit = secp::commit(value, &blind)?; 897 | 898 | let server_key = secp::random_secret(); 899 | let hop_excess = secp::random_secret(); 900 | let (_output_commit, proof) = 901 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 902 | 903 | let wrong_server_key = secp::random_secret(); 904 | let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof)); 905 | 906 | let onion = create_onion(&input_commit, &vec![hop])?; 907 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 908 | 909 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 910 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 911 | let result = server.swap(&onion, &comsig).await; 912 | 913 | assert!(result.is_err()); 914 | assert_error_type!(result, SwapError::PeelOnionFailure(_)); 915 | 916 | Ok(()) 917 | } 918 | 919 | /// Returns FeeTooLow when the minimum fee is not met. 920 | #[tokio::test] 921 | #[named] 922 | async fn swap_fee_too_low() -> Result<(), Box> { 923 | let test_dir = init_test!(); 924 | 925 | let value: u64 = 200_000_000; 926 | let fee: u32 = 1_000_000; 927 | let blind = secp::random_secret(); 928 | let input_commit = secp::commit(value, &blind)?; 929 | 930 | let server_key = secp::random_secret(); 931 | let hop_excess = secp::random_secret(); 932 | let (_output_commit, proof) = 933 | onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); 934 | let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 935 | 936 | let onion = create_onion(&input_commit, &vec![hop])?; 937 | let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; 938 | 939 | let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); 940 | let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); 941 | let result = server.swap(&onion, &comsig).await; 942 | assert_eq!( 943 | Err(SwapError::FeeTooLow { 944 | minimum_fee: 12_500_000, 945 | actual_fee: fee as u64, 946 | }), 947 | result 948 | ); 949 | 950 | Ok(()) 951 | } 952 | } 953 | --------------------------------------------------------------------------------