diff --git a/Cargo.lock b/Cargo.lock index dad800c9..e380988e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,12 +926,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" version = "2.1.1" @@ -1569,6 +1563,7 @@ dependencies = [ "blake2-rfc", "bs58", "byteorder", + "chacha20", "chrono", "curve25519-dalek 2.1.3", "ed25519-dalek", @@ -1578,6 +1573,7 @@ dependencies = [ "grin_util", "grin_wallet_config", "grin_wallet_util", + "hmac 0.12.1", "lazy_static", "log", "num-bigint", @@ -1587,7 +1583,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2 0.8.2", + "sha2 0.10.8", "strum", "strum_macros", "thiserror", @@ -3617,18 +3613,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" version = "0.9.9" diff --git a/api/src/owner.rs b/api/src/owner.rs index f27aaa1f..a749d8b8 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -16,6 +16,7 @@ use chrono::prelude::*; use ed25519_dalek::SecretKey as DalekSecretKey; +use grin_wallet_libwallet::mwixnet::{MixnetReqCreationParams, SwapReq}; use grin_wallet_libwallet::RetrieveTxQueryArgs; use uuid::Uuid; @@ -33,7 +34,7 @@ use crate::libwallet::{ TxLogEntry, ViewWallet, WalletInfo, WalletInst, WalletLCProvider, }; use crate::util::logger::LoggingConfig; -use crate::util::secp::key::SecretKey; +use crate::util::secp::{key::SecretKey, pedersen::Commitment}; use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; use grin_wallet_util::OnionV3Address; use std::convert::TryFrom; @@ -2423,6 +2424,71 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; owner::build_output(&mut **w, keychain_mask, features, amount) } + + // MWIXNET + + /// Creates an mwixnet request [SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html) + /// from a given output commitment under this wallet's control. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `params` - A [MixnetReqCreationParams](../grin_wallet_libwallet/api_impl/types/struct.MixnetReqCreationParams.html) + /// struct containing the parameters for the request, which include: + /// `server_keys` - The public keys of the servers participating in the mixnet (each encoded internally as a `SecretKey`) + /// `fee_per_hop` - The fee to be paid to each server for each hop in the mixnet + /// * `commitment` - The commitment of the output to be mixed + /// * `lock_output` - Whether to lock the referenced output after creating the request + /// + /// # Returns + /// * Ok([SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html)) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let keychain_mask = None; + /// let params = MixnetReqCreationParams { + /// server_keys: vec![], // Public keys here in secret key representation + /// fee_per_hop: 100, + /// }; + /// + /// let commitment = Commitment::from_vec(vec![0; 32]); + /// let lock_output = true; + /// + /// let result = api_owner.create_mwixnet_req( + /// keychain_mask, + /// ¶ms, + /// &commitment, + /// lock_output, + /// ); + /// + /// if let Ok(req) = result { + /// //... + /// } + /// ``` + + pub fn create_mwixnet_req( + &self, + keychain_mask: Option<&SecretKey>, + params: &MixnetReqCreationParams, + commitment: &Commitment, + lock_output: bool, // use_test_rng: bool, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::create_mwixnet_req( + &mut **w, + keychain_mask, + params, + commitment, + lock_output, + self.doctest_mode, + ) + } } /// attempt to send slate synchronously with TOR @@ -2524,13 +2590,17 @@ macro_rules! doctest_helper_setup_doc_env { use keychain::ExtKeychain; use tempfile::tempdir; + use grin_util::secp::pedersen::Commitment; use std::sync::Arc; use util::{Mutex, ZeroingString}; use api::{Foreign, Owner}; use config::WalletConfig; use impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; - use libwallet::{BlockFees, InitTxArgs, IssueInvoiceTxArgs, Slate, WalletInst}; + use libwallet::{ + mwixnet::MixnetReqCreationParams, BlockFees, InitTxArgs, IssueInvoiceTxArgs, Slate, + WalletInst, + }; use uuid::Uuid; diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 23fc6b63..c983c20c 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -14,6 +14,7 @@ //! JSON-RPC Stub generation for the Owner API use grin_wallet_libwallet::RetrieveTxQueryArgs; +use libwallet::mwixnet::SwapReq; use uuid::Uuid; use crate::config::{TorConfig, WalletConfig}; @@ -21,14 +22,15 @@ use crate::core::core::OutputFeatures; use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::{ - AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, - NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack, - SlatepackAddress, StatusMessage, TxLogEntry, VersionedSlate, ViewWallet, WalletInfo, - WalletLCProvider, + mwixnet::MixnetReqCreationParams, AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, + IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, + SlateVersion, Slatepack, SlatepackAddress, StatusMessage, TxLogEntry, VersionedSlate, + ViewWallet, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::{PublicKey, SecretKey}; -use crate::util::{static_secp_instance, Mutex, ZeroingString}; +use crate::util::secp::pedersen::Commitment; +use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; use crate::{ECDHPubkey, Ed25519SecretKey, Owner, Token}; use easy_jsonrpc_mw; use grin_wallet_util::OnionV3Address; @@ -1963,6 +1965,63 @@ pub trait OwnerRpc { features: OutputFeatures, amount: Amount, ) -> Result; + + /** + Networked version of [Owner::build_output](struct.Owner.html#method.create_mwixnet_req). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_mwixnet_req", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "commitment": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "fee_per_hop": "5000000", + "lock_output": true, + "server_keys": [ + "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332", + "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e", + "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5" + ] + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "comsig": "099561ed0be59f6502ee358ee4f6760cd16d6be04d58d7a2c1bf2fd09dd7fd2d291beaae5483c6f18d1ceaae6321f06f9ba129a1ee9e7d15f152c67397a621538b5c10bbeb95140dee815c02657c91152939afe389458dc59af095e8e8e5c81a08", + "onion": { + "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "data": [ + "37f68116475e1aa6b58fc911addbd0e04e7aa19ab3e82e7b5cfcaf57d82cf35e7388ce51711cc5ef8cf7630f7dc7229878f91c7ec85991a7fc0051a7bbc66569db3a3aa89ef490055f3c", + "b9ff8c0c1699808efce46d581647c65764a28e813023ae677d688282422a07505ae1a051037d7ba58f3279846d0300800fc1c5bfcc548dab815e9fd2f29df9515170c41fa6e4e44b8bcb", + "62ea6b8369686a0415e1e752b9b4d6e66cf5b6066a2d3c60d8818890a55f3adff4601466f4c6e6b646568b99ae93549a3595b7a7b4be815ced87d9297cabbd69518d7b2ed6edd14007528fd346aaea765a1165fe886666627ebcab9588b8ee1c9e98395ae67913c48eb6e924581b40182fce807f97312fb07fd5e216d99941f2b488babce4078a50cd66b28b30a66c4f54fcc127437408a99b30ffd6c3d0d8c7d39e864fc04e321b8c10138c8852d4cad0a4f2780412b9dadcc6e0f2657b7803a81bccb809ca392464be2e01755be7377d0e815698ad6ea51d4617cc92c3ccf852f038e33cc9c90992438ba5c49cca7cc188b682da684e2f4c9733a84a7b64ac5c2216ebf5926f0ee67b664fb5bab799109cbee755ce1aebc8cd352fea51cd84c333cb958093c53544c3f3ab05dba64d8f041c3b179796b476ec04b11044e39db6994ab767315e52cc0ef023432ec88ade2911612db7e74e0923889f765b58b00e3869c5072a4e882c1b721913f63bda986b8c97b7ae575f0d4be596a1ac3cd0db96ce6074ee000b32018b3bda16d7dba34a13ba9c3ce983946414c16e278351a3411cb8ef2cb8ef5b6e1667c4c58bc797c0324ae4fec8960d684e561c0e833ee4c3331c6c439b59042a62993535e23cc8a8a4cf705c0f9b1d62db4e3d76c22c01138800414b143ddff471e4df4413e842a1b41f43cc9647e47145fd6c86d4d1a34fb2f62f5a55b31c9353ee34743c548eff955f2d2143c1a86cbcb452104f96d0142db31153021bbeed995c71a92de8fb1f97269533a508085c543fcb3ee57000bb265e74187b858403aa97b6c7b085e5d5b6025cbfe5f6926d33c835f90e60fc62013e80bbe0a855da5938b4b8f83ac29c5e8251827795356222079a6d1612e2fdf93bd7836d1613c7a353ada48ce256f880bbbb3108e037e3b5647101bd4d549101b0ee73d2248a932a802a3b1beb0b69d777c4285d57e91d83e96fe2f8a1a2f182fe2c6ca37b18460cf8d7f56c201147b9be19f1d01f8ad305c1e9c4dd79b5d8719d6550432352cf737082b1e9de7a083ffbe1" + ], + "pubkey": "e7ee7d51b11d09f268ade98bc9d7ae9be3c4ac124ce1c3a40e50d34460fa5f08" + } + } + } + } + # "# + # , 5, true, true, false, false); + ``` + * + */ + + fn create_mwixnet_req( + &self, + token: Token, + commitment: String, + fee_per_hop: String, + lock_output: bool, + server_keys: Vec, + ) -> Result; } impl OwnerRpc for Owner @@ -2372,6 +2431,44 @@ where ) -> Result { Owner::build_output(self, (&token.keychain_mask).as_ref(), features, amount.0) } + + fn create_mwixnet_req( + &self, + token: Token, + commitment: String, + fee_per_hop: String, + lock_output: bool, + server_keys: Vec, + ) -> Result { + let commit = + Commitment::from_vec(from_hex(&commitment).map_err(|e| Error::CommitDeser(e))?); + + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let mut keys = vec![]; + for key in server_keys { + keys.push(SecretKey::from_slice( + &secp, + &grin_util::from_hex(&key).map_err(|e| Error::ServerKeyDeser(e))?, + )?) + } + + let req_params = MixnetReqCreationParams { + server_keys: keys, + fee_per_hop: fee_per_hop + .parse::() + .map_err(|_| Error::U64Deser(fee_per_hop))?, + }; + + Owner::create_mwixnet_req( + self, + (&token.keychain_mask).as_ref(), + &req_params, + &commit, + lock_output, + ) + } } /// helper to set up a real environment to run integrated doctests diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index b1adaa55..2fe7d648 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -82,16 +82,16 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { wallet_inst!(wallet1, w); w.set_parent_key_id_by_name("mining")?; } - let mut bh = 10u64; + let mut _bh = 10u64; let _ = - test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, _bh as usize, false); // Sanity check wallet 1 contents wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; assert!(wallet1_refreshed); - assert_eq!(wallet1_info.last_confirmed_height, bh); - assert_eq!(wallet1_info.total, bh * reward); + assert_eq!(wallet1_info.last_confirmed_height, _bh); + assert_eq!(wallet1_info.total, _bh * reward); Ok(()) })?; @@ -138,10 +138,10 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { api.post_tx(m, &slate, false)?; Ok(()) })?; - bh += 1; + _bh += 1; let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); - bh += 3; + _bh += 3; // Check transaction log for wallet 2 wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { @@ -151,7 +151,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { assert!(txs.len() == 1); println!( "last confirmed height: {}, bh: {}", - wallet2_info.last_confirmed_height, bh + wallet2_info.last_confirmed_height, _bh ); assert!(refreshed); Ok(()) @@ -163,10 +163,10 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; assert!(refreshed); - assert_eq!(txs.len() as u64, bh + 1); + assert_eq!(txs.len() as u64, _bh + 1); println!( "Wallet 1: last confirmed height: {}, bh: {}", - wallet1_info.last_confirmed_height, bh + wallet1_info.last_confirmed_height, _bh ); Ok(()) })?; @@ -248,7 +248,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // test that payee can only cancel once let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); - bh += 3; + _bh += 3; wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { // Wallet 2 inititates an invoice transaction, requesting payment diff --git a/controller/tests/mwixnet.rs b/controller/tests/mwixnet.rs new file mode 100644 index 00000000..0db537d2 --- /dev/null +++ b/controller/tests/mwixnet.rs @@ -0,0 +1,190 @@ +// Copyright 2024 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self, then creation of comsig request +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_util as util; +use grin_util::secp::key::SecretKey; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{mwixnet::MixnetReqCreationParams, InitTxArgs}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let mut slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + // Send directly to self + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = api.receive_tx(&slate, Some("listener"), None)?; + Ok(()) + })?; + slate = api.finalize_tx(m, &slate)?; + api.post_tx(m, &slate, false)?; // mines a block + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'listener' account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("listener")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, 2 * reward); + Ok(()) + })?; + + // Recipient wallet creates a mwixnet request from the last output + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let secp_locked = util::static_secp_instance(); + let secp = secp_locked.lock(); + let server_pubkey_str_1 = + "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332"; + let server_pubkey_str_2 = + "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e"; + let server_pubkey_str_3 = + "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5"; + let server_key_1 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_1).unwrap()) + .unwrap(); + let server_key_2 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_2).unwrap()) + .unwrap(); + let server_key_3 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_3).unwrap()) + .unwrap(); + let params = MixnetReqCreationParams { + server_keys: vec![server_key_1, server_key_2, server_key_3], + fee_per_hop: 50_000_000, + }; + let outputs = api.retrieve_outputs(mask1, false, false, None)?; + // get last output + let last_output = outputs.1[outputs.1.len() - 1].clone(); + + let mwixnet_req = api.create_mwixnet_req(m, ¶ms, &last_output.commit, true)?; + + println!("MWIXNET REQ: {:?}", mwixnet_req); + + // check output we created comsig for is indeed locked + let outputs = api.retrieve_outputs(mask1, false, false, None)?; + // get last output + let last_output = outputs.1[outputs.1.len() - 1].clone(); + assert!(last_output.output.status == libwallet::OutputStatus::Locked); + + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn mwixnet_comsig_test() { + let test_dir = "test_output/mwixnet"; + setup(test_dir); + if let Err(e) = mwixnet_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 584c4618..dcfe8a4d 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -27,7 +27,7 @@ ed25519-dalek = "1.0.0-pre.4" x25519-dalek = "0.6" base64 = "0.9" regex = "1.3" -sha2 = "0.8" +sha2 = "0.10.0" bs58 = "0.3" age = "0.7" curve25519-dalek = "2.1" @@ -36,6 +36,10 @@ bech32 = "0.7" byteorder = "1.3" num-bigint = "0.2" +#mwixnet onion +chacha20 = "0.8.1" +hmac = { version = "0.12.0", features = ["std"]} + grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 6432656e..017bf4e9 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -18,10 +18,10 @@ use uuid::Uuid; use crate::api_impl::foreign::finalize_tx as foreign_finalize; use crate::grin_core::core::hash::Hashed; -use crate::grin_core::core::{Output, OutputFeatures, Transaction}; +use crate::grin_core::core::{FeeFields, Output, OutputFeatures, Transaction}; use crate::grin_core::libtx::proof; use crate::grin_keychain::ViewKey; -use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::{key::SecretKey, pedersen::Commitment}; use crate::grin_util::Mutex; use crate::grin_util::ToHex; use crate::util::{OnionV3Address, OnionV3AddressError}; @@ -33,14 +33,18 @@ use crate::slate::{PaymentInfo, Slate, SlateState}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; use crate::Error; use crate::{ - address, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, + address, + mwixnet::{create_onion, ComSignature, Hop, MixnetReqCreationParams, SwapReq}, + wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, WalletInst, WalletLCProvider, }; + use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::Verifier; +use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; use std::convert::{TryFrom, TryInto}; use std::sync::mpsc::Sender; @@ -1369,3 +1373,106 @@ where output: output, }) } + +/// Create MXMixnet request +pub fn create_mwixnet_req<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + params: &MixnetReqCreationParams, + commitment: &Commitment, + lock_output: bool, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = w.parent_key_id(); + let keychain = w.keychain(keychain_mask)?; + let outputs = updater::retrieve_outputs(w, keychain_mask, false, None, Some(&parent_key_id))?; + + let mut output = None; + for o in &outputs { + if o.commit == *commitment { + output = Some(o.output.clone()); + break; + } + } + + if output.is_none() { + return Err(Error::GenericError(String::from("output not found"))); + } + + let amount = output.clone().unwrap().value; + let input_blind = keychain.derive_key( + amount, + &output.clone().unwrap().key_id, + SwitchCommitmentType::Regular, + )?; + + let mut server_pubkeys = vec![]; + for i in 0..params.server_keys.len() { + server_pubkeys.push(xPublicKey::from(&StaticSecret::from( + params.server_keys[i].0, + ))); + } + + let fee = grin_core::libtx::tx_fee(1, 1, 1); + let new_amount = amount - (fee * server_pubkeys.len() as u64); + let new_output = build_output(w, keychain_mask, OutputFeatures::Plain, new_amount)?; + let secp = keychain.secp(); + + let mut blind_sum = new_output + .blind + .split(&BlindingFactor::from_secret_key(input_blind.clone()), &secp)?; + + let hops = server_pubkeys + .iter() + .enumerate() + .map(|(i, &p)| { + if (i + 1) == server_pubkeys.len() { + Hop { + server_pubkey: p.clone(), + excess: blind_sum.secret_key(&secp).unwrap(), + fee: FeeFields::from(fee as u32), + rangeproof: Some(new_output.output.proof.clone()), + } + } else { + let hop_excess; + if use_test_rng { + hop_excess = BlindingFactor::zero(); + } else { + hop_excess = BlindingFactor::rand(&secp); + } + blind_sum = blind_sum.split(&hop_excess, &secp).unwrap(); + Hop { + server_pubkey: p.clone(), + excess: hop_excess.secret_key(&secp).unwrap(), + fee: FeeFields::from(fee as u32), + rangeproof: None, + } + } + }) + .collect(); + + let onion = create_onion(&commitment, &hops, use_test_rng).unwrap(); + let comsig = ComSignature::sign( + amount, + &input_blind, + &onion.serialize().unwrap(), + use_test_rng, + ) + .unwrap(); + + // Lock output if requested + if lock_output { + let mut batch = w.batch(keychain_mask)?; + let mut update_output = batch.get(&output.as_ref().unwrap().key_id, &None)?; + update_output.lock(); + batch.lock_output(&mut update_output)?; + batch.commit()?; + } + + Ok(SwapReq { comsig, onion }) +} diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index f50bfc48..d942cd61 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -26,6 +26,8 @@ use crate::SlatepackAddress; use chrono::prelude::*; use ed25519_dalek::Signature as DalekSignature; +pub use crate::mwixnet::{Hop, MixnetReqCreationParams, SwapReq}; + /// Type for storing amounts (in nanogrins). /// Serializes as a string but can deserialize from a string or u64. #[derive(Serialize, Deserialize)] diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index fe4bbd8c..d5a9f9cd 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -181,6 +181,18 @@ pub enum Error { #[error("Committed Error: {0}")] Commit(String), + /// Error Deserializing commit + #[error("Commit Deserialize Error: {0}")] + CommitDeser(String), + + /// Error Deserializing key + #[error("Server Key Deserialize Error: {0}")] + ServerKeyDeser(String), + + /// Parsing integert + #[error("Can't parse as u64: {0}")] + U64Deser(String), + /// Can't parse slate version #[error("Can't parse slate version")] SlateVersionParse, diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 3c663a9c..95185c5c 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -47,6 +47,7 @@ pub mod address; pub mod api_impl; mod error; mod internal; +pub mod mwixnet; mod slate; pub mod slate_versions; pub mod slatepack; diff --git a/libwallet/src/mwixnet/mod.rs b/libwallet/src/mwixnet/mod.rs new file mode 100644 index 00000000..06052658 --- /dev/null +++ b/libwallet/src/mwixnet/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion modules for mxmixnet +mod onion; +mod types; + +pub use onion::{ + create_onion, onion::Onion, onion::OnionError, util as onion_util, ComSigError, ComSignature, + MwixnetPublicKey, +}; + +pub use types::{Hop, MixnetReqCreationParams, SwapReq}; diff --git a/libwallet/src/mwixnet/onion/crypto/comsig.rs b/libwallet/src/mwixnet/onion/crypto/comsig.rs new file mode 100644 index 00000000..bf9e3479 --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/comsig.rs @@ -0,0 +1,230 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Comsig modules for mxmixnet + +use grin_util::secp::{ + self as secp256k1zkp, pedersen::Commitment, rand::thread_rng, ContextFlag, Secp256k1, SecretKey, +}; + +use blake2_rfc::blake2b::Blake2b; +use byteorder::{BigEndian, ByteOrder}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use rand::rngs::mock::StepRng; +use thiserror::Error; + +/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys +#[derive(Clone, Debug)] +pub struct ComSignature { + pub_nonce: Commitment, + s: SecretKey, + t: SecretKey, +} + +/// Error types for Commitment Signatures +#[derive(Error, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum ComSigError { + /// Invalid commitment signature + #[error("Commitment signature is invalid")] + InvalidSig, + /// Secp256k1zkp error + #[error("Secp256k1zkp error: {0:?}")] + Secp256k1zkp(secp256k1zkp::Error), +} + +impl From for ComSigError { + fn from(err: secp256k1zkp::Error) -> ComSigError { + ComSigError::Secp256k1zkp(err) + } +} + +impl ComSignature { + /// Create a new ComSignature + pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { + ComSignature { + pub_nonce: pub_nonce.to_owned(), + s: s.to_owned(), + t: t.to_owned(), + } + } + + /// Sign commitment with amount, blinding factor, and message + pub fn sign( + amount: u64, + blind: &SecretKey, + msg: &Vec, + use_test_rng: bool, + ) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let mut amt_bytes = [0; 32]; + BigEndian::write_u64(&mut amt_bytes[24..32], amount); + let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; + let k_1; + let k_2; + + if use_test_rng { + // allow for consistent test results + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + k_1 = SecretKey::new(&secp, &mut test_rng); + k_2 = SecretKey::new(&secp, &mut test_rng); + } else { + k_1 = SecretKey::new(&secp, &mut thread_rng()); + k_2 = SecretKey::new(&secp, &mut thread_rng()); + } + + let commitment = secp.commit(amount, blind.clone())?; + let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; + + let e = ComSignature::calc_challenge( + &secp, + &commitment, + &nonce_commitment, + &msg, + use_test_rng, + )?; + + // s = k_1 + (e * amount) + let mut s = k_amt.clone(); + s.mul_assign(&secp, &e)?; + s.add_assign(&secp, &k_1)?; + + // t = k_2 + (e * blind) + let mut t = blind.clone(); + t.mul_assign(&secp, &e)?; + t.add_assign(&secp, &k_2)?; + + Ok(ComSignature::new(&nonce_commitment, &s, &t)) + } + + /// Verify the commitment signature + pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let s1 = secp.commit_blind(self.s.clone(), self.t.clone())?; + + let mut ce = commit.to_pubkey(&secp)?; + let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg, false)?; + ce.mul_assign(&secp, &e)?; + + let commits = vec![Commitment::from_pubkey(&secp, &ce)?, self.pub_nonce.clone()]; + let s2 = secp.commit_sum(commits, Vec::new())?; + + if s1 != s2 { + return Err(ComSigError::InvalidSig); + } + + Ok(()) + } + + fn calc_challenge( + secp: &Secp256k1, + commit: &Commitment, + nonce_commit: &Commitment, + msg: &Vec, + use_test_rng: bool, + ) -> Result { + let mut challenge_hasher = Blake2b::new(32); + if use_test_rng { + return Ok(super::secp::random_secret(use_test_rng)); + } + challenge_hasher.update(&commit.0); + challenge_hasher.update(&nonce_commit.0); + challenge_hasher.update(msg); + + let mut challenge = [0; 32]; + challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); + + Ok(SecretKey::from_slice(&secp, &challenge)?) + } +} + +/// Serializes a ComSignature to and from hex +pub mod comsig_serde { + use super::ComSignature; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use serde::{Deserialize, Serializer}; + + /// Serializes a ComSignature as a hex string + pub fn serialize(comsig: &ComSignature, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; + serializer.serialize_str(&bytes.to_hex()) + } + + /// Creates a ComSignature from a hex string + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let bytes = String::deserialize(deserializer) + .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; + let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?; + Ok(sig) + } +} + +#[allow(non_snake_case)] +impl Readable for ComSignature { + fn read(reader: &mut R) -> Result { + let R = Commitment::read(reader)?; + let s = super::secp::read_secret_key(reader)?; + let t = super::secp::read_secret_key(reader)?; + Ok(ComSignature::new(&R, &s, &t)) + } +} + +impl Writeable for ComSignature { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.pub_nonce.0)?; + writer.write_fixed_bytes(self.s.0)?; + writer.write_fixed_bytes(self.t.0)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; + + use grin_util::secp::rand::{thread_rng, RngCore}; + use rand::Rng; + + /// Test signing and verification of ComSignatures + #[test] + fn verify_comsig() -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let amount = thread_rng().next_u64(); + let blind = SecretKey::new(&secp, &mut thread_rng()); + let msg: [u8; 16] = rand::thread_rng().gen(); + let comsig = ComSignature::sign(amount, &blind, &msg.to_vec(), false)?; + + let commit = secp.commit(amount, blind.clone())?; + assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); + + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); + + let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; + assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); + + Ok(()) + } +} diff --git a/libwallet/src/mwixnet/onion/crypto/dalek.rs b/libwallet/src/mwixnet/onion/crypto/dalek.rs new file mode 100644 index 00000000..a10c65b0 --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/dalek.rs @@ -0,0 +1,297 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Dalek key wrapper for mwixnet primitives + +use grin_util::secp::key::SecretKey; + +use ed25519_dalek::{PublicKey, Signature, Verifier}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use grin_util::ToHex; +use thiserror::Error; + +/// Error types for Dalek structures and logic +#[derive(Clone, Error, Debug, PartialEq)] +pub enum DalekError { + /// Hex deser error + #[error("Hex error {0:?}")] + HexError(String), + /// Key parsing error + #[error("Failed to parse secret key")] + KeyParseError, + /// Error validating signature + #[error("Failed to verify signature")] + SigVerifyFailed, +} + +/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekPublicKey(PublicKey); + +impl DalekPublicKey { + /// Convert DalekPublicKey to hex string + pub fn to_hex(&self) -> String { + self.0.to_hex() + } + + /// Convert hex string to DalekPublicKey. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let pk = PublicKey::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekPublicKey(pk)) + } + + /// Compute DalekPublicKey from a SecretKey + pub fn from_secret(key: &SecretKey) -> Self { + let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap(); + let pk: PublicKey = (&secret).into(); + DalekPublicKey(pk) + } +} + +impl AsRef for DalekPublicKey { + fn as_ref(&self) -> &PublicKey { + &self.0 + } +} + +#[cfg(test)] +/// Serializes an Option to and from hex +pub mod option_dalek_pubkey_serde { + use super::DalekPublicKey; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(pk: &Option, serializer: S) -> Result + where + S: Serializer, + { + match pk { + Some(pk) => serializer.serialize_str(&pk.0.to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => DalekPublicKey::from_hex(&string) + .map_err(|e| Error::custom(e.to_string())) + .and_then(|pk: DalekPublicKey| Ok(Some(pk))), + None => Ok(None), + }) + } +} + +impl Readable for DalekPublicKey { + fn read(reader: &mut R) -> Result { + let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?) + .map_err(|_| ser::Error::CorruptedData)?; + Ok(DalekPublicKey(pk)) + } +} + +impl Writeable for DalekPublicKey { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.0.to_bytes())?; + Ok(()) + } +} + +/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekSignature(Signature); + +impl DalekSignature { + /// Convert hex string to DalekSignature. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let sig = Signature::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekSignature(sig)) + } + + /// Verifies DalekSignature + pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> { + pk.as_ref() + .verify(&msg, &self.0) + .map_err(|_| DalekError::SigVerifyFailed) + } +} + +impl AsRef for DalekSignature { + fn as_ref(&self) -> &Signature { + &self.0 + } +} + +/// Serializes a DalekSignature to and from hex +#[cfg(test)] +pub mod dalek_sig_serde { + use super::DalekSignature; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(sig: &DalekSignature, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&sig.0.to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let str = String::deserialize(deserializer)?; + let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?; + Ok(sig) + } +} + +/// Dalek signature sign wrapper +// TODO: This is likely duplicated throughout crate, check +#[cfg(test)] +pub fn sign(sk: &SecretKey, message: &[u8]) -> Result { + use ed25519_dalek::{Keypair, Signer}; + let secret = + ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?; + let public: PublicKey = (&secret).into(); + let keypair = Keypair { secret, public }; + let sig = keypair.sign(&message); + Ok(DalekSignature(sig)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mwixnet::onion::test_util::rand_keypair; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use rand::Rng; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestPubKeySerde { + #[serde(with = "option_dalek_pubkey_serde", default)] + pk: Option, + } + + #[test] + fn pubkey_test() -> Result<(), Box> { + // Test from_hex + let rand_pk = rand_keypair().1; + let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap(); + assert_eq!(rand_pk.0, pk_from_hex.0); + + // Test ser (de-)serialization + let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap(); + assert_eq!(bytes.len(), 32); + let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap(); + assert_eq!(rand_pk.0, pk_from_deser.0); + + // Test serde with Some(rand_pk) + let some = TestPubKeySerde { + pk: Some(rand_pk.clone()), + }; + let val = serde_json::to_value(some.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("pk").unwrap() { + assert_eq!(s, &rand_pk.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(some, serde_json::from_value(val).unwrap()); + + // Test serde with empty pk field + let none = TestPubKeySerde { pk: None }; + let val = serde_json::to_value(none.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::Null = o.get("pk").unwrap() { + // ok + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(none, serde_json::from_value(val).unwrap()); + + // Test serde with no pk field + let none2 = serde_json::from_str::("{}").unwrap(); + assert_eq!(none, none2); + + Ok(()) + } + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestSigSerde { + #[serde(with = "dalek_sig_serde")] + sig: DalekSignature, + } + + #[test] + fn sig_test() -> Result<(), Box> { + // Sign a message + let (sk, pk) = rand_keypair(); + let msg: [u8; 16] = rand::thread_rng().gen(); + let sig = sign(&sk, &msg).unwrap(); + + // Verify signature + assert!(sig.verify(&pk, &msg).is_ok()); + + // Wrong message + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(sig.verify(&pk, &wrong_msg).is_err()); + + // Wrong pubkey + let wrong_pk = rand_keypair().1; + assert!(sig.verify(&wrong_pk, &msg).is_err()); + + // Test from_hex + let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap(); + assert_eq!(sig.0, sig_from_hex.0); + + // Test serde (de-)serialization + let serde_test = TestSigSerde { sig: sig.clone() }; + let val = serde_json::to_value(serde_test.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("sig").unwrap() { + assert_eq!(s, &sig.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(serde_test, serde_json::from_value(val).unwrap()); + + Ok(()) + } +} diff --git a/libwallet/src/mwixnet/onion/crypto/mod.rs b/libwallet/src/mwixnet/onion/crypto/mod.rs new file mode 100644 index 00000000..58544e9f --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion and comsig modules for mxmixnet + +pub mod comsig; +pub mod dalek; +pub mod secp; + +pub use comsig::{comsig_serde, ComSigError, ComSignature}; diff --git a/libwallet/src/mwixnet/onion/crypto/secp.rs b/libwallet/src/mwixnet/onion/crypto/secp.rs new file mode 100644 index 00000000..eda6abe7 --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/secp.rs @@ -0,0 +1,98 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SECP operations for comsig + +pub use grin_util::secp::{ + self as secp256k1zkp, + constants::SECRET_KEY_SIZE, + key::{SecretKey, ZERO_KEY}, + pedersen::Commitment, + rand::thread_rng, + ContextFlag, Secp256k1, +}; + +use grin_core::ser::{self, Reader}; +use rand::rngs::mock::StepRng; + +/// Generate a random SecretKey. +pub fn random_secret(use_test_rng: bool) -> SecretKey { + let secp = Secp256k1::new(); + if use_test_rng { + // allow for consistent test results + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + SecretKey::new(&secp, &mut test_rng) + } else { + SecretKey::new(&secp, &mut thread_rng()) + } +} + +/// Deserialize a SecretKey from a Reader +pub fn read_secret_key(reader: &mut R) -> Result { + let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; + let secp = Secp256k1::with_caps(ContextFlag::None); + let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; + Ok(pk) +} + +/// Build a Pedersen Commitment using the provided value and blinding factor +#[cfg(test)] +pub fn commit(value: u64, blind: &SecretKey) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let commit = secp.commit(value, blind.clone())?; + Ok(commit) +} + +/// Add a blinding factor to an existing Commitment +pub fn add_excess( + commitment: &Commitment, + excess: &SecretKey, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let excess_commit: Commitment = secp.commit(0, excess.clone())?; + + let commits = vec![commitment.clone(), excess_commit.clone()]; + let sum = secp.commit_sum(commits, Vec::new())?; + Ok(sum) +} + +/// Subtracts a value (v*H) from an existing commitment +pub fn sub_value(commitment: &Commitment, value: u64) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?; + let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; + Ok(sum) +} + +/// Signs the message with the provided SecretKey +#[cfg(test)] +#[allow(dead_code)] +pub fn sign( + sk: &SecretKey, + msg: &grin_util::secp::Message, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Full); + let pubkey = grin_util::secp::PublicKey::from_secret_key(&secp, &sk)?; + let sig = grin_util::secp::aggsig::sign_single( + &secp, + &msg, + &sk, + None, + None, + None, + Some(&pubkey), + None, + )?; + Ok(sig) +} diff --git a/libwallet/src/mwixnet/onion/mod.rs b/libwallet/src/mwixnet/onion/mod.rs new file mode 100644 index 00000000..d052d1c4 --- /dev/null +++ b/libwallet/src/mwixnet/onion/mod.rs @@ -0,0 +1,207 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion module definition + +mod crypto; +pub mod onion; +pub mod util; + +pub use crypto::{ + comsig_serde, dalek::DalekPublicKey as MwixnetPublicKey, ComSigError, ComSignature, +}; + +use chacha20::cipher::StreamCipher; +use grin_core::core::FeeFields; +use grin_util::secp::{ + pedersen::{Commitment, RangeProof}, + SecretKey, +}; +use x25519_dalek::PublicKey as xPublicKey; +use x25519_dalek::{SharedSecret, StaticSecret}; + +use crypto::secp::random_secret; +use onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes}; + +/// Onion hop struct +#[derive(Clone)] +pub struct Hop { + /// Comsig server public key + pub server_pubkey: xPublicKey, + /// Kernel excess + pub excess: SecretKey, + /// Fee + pub fee: FeeFields, + /// Rangeproof + pub rangeproof: Option, +} + +/// Crate a new hop +#[cfg(test)] +pub fn new_hop( + server_key: &SecretKey, + hop_excess: &SecretKey, + fee: u32, + proof: Option, +) -> Hop { + Hop { + server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())), + excess: hop_excess.clone(), + fee: FeeFields::from(fee as u32), + rangeproof: proof, + } +} + +/// Create an Onion for the Commitment, encrypting the payload for each hop +pub fn create_onion( + commitment: &Commitment, + hops: &Vec, + use_test_rng: bool, +) -> Result { + if hops.is_empty() { + return Ok(Onion { + ephemeral_pubkey: xPublicKey::from([0u8; 32]), + commit: commitment.clone(), + enc_payloads: vec![], + }); + } + + let mut shared_secrets: Vec = Vec::new(); + let mut enc_payloads: Vec = Vec::new(); + let mut ephemeral_sk = StaticSecret::from(random_secret(use_test_rng).0); + let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk); + for i in 0..hops.len() { + let hop = &hops[i]; + let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey); + shared_secrets.push(shared_secret); + + ephemeral_sk = StaticSecret::from(random_secret(use_test_rng).0); + let next_ephemeral_pk = if i < (hops.len() - 1) { + xPublicKey::from(&ephemeral_sk) + } else { + xPublicKey::from([0u8; 32]) + }; + + let payload = Payload { + next_ephemeral_pk, + excess: hop.excess.clone(), + fee: hop.fee.clone(), + rangeproof: hop.rangeproof.clone(), + }; + enc_payloads.push(payload.serialize()?); + } + + for i in (0..shared_secrets.len()).rev() { + let mut cipher = new_stream_cipher(&shared_secrets[i])?; + for j in i..shared_secrets.len() { + cipher.apply_keystream(&mut enc_payloads[j]); + } + } + + let onion = Onion { + ephemeral_pubkey: onion_ephemeral_pk, + commit: commitment.clone(), + enc_payloads, + }; + Ok(onion) +} + +/// Internal tests +#[allow(missing_docs, dead_code)] +#[cfg(test)] +pub mod test_util { + use super::*; + use crypto::dalek::DalekPublicKey; + use crypto::secp; + + use grin_core::core::hash::Hash; + use grin_util::secp::Secp256k1; + use grin_util::ToHex; + use rand::{thread_rng, RngCore}; + + pub fn rand_onion() -> Onion { + let commit = rand_commit(); + let mut hops = Vec::new(); + let k = (thread_rng().next_u64() % 5) + 1; + for i in 0..k { + let rangeproof = if i == (k - 1) { + Some(rand_proof()) + } else { + None + }; + let hop = new_hop( + &random_secret(false), + &random_secret(false), + thread_rng().next_u32(), + rangeproof, + ); + hops.push(hop); + } + + create_onion(&commit, &hops, false).unwrap() + } + + pub fn rand_commit() -> Commitment { + secp::commit(rand::thread_rng().next_u64(), &secp::random_secret(false)).unwrap() + } + + pub fn rand_hash() -> Hash { + Hash::from_hex(secp::random_secret(false).to_hex().as_str()).unwrap() + } + + pub fn rand_proof() -> RangeProof { + let secp = Secp256k1::new(); + secp.bullet_proof( + rand::thread_rng().next_u64(), + secp::random_secret(false), + secp::random_secret(false), + secp::random_secret(false), + None, + None, + ) + } + + pub fn proof( + value: u64, + fee: u32, + input_blind: &SecretKey, + hop_excesses: &Vec<&SecretKey>, + ) -> (Commitment, RangeProof) { + let secp = Secp256k1::new(); + + let mut blind = input_blind.clone(); + for hop_excess in hop_excesses { + blind.add_assign(&secp, &hop_excess).unwrap(); + } + + let out_value = value - (fee as u64); + + let rp = secp.bullet_proof( + out_value, + blind.clone(), + secp::random_secret(false), + secp::random_secret(false), + None, + None, + ); + + (secp::commit(out_value, &blind).unwrap(), rp) + } + + pub fn rand_keypair() -> (SecretKey, DalekPublicKey) { + let sk = random_secret(false); + let pk = DalekPublicKey::from_secret(&sk); + (sk, pk) + } +} diff --git a/libwallet/src/mwixnet/onion/onion.rs b/libwallet/src/mwixnet/onion/onion.rs new file mode 100644 index 00000000..2540daec --- /dev/null +++ b/libwallet/src/mwixnet/onion/onion.rs @@ -0,0 +1,438 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion defn for mwixnet + +use super::util::{read_optional, vec_to_array, write_optional}; + +use std::convert::TryFrom; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::result::Result; + +use chacha20::cipher::{NewCipher, StreamCipher}; +use chacha20::{ChaCha20, Key, Nonce}; +use grin_core::core::FeeFields; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use grin_util::secp::{ + self as secp256k1zkp, + key::SecretKey, + pedersen::{Commitment, RangeProof}, +}; +use grin_util::{self, ToHex}; +use hmac::digest::InvalidLength; +use hmac::{Hmac, Mac}; +use serde::ser::SerializeStruct; +use serde::Deserialize; +use sha2::Sha256; +use thiserror::Error; +use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret}; + +use super::crypto::secp; + +type HmacSha256 = Hmac; +/// Raw bytes alias +pub type RawBytes = Vec; + +const CURRENT_ONION_VERSION: u8 = 0; + +/// A data packet with layers of encryption +#[derive(Clone, Debug)] +pub struct Onion { + /// The onion originator's portion of the shared secret + pub ephemeral_pubkey: xPublicKey, + /// The pedersen commitment before adjusting the excess and subtracting the fee + pub commit: Commitment, + /// The encrypted payloads which represent the layers of the onion + pub enc_payloads: Vec, +} + +impl PartialEq for Onion { + fn eq(&self, other: &Onion) -> bool { + *self.ephemeral_pubkey.as_bytes() == *other.ephemeral_pubkey.as_bytes() + && self.commit == other.commit + && self.enc_payloads == other.enc_payloads + } +} + +impl Eq for Onion {} + +impl Hash for Onion { + fn hash(&self, state: &mut H) { + state.write(self.ephemeral_pubkey.as_bytes()); + state.write(self.commit.as_ref()); + state.write_usize(self.enc_payloads.len()); + for p in &self.enc_payloads { + state.write(p.as_slice()); + } + } +} + +/// A single, decrypted/peeled layer of an Onion. +#[derive(Debug, Clone)] +pub struct Payload { + /// PK of next server + pub next_ephemeral_pk: xPublicKey, + /// Excess calculation + pub excess: SecretKey, + /// Fee + pub fee: FeeFields, + /// Rangeproof + pub rangeproof: Option, +} + +impl Payload { + /// Deserialize + pub fn deserialize(bytes: &Vec) -> Result { + let payload: Payload = ser::deserialize_default(&mut &bytes[..])?; + Ok(payload) + } + + /// Serialize + pub fn serialize(&self) -> Result, ser::Error> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } +} + +impl Readable for Payload { + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_ONION_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } + + let next_ephemeral_pk = + xPublicKey::from(vec_to_array::<32>(&reader.read_fixed_bytes(32)?)?); + let excess = secp::read_secret_key(reader)?; + let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; + let rangeproof = read_optional(reader)?; + Ok(Payload { + next_ephemeral_pk, + excess, + fee, + rangeproof, + }) + } +} + +impl Writeable for Payload { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_ONION_VERSION)?; + writer.write_fixed_bytes(&self.next_ephemeral_pk.as_bytes())?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_u64(self.fee.into())?; + write_optional(writer, &self.rangeproof)?; + Ok(()) + } +} + +/// An onion with a layer decrypted +#[derive(Clone, Debug)] +pub struct PeeledOnion { + /// The payload from the peeled layer + pub payload: Payload, + /// The onion remaining after a layer was peeled + pub onion: Onion, +} + +impl Onion { + /// Serialize to binary + pub fn serialize(&self) -> Result, ser::Error> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } + + /// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload + pub fn peel_layer(&self, server_key: &SecretKey) -> Result { + let shared_secret = StaticSecret::from(server_key.0).diffie_hellman(&self.ephemeral_pubkey); + let mut cipher = new_stream_cipher(&shared_secret)?; + + let mut decrypted_bytes = self.enc_payloads[0].clone(); + cipher.apply_keystream(&mut decrypted_bytes); + let decrypted_payload = Payload::deserialize(&decrypted_bytes) + .map_err(|e| OnionError::DeserializationError(e))?; + + let enc_payloads: Vec = self + .enc_payloads + .iter() + .enumerate() + .filter(|&(i, _)| i != 0) + .map(|(_, enc_payload)| { + let mut p = enc_payload.clone(); + cipher.apply_keystream(&mut p); + p + }) + .collect(); + + let mut commitment = self.commit.clone(); + commitment = secp::add_excess(&commitment, &decrypted_payload.excess) + .map_err(|e| OnionError::CalcCommitError(e))?; + commitment = secp::sub_value(&commitment, decrypted_payload.fee.into()) + .map_err(|e| OnionError::CalcCommitError(e))?; + + let peeled_onion = Onion { + ephemeral_pubkey: decrypted_payload.next_ephemeral_pk, + commit: commitment.clone(), + enc_payloads, + }; + Ok(PeeledOnion { + payload: decrypted_payload, + onion: peeled_onion, + }) + } +} + +/// Create a new stream cipher +pub fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { + let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; + mu_hmac.update(shared_secret.as_bytes()); + let mukey = mu_hmac.finalize().into_bytes(); + + let key = Key::from_slice(&mukey[0..32]); + let nonce = Nonce::from_slice(b"NONCE1234567"); + + Ok(ChaCha20::new(&key, &nonce)) +} + +impl Writeable for Onion { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.ephemeral_pubkey.as_bytes())?; + writer.write_fixed_bytes(&self.commit)?; + writer.write_u64(self.enc_payloads.len() as u64)?; + for p in &self.enc_payloads { + writer.write_u64(p.len() as u64)?; + p.write(writer)?; + } + Ok(()) + } +} + +impl Readable for Onion { + fn read(reader: &mut R) -> Result { + let pubkey_bytes: [u8; 32] = vec_to_array(&reader.read_fixed_bytes(32)?)?; + let ephemeral_pubkey = xPublicKey::from(pubkey_bytes); + let commit = Commitment::read(reader)?; + let mut enc_payloads: Vec = Vec::new(); + let len = reader.read_u64()?; + for _ in 0..len { + let size = reader.read_u64()?; + let bytes = reader.read_fixed_bytes(size as usize)?; + enc_payloads.push(bytes); + } + Ok(Onion { + ephemeral_pubkey, + commit, + enc_payloads, + }) + } +} + +impl serde::ser::Serialize for Onion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + let mut state = serializer.serialize_struct("Onion", 3)?; + + state.serialize_field("pubkey", &self.ephemeral_pubkey.as_bytes().to_hex())?; + state.serialize_field("commit", &self.commit.to_hex())?; + + let hex_payloads: Vec = self.enc_payloads.iter().map(|v| v.to_hex()).collect(); + state.serialize_field("data", &hex_payloads)?; + state.end() + } +} + +impl<'de> serde::de::Deserialize<'de> for Onion { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Pubkey, + Commit, + Data, + } + + struct OnionVisitor; + + impl<'de> serde::de::Visitor<'de> for OnionVisitor { + type Value = Onion; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an Onion") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut pubkey = None; + let mut commit = None; + let mut data = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Pubkey => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + pubkey = + Some(xPublicKey::from(vec_to_array::<32>(&vec).map_err( + |_| serde::de::Error::custom("Invalid length pubkey"), + )?)); + } + Field::Commit => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + commit = Some(Commitment::from_vec(vec)); + } + Field::Data => { + let val: Vec = map.next_value()?; + let mut vec: Vec> = Vec::new(); + for hex in val { + vec.push( + grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?, + ); + } + data = Some(vec); + } + } + } + + Ok(Onion { + ephemeral_pubkey: pubkey.unwrap(), + commit: commit.unwrap(), + enc_payloads: data.unwrap(), + }) + } + } + + const FIELDS: &[&str] = &["pubkey", "commit", "data"]; + deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor) + } +} + +/// Error types for creating and peeling Onions +#[derive(Clone, Error, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum OnionError { + /// Invalid Key Length + #[error("Invalid key length for MAC initialization")] + InvalidKeyLength, + /// Serialization Error + #[error("Serialization error occurred: {0:?}")] + SerializationError(ser::Error), + /// Deserialization Error + #[error("Deserialization error occurred: {0:?}")] + DeserializationError(ser::Error), + /// Error calculating blinding factor + #[error("Error calculating blinding factor: {0:?}")] + CalcBlindError(secp256k1zkp::Error), + /// Error calculating ephemeral pubkey + #[error("Error calculating ephemeral pubkey: {0:?}")] + CalcPubKeyError(secp256k1zkp::Error), + /// Error calculating commit + #[error("Error calculating commitment: {0:?}")] + CalcCommitError(secp256k1zkp::Error), +} + +impl From for OnionError { + fn from(_err: InvalidLength) -> OnionError { + OnionError::InvalidKeyLength + } +} + +impl From for OnionError { + fn from(err: ser::Error) -> OnionError { + OnionError::SerializationError(err) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::mwixnet::onion::crypto::secp::random_secret; + use crate::mwixnet::onion::{new_hop, Hop}; + + use grin_core::core::FeeFields; + + /// Test end-to-end Onion creation and unwrapping logic. + #[test] + fn onion() { + let total_fee: u64 = 10; + let fee_per_hop: u32 = 2; + let in_value: u64 = 1000; + let out_value: u64 = in_value - total_fee; + let blind = random_secret(false); + let commitment = secp::commit(in_value, &blind).unwrap(); + + let mut hops: Vec = Vec::new(); + let mut keys: Vec = Vec::new(); + let mut final_commit = secp::commit(out_value, &blind).unwrap(); + let mut final_blind = blind.clone(); + for i in 0..5 { + keys.push(random_secret(false)); + + let excess = random_secret(false); + + let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); + final_blind.add_assign(&secp, &excess).unwrap(); + final_commit = secp::add_excess(&final_commit, &excess).unwrap(); + let proof = if i == 4 { + let n1 = random_secret(false); + let rp = secp.bullet_proof( + out_value, + final_blind.clone(), + n1.clone(), + n1.clone(), + None, + None, + ); + assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok()); + Some(rp) + } else { + None + }; + + let hop = new_hop(&keys[i], &excess, fee_per_hop, proof); + hops.push(hop); + } + + let mut onion_packet = + crate::mwixnet::onion::create_onion(&commitment, &hops, false).unwrap(); + + let mut payload = Payload { + next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(), + excess: random_secret(false), + fee: FeeFields::from(fee_per_hop), + rangeproof: None, + }; + for i in 0..5 { + let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); + payload = peeled.payload; + onion_packet = peeled.onion; + } + + assert!(payload.rangeproof.is_some()); + assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap()); + assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit); + assert_eq!(payload.fee, FeeFields::from(fee_per_hop)); + } +} diff --git a/libwallet/src/mwixnet/onion/util.rs b/libwallet/src/mwixnet/onion/util.rs new file mode 100644 index 00000000..66611774 --- /dev/null +++ b/libwallet/src/mwixnet/onion/util.rs @@ -0,0 +1,185 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Util fns for mwixnet +//! TODO: possibly redundant, check or move elsewhere + +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use std::convert::TryInto; + +/// Writes an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to serialize an optional value into a Writer. If the option +/// contains Some value, it writes '1' followed by the serialized value. If the option +/// is None, it just writes '0'. +/// +/// # Arguments +/// +/// * `writer` - A Writer instance where the data will be written. +/// * `o` - The Optional value that will be written. +/// +/// # Returns +/// +/// * If successful, returns Ok with nothing. +/// * If an error occurs during writing, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::write_optional; +/// let mut writer:Vec = vec![]; +/// let optional_value: Option = Some(10); +/// //write_optional(&mut writer, &optional_value); +/// ``` +pub fn write_optional( + writer: &mut W, + o: &Option, +) -> Result<(), ser::Error> { + match &o { + Some(o) => { + writer.write_u8(1)?; + o.write(writer)?; + } + None => writer.write_u8(0)?, + }; + Ok(()) +} + +/// Reads an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to deserialize an optional value from a Reader. If the first byte +/// read is '0', it returns None. If the first byte is '1', it reads the next value and +/// returns Some(value). +/// +/// # Arguments +/// +/// * `reader` - A Reader instance from where the data will be read. +/// +/// # Returns +/// +/// * If successful, returns Ok wrapping an optional value. If the first byte read was '0', +/// returns None. If it was '1', returns Some(value). +/// * If an error occurs during reading, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::read_optional; +/// use grin_core::ser::{BinReader, ProtocolVersion, DeserializationMode}; +/// let mut buf: &[u8] = &[1, 0, 0, 0, 10]; +/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default()); +/// let optional_value: Option = read_optional(&mut reader).unwrap(); +/// assert_eq!(optional_value, Some(10)); +/// ``` +pub fn read_optional(reader: &mut R) -> Result, ser::Error> { + let o = if reader.read_u8()? == 0 { + None + } else { + Some(O::read(reader)?) + }; + Ok(o) +} + +/// Convert a vector to an array of size `S`. +/// +/// # Arguments +/// +/// * `vec` - The input vector. +/// +/// # Returns +/// +/// * If successful, returns an `Ok` wrapping an array of size `S` containing +/// the first `S` bytes of `vec`. +/// * If `vec` is smaller than `S`, returns an `Err` indicating a count error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::vec_to_array; +/// let v = vec![0, 1, 2, 3, 4, 5]; +/// let a = vec_to_array::<4>(&v).unwrap(); +/// assert_eq!(a, [0, 1, 2, 3]); +/// ``` +pub fn vec_to_array(vec: &Vec) -> Result<[u8; S], ser::Error> { + if vec.len() < S { + return Err(ser::Error::CountError); + } + let arr: [u8; S] = vec[0..S].try_into().unwrap(); + Ok(arr) +} + +#[cfg(test)] +mod tests { + use super::*; + use grin_core::ser::{BinReader, BinWriter, DeserializationMode, ProtocolVersion}; + + #[test] + fn test_write_optional() { + // Test with Some value + let mut buf: Vec = vec![]; + let val: Option = Some(10); + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[1, 0, 0, 0, 10]); // 1 for Some, then 10 as a little-endian u32 + + // Test with None value + buf.clear(); + let val: Option = None; + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[0]); // 0 for None + } + + #[test] + fn test_read_optional() { + // Test with Some value + let mut buf: &[u8] = &[1, 0, 0, 0, 10]; // 1 for Some, then 10 as a little-endian u32 + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, Some(10)); + + // Test with None value + buf = &[0]; // 0 for None + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, None); + } + + #[test] + fn test_vec_to_array_success() { + let v = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let a = vec_to_array::<4>(&v).unwrap(); + assert_eq!(a, [1, 2, 3, 4]); + } + + #[test] + fn test_vec_to_array_too_small() { + let v = vec![1, 2, 3]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } + + #[test] + fn test_vec_to_array_empty() { + let v = vec![]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } +} diff --git a/libwallet/src/mwixnet/types.rs b/libwallet/src/mwixnet/types.rs new file mode 100644 index 00000000..1213f3ae --- /dev/null +++ b/libwallet/src/mwixnet/types.rs @@ -0,0 +1,43 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types related to mwixnet requests required by rest of lib crate apis +//! Should rexport all needed types here + +use super::onion::comsig_serde; +use grin_core::libtx::secp_ser::string_or_u64; +use grin_util::secp::key::SecretKey; +use serde::{Deserialize, Serialize}; + +pub use super::onion::{onion::Onion, ComSignature, Hop}; + +/// A Swap request +#[derive(Serialize, Deserialize, Debug)] +pub struct SwapReq { + /// Com signature + #[serde(with = "comsig_serde")] + pub comsig: ComSignature, + /// Onion + pub onion: Onion, +} + +/// mwixnetRequest Creation Params +#[derive(Serialize, Deserialize, Debug)] +pub struct MixnetReqCreationParams { + /// List of all the server keys + pub server_keys: Vec, + /// Fees per hop + #[serde(with = "string_or_u64")] + pub fee_per_hop: u64, +} diff --git a/libwallet/src/slatepack/armor.rs b/libwallet/src/slatepack/armor.rs index 12a0ced2..eacdf32f 100644 --- a/libwallet/src/slatepack/armor.rs +++ b/libwallet/src/slatepack/armor.rs @@ -188,11 +188,11 @@ fn format_slatepack(slatepack: &str) -> Result { // Returns the first four bytes of a double sha256 hash of some bytes fn generate_check(payload: &[u8]) -> Result, Error> { - let mut first_hash = Sha256::new(); - first_hash.input(payload); - let mut second_hash = Sha256::new(); - second_hash.input(first_hash.result()); - let checksum = second_hash.result(); + let mut first_hasher = Sha256::new(); + first_hasher.update(payload); + let mut second_hasher = Sha256::new(); + second_hasher.update(first_hasher.finalize()); + let checksum = second_hasher.finalize(); let check_bytes: Vec = checksum[0..4].to_vec(); Ok(check_bytes) } diff --git a/libwallet/src/slatepack/types.rs b/libwallet/src/slatepack/types.rs index 90c20031..fe08c933 100644 --- a/libwallet/src/slatepack/types.rs +++ b/libwallet/src/slatepack/types.rs @@ -190,8 +190,8 @@ impl Slatepack { let mut b = [0u8; 32]; b.copy_from_slice(&dec_key.as_bytes()[0..32]); let mut hasher = Sha512::new(); - hasher.input(b); - let result = hasher.result(); + hasher.update(b); + let result = hasher.finalize(); b.copy_from_slice(&result[0..32]); let x_dec_secret = StaticSecret::from(b);