From b8dd2e42f72ff015939e8c3b6e97da04e282a139 Mon Sep 17 00:00:00 2001 From: scilio Date: Thu, 25 Aug 2022 17:16:29 -0400 Subject: [PATCH 1/5] add SwapStore (cherry picked from commit f1f23c4295097e0f806098647dcc2a4d31715355) --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 1 + src/onion.rs | 19 ++++++- src/store.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/store.rs diff --git a/Cargo.lock b/Cargo.lock index 2a7812e..ed8c4a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,6 +2483,7 @@ dependencies = [ "grin_keychain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "grin_secp256k1zkp", "grin_servers", + "grin_store 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "grin_util 5.1.1", "grin_wallet_api", "grin_wallet_impls", diff --git a/Cargo.toml b/Cargo.toml index 1d704e8..e9ef822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ grin_core = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alp grin_chain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } grin_keychain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } grin_servers = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_store = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 681f04d..765adf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod onion; mod rpc; mod secp; mod server; +mod store; mod types; mod wallet; diff --git a/src/onion.rs b/src/onion.rs index 935778f..a81c066 100644 --- a/src/onion.rs +++ b/src/onion.rs @@ -4,7 +4,7 @@ use crate::types::Payload; use crate::onion::OnionError::{InvalidKeyLength, SerializationError}; use chacha20::cipher::{NewCipher, StreamCipher}; use chacha20::{ChaCha20, Key, Nonce}; -use grin_core::ser::{self, ProtocolVersion, Writeable, Writer}; +use grin_core::ser::{self, ProtocolVersion, Readable, Reader, Writeable, Writer}; use grin_util::{self, ToHex}; use hmac::digest::InvalidLength; use hmac::{Hmac, Mac}; @@ -122,6 +122,23 @@ impl Writeable for Onion { } } +impl Readable for Onion { + fn read(reader: &mut R) -> Result { + let ephemeral_pubkey = PublicKey::read(reader)?; + let commit = Commitment::read(reader)?; + let mut enc_payloads: Vec = Vec::new(); + let len = reader.read_u64()?; + for _ in 0..len { + enc_payloads.push(RawBytes::read(reader)?); + } + Ok(Onion { + ephemeral_pubkey, + commit, + enc_payloads, + }) + } +} + impl serde::ser::Serialize for Onion { fn serialize(&self, serializer: S) -> Result where diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..f04971a --- /dev/null +++ b/src/store.rs @@ -0,0 +1,152 @@ +use crate::onion::Onion; +use crate::secp::{self, Commitment, RangeProof, SecretKey}; + +use grin_core::core::Input; +use grin_core::ser::{self, ProtocolVersion, Readable, Reader, Writeable, Writer}; +use grin_store::{self as store, Store}; +use grin_util::ToHex; +use thiserror::Error; + +const DB_NAME: &str = "swap"; +const STORE_SUBPATH: &str = "swaps"; + +const CURRENT_VERSION: u8 = 0; +const SWAP_PREFIX: u8 = b'S'; + +/// Data needed to swap a single output. +#[derive(Clone, Debug, PartialEq)] +pub struct SwapData { + /// The total excess for the output commitment + pub excess: SecretKey, + /// The derived output commitment after applying excess and fee + pub output_commit: Commitment, + /// The rangeproof, included only for the final hop (node N) + pub rangeproof: Option, + /// Transaction input being spent + pub input: Input, + /// Transaction fee + pub fee: u64, + /// The remaining onion after peeling off our layer + pub onion: Onion, + // todo: include a SwapStatus enum value +} + +impl Writeable for SwapData { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_VERSION)?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_fixed_bytes(&self.output_commit)?; + + // todo: duplicated in payload. Can we impl Writeable for Option? + match &self.rangeproof { + Some(proof) => { + writer.write_u8(1)?; + proof.write(writer)?; + } + None => writer.write_u8(0)?, + }; + + self.input.write(writer)?; + writer.write_u64(self.fee.into())?; + self.onion.write(writer)?; + + Ok(()) + } +} + +impl Readable for SwapData { + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } + + let excess = secp::read_secret_key(reader)?; + let output_commit = Commitment::read(reader)?; + let rangeproof = if reader.read_u8()? == 0 { + None + } else { + Some(RangeProof::read(reader)?) + }; + let input = Input::read(reader)?; + let fee = reader.read_u64()?; + let onion = Onion::read(reader)?; + + Ok(SwapData { + excess, + output_commit, + rangeproof, + input, + fee, + onion, + }) + } +} + +/// Storage facility for swap data. +pub struct SwapStore { + db: Store, +} + +/// Store error types +#[derive(Clone, Error, Debug, PartialEq)] +pub enum StoreError { + #[error("Error occurred while attempting to open db: {0}")] + OpenError(store::lmdb::Error), + #[error("Serialization error occurred: {0}")] + SerializationError(ser::Error), + #[error("Error occurred while attempting to read from db: {0}")] + ReadError(store::lmdb::Error), + #[error("Error occurred while attempting to write to db: {0}")] + WriteError(store::lmdb::Error), +} + +impl From for StoreError { + fn from(e: ser::Error) -> StoreError { + StoreError::SerializationError(e) + } +} + +impl SwapStore { + /// Create new chain store + #[allow(dead_code)] + pub fn new(db_root: &str) -> Result { + let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) + .map_err(StoreError::OpenError)?; + Ok(SwapStore { db }) + } + + /// Writes a single key-value pair to the database + fn write>( + &self, + prefix: u8, + k: K, + value: &Vec, + ) -> Result<(), store::lmdb::Error> { + let batch = self.db.batch()?; + batch.put(&store::to_key(prefix, k)[..], &value[..])?; + batch.commit() + } + + /// Reads a single value by key + fn read + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result { + store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k)[..], None), || { + format!("{}:{}", prefix, k.to_hex()) + }) + .map_err(StoreError::ReadError) + } + + /// Saves a swap to the database + #[allow(dead_code)] + pub fn save_swap(&self, s: &SwapData) -> Result<(), StoreError> { + let data = ser::ser_vec(&s, ProtocolVersion::local())?; + self.write(SWAP_PREFIX, &s.output_commit, &data) + .map_err(StoreError::WriteError) + } + + /// Reads a swap from the database + #[allow(dead_code)] + pub fn get_swap(&self, commit: &Commitment) -> Result { + self.read(SWAP_PREFIX, commit) + } +} From b393222ac8eaed33790d9f23f071db652e2d6a70 Mon Sep 17 00:00:00 2001 From: scilio Date: Thu, 25 Aug 2022 17:22:55 -0400 Subject: [PATCH 2/5] serialization helpers for reading and writing Options (cherry picked from commit 7ab6d3086e4c946ef117ed244bd33c87ff5b9142) --- src/store.rs | 19 +++---------------- src/types.rs | 47 +++++++++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/store.rs b/src/store.rs index f04971a..8f7cd2a 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,5 +1,6 @@ use crate::onion::Onion; use crate::secp::{self, Commitment, RangeProof, SecretKey}; +use crate::types::{read_optional, write_optional}; use grin_core::core::Input; use grin_core::ser::{self, ProtocolVersion, Readable, Reader, Writeable, Writer}; @@ -36,16 +37,7 @@ impl Writeable for SwapData { writer.write_u8(CURRENT_VERSION)?; writer.write_fixed_bytes(&self.excess)?; writer.write_fixed_bytes(&self.output_commit)?; - - // todo: duplicated in payload. Can we impl Writeable for Option? - match &self.rangeproof { - Some(proof) => { - writer.write_u8(1)?; - proof.write(writer)?; - } - None => writer.write_u8(0)?, - }; - + write_optional(writer, &self.rangeproof)?; self.input.write(writer)?; writer.write_u64(self.fee.into())?; self.onion.write(writer)?; @@ -63,15 +55,10 @@ impl Readable for SwapData { let excess = secp::read_secret_key(reader)?; let output_commit = Commitment::read(reader)?; - let rangeproof = if reader.read_u8()? == 0 { - None - } else { - Some(RangeProof::read(reader)?) - }; + let rangeproof = read_optional(reader)?; let input = Input::read(reader)?; let fee = reader.read_u64()?; let onion = Onion::read(reader)?; - Ok(SwapData { excess, output_commit, diff --git a/src/types.rs b/src/types.rs index 3a1d8a5..9fe73b4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,31 @@ use serde::{Deserialize, Serialize}; const CURRENT_VERSION: u8 = 0; +/// Writes an optional value as '1' + value if Some, or '0' if None +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 +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) +} + // todo: Belongs in Onion #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Payload { @@ -37,18 +62,12 @@ impl Readable for Payload { let excess = secp::read_secret_key(reader)?; let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; - let rangeproof = if reader.read_u8()? == 0 { - None - } else { - Some(RangeProof::read(reader)?) - }; - - let payload = Payload { + let rangeproof = read_optional(reader)?; + Ok(Payload { excess, fee, rangeproof, - }; - Ok(payload) + }) } } @@ -57,15 +76,7 @@ impl Writeable for Payload { writer.write_u8(CURRENT_VERSION)?; writer.write_fixed_bytes(&self.excess)?; writer.write_u64(self.fee.into())?; - - match &self.rangeproof { - Some(proof) => { - writer.write_u8(1)?; - proof.write(writer)?; - } - None => writer.write_u8(0)?, - }; - + write_optional(writer, &self.rangeproof)?; Ok(()) } } From f2241a611bd92babefc256a98710b451931d4980 Mon Sep 17 00:00:00 2001 From: scilio Date: Sun, 28 Aug 2022 01:39:58 -0400 Subject: [PATCH 3/5] replace submissions HashMap with SwapStore in server.rs --- src/main.rs | 20 ++++++- src/rpc.rs | 4 +- src/server.rs | 148 ++++++++++++++++++++++++++++---------------------- src/store.rs | 57 +++++++++++++++---- 4 files changed, 151 insertions(+), 78 deletions(-) diff --git a/src/main.rs b/src/main.rs index 765adf5..26eb8ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ use config::ServerConfig; use node::HttpGrinNode; +use store::SwapStore; use wallet::HttpWallet; +use crate::store::StoreError; use clap::App; use grin_core::global::ChainTypes; use grin_util::{StopState, ZeroingString}; @@ -143,6 +145,16 @@ fn real_main() -> Result<(), Box> { &server_config.node_api_secret(), ); + // Open SwapStore + let store = SwapStore::new( + config::get_grin_path(&chain_type) // todo: load from config + .join("db") + .to_str() + .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( + "db_root path error".to_string(), + )))?, + )?; + let stop_state = Arc::new(StopState::new()); let stop_state_clone = stop_state.clone(); @@ -153,7 +165,13 @@ fn real_main() -> Result<(), Box> { }); // Start the mwixnet JSON-RPC HTTP server - rpc::listen(server_config, Arc::new(wallet), Arc::new(node), stop_state) + rpc::listen( + server_config, + Arc::new(wallet), + Arc::new(node), + store, + stop_state, + ) } async fn build_signals_fut() { diff --git a/src/rpc.rs b/src/rpc.rs index 2d09bca..58bc426 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -3,6 +3,7 @@ use crate::node::GrinNode; use crate::onion::Onion; use crate::secp::{self, ComSignature}; use crate::server::{Server, ServerImpl, SwapError}; +use crate::store::SwapStore; use crate::wallet::Wallet; use grin_util::StopState; @@ -87,9 +88,10 @@ pub fn listen( server_config: ServerConfig, wallet: Arc, node: Arc, + store: SwapStore, stop_state: Arc, ) -> std::result::Result<(), Box> { - let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone()); + let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone(), store); let server = Arc::new(Mutex::new(server)); let rpc_server = RPCServer { diff --git a/src/server.rs b/src/server.rs index 99dc49d..a082443 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,33 +1,17 @@ use crate::config::ServerConfig; use crate::node::{self, GrinNode}; use crate::onion::{Onion, OnionError}; -use crate::secp::{ComSignature, Commitment, RangeProof, Secp256k1, SecretKey}; +use crate::secp::{ComSignature, Commitment, Secp256k1, SecretKey}; +use crate::store::{StoreError, SwapData, SwapStore}; use crate::wallet::{self, Wallet}; use grin_core::core::{Input, Output, OutputFeatures, TransactionBody}; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use itertools::Itertools; -use std::collections::HashMap; use std::result::Result; use std::sync::{Arc, Mutex}; use thiserror::Error; -#[derive(Clone, Debug, PartialEq)] -struct Submission { - /// The total excess for the output commitment - excess: SecretKey, - /// The derived output commitment after applying excess and fee - output_commit: Commitment, - /// The rangeproof, included only for the final hop (node N) - rangeproof: Option, - /// Transaction input being spent - input: Input, - /// Transaction fee - fee: u64, - /// The remaining onion after peeling off our layer - onion: Onion, -} - /// Swap error types #[derive(Clone, Error, Debug, PartialEq)] pub enum SwapError { @@ -47,6 +31,8 @@ pub enum SwapError { PeelOnionFailure(OnionError), #[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")] FeeTooLow { minimum_fee: u64, actual_fee: u64 }, + #[error("Error saving swap to data store: {0}")] + StoreError(StoreError), #[error("{0}")] UnknownError(String), } @@ -69,7 +55,7 @@ pub struct ServerImpl { server_config: ServerConfig, wallet: Arc, node: Arc, - submissions: Arc>>, + store: Arc>, } impl ServerImpl { @@ -78,12 +64,13 @@ impl ServerImpl { server_config: ServerConfig, wallet: Arc, node: Arc, + store: SwapStore, ) -> Self { ServerImpl { server_config, wallet, node, - submissions: Arc::new(Mutex::new(HashMap::new())), + store: Arc::new(Mutex::new(store)), } } @@ -147,40 +134,37 @@ impl Server for ServerImpl { return Err(SwapError::MissingRangeproof); } - let mut locked = self.submissions.lock().unwrap(); - if locked.contains_key(&onion.commit) { - return Err(SwapError::AlreadySwapped { - commit: onion.commit.clone(), - }); - } + let locked = self.store.lock().unwrap(); - locked.insert( - onion.commit, - Submission { + locked + .save_swap(&SwapData { excess: peeled.0.excess, output_commit: peeled.1.commit, rangeproof: peeled.0.rangeproof, input, fee, onion: peeled.1, - }, - ); + }) + .map_err(|e| match e { + StoreError::AlreadyExists(_) => SwapError::AlreadySwapped { + commit: onion.commit.clone(), + }, + _ => SwapError::StoreError(e), + })?; Ok(()) } fn execute_round(&self) -> Result<(), Box> { - let mut locked_state = self.submissions.lock().unwrap(); + let locked_store = self.store.lock().unwrap(); let next_block_height = self.node.get_chain_height()? + 1; - let spendable: Vec = locked_state - .values() - .into_iter() + let spendable: Vec = locked_store + .swaps_iter()? .unique_by(|s| s.output_commit) .filter(|s| { node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false) }) .filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true)) - .cloned() .collect(); if spendable.len() == 0 { @@ -219,7 +203,7 @@ impl Server for ServerImpl { )?; self.node.post_tx(&tx)?; - locked_state.clear(); + // todo: Update swap statuses Ok(()) } @@ -273,7 +257,8 @@ mod tests { use crate::secp::{ self, ComSignature, Commitment, PublicKey, RangeProof, Secp256k1, SecretKey, }; - use crate::server::{Server, ServerImpl, Submission, SwapError}; + use crate::server::{Server, ServerImpl, SwapError}; + use crate::store::{SwapData, SwapStore}; use crate::types::Payload; use crate::wallet::mock::MockWallet; @@ -294,9 +279,14 @@ mod tests { } fn new_server( + test_name: &str, server_key: &SecretKey, utxos: &Vec<&Commitment>, ) -> (ServerImpl, Arc) { + global::set_local_chain_type(ChainTypes::AutomatedTesting); + let db_root = format!("./target/tmp/.{}", test_name); + let _ = std::fs::remove_dir_all(db_root.as_str()); + let config = ServerConfig { key: server_key.clone(), interval_s: 1, @@ -315,8 +305,9 @@ mod tests { mut_node.add_default_utxo(&utxo); } let node = Arc::new(mut_node); + let store = SwapStore::new(db_root.as_str()).unwrap(); - let server = ServerImpl::new(config, wallet.clone(), node.clone()); + let server = ServerImpl::new(config, wallet.clone(), node.clone(), store); (server, node) } @@ -370,14 +361,14 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, node) = new_server(&server_key, &vec![&input_commit]); + let (server, node) = new_server("swap_lifecycle", &server_key, &vec![&input_commit]); server.swap(&onion, &comsig)?; // Make sure entry is added to server. let output_commit = secp::add_excess(&input_commit, &hop_excess)?; let output_commit = secp::sub_value(&output_commit, fee)?; - let expected = Submission { + let expected = SwapData { excess: hop_excess.clone(), output_commit: output_commit.clone(), rangeproof: Some(proof), @@ -391,16 +382,19 @@ mod tests { }; { - let submissions = server.submissions.lock().unwrap(); - assert_eq!(1, submissions.len()); - assert!(submissions.contains_key(&input_commit)); - assert_eq!(&expected, submissions.get(&input_commit).unwrap()); + let store = server.store.lock().unwrap(); + assert_eq!(1, store.swaps_iter().unwrap().count()); + assert!(store.swap_exists(&input_commit).unwrap()); + assert_eq!(expected, store.get_swap(&input_commit).unwrap()); } server.execute_round()?; - // Make sure entry is removed from server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // todo: Make sure entry is removed from server.submissions + // assert_eq!( + // 0, + // server.store.lock().unwrap().swaps_iter().unwrap().count() + // ); // check that the transaction was posted let posted_txns = node.get_posted_txns(); @@ -410,7 +404,6 @@ mod tests { assert!(posted_txn.outputs_committed().contains(&output_commit)); // todo: check that outputs also contain the commitment generated by our wallet - global::set_local_chain_type(ChainTypes::AutomatedTesting); posted_txn.validate(Weighting::AsTransaction)?; Ok(()) @@ -433,7 +426,8 @@ mod tests { let onion = test_util::create_onion(&input_commit, &hops)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = + new_server("swap_too_many_payloads", &server_key, &vec![&input_commit]); let result = server.swap(&onion, &comsig); assert_eq!( Err(SwapError::InvalidPayloadLength { @@ -443,8 +437,11 @@ mod tests { result ); - // Make sure no entry is added to server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // Make sure no entry is added to the store + assert_eq!( + 0, + server.store.lock().unwrap().swaps_iter().unwrap().count() + ); Ok(()) } @@ -467,12 +464,19 @@ mod tests { let wrong_blind = secp::random_secret(); let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = new_server( + "swap_invalid_com_signature", + &server_key, + &vec![&input_commit], + ); let result = server.swap(&onion, &comsig); assert_eq!(Err(SwapError::InvalidComSignature), result); - // Make sure no entry is added to server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // Make sure no entry is added to the store + assert_eq!( + 0, + server.store.lock().unwrap().swaps_iter().unwrap().count() + ); Ok(()) } @@ -494,12 +498,16 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = + new_server("swap_invalid_rangeproof", &server_key, &vec![&input_commit]); let result = server.swap(&onion, &comsig); assert_eq!(Err(SwapError::InvalidRangeproof), result); - // Make sure no entry is added to server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // Make sure no entry is added to the store + assert_eq!( + 0, + server.store.lock().unwrap().swaps_iter().unwrap().count() + ); Ok(()) } @@ -519,12 +527,16 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = + new_server("swap_missing_rangeproof", &server_key, &vec![&input_commit]); let result = server.swap(&onion, &comsig); assert_eq!(Err(SwapError::MissingRangeproof), result); - // Make sure no entry is added to server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // Make sure no entry is added to the store + assert_eq!( + 0, + server.store.lock().unwrap().swaps_iter().unwrap().count() + ); Ok(()) } @@ -545,7 +557,7 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![]); + let (server, _node) = new_server("swap_utxo_missing", &server_key, &vec![]); let result = server.swap(&onion, &comsig); assert_eq!( Err(SwapError::CoinNotFound { @@ -554,8 +566,11 @@ mod tests { result ); - // Make sure no entry is added to server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + // Make sure no entry is added to the store + assert_eq!( + 0, + server.store.lock().unwrap().swaps_iter().unwrap().count() + ); Ok(()) } @@ -576,7 +591,7 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = new_server("swap_already_swapped", &server_key, &vec![&input_commit]); server.swap(&onion, &comsig)?; // Call swap a second time @@ -609,7 +624,8 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = + new_server("swap_peel_onion_failure", &server_key, &vec![&input_commit]); let result = server.swap(&onion, &comsig); assert!(result.is_err()); @@ -634,7 +650,7 @@ mod tests { let onion = test_util::create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - let (server, _node) = new_server(&server_key, &vec![&input_commit]); + let (server, _node) = new_server("swap_fee_too_low", &server_key, &vec![&input_commit]); let result = server.swap(&onion, &comsig); assert_eq!( Err(SwapError::FeeTooLow { diff --git a/src/store.rs b/src/store.rs index 8f7cd2a..2c8b4c5 100644 --- a/src/store.rs +++ b/src/store.rs @@ -3,7 +3,9 @@ use crate::secp::{self, Commitment, RangeProof, SecretKey}; use crate::types::{read_optional, write_optional}; use grin_core::core::Input; -use grin_core::ser::{self, ProtocolVersion, Readable, Reader, Writeable, Writer}; +use grin_core::ser::{ + self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer, +}; use grin_store::{self as store, Store}; use grin_util::ToHex; use thiserror::Error; @@ -78,6 +80,8 @@ pub struct SwapStore { /// Store error types #[derive(Clone, Error, Debug, PartialEq)] pub enum StoreError { + #[error("Swap entry already exists for '{0:?}'")] + AlreadyExists(Commitment), #[error("Error occurred while attempting to open db: {0}")] OpenError(store::lmdb::Error), #[error("Serialization error occurred: {0}")] @@ -96,7 +100,6 @@ impl From for StoreError { impl SwapStore { /// Create new chain store - #[allow(dead_code)] pub fn new(db_root: &str) -> Result { let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) .map_err(StoreError::OpenError)?; @@ -109,10 +112,16 @@ impl SwapStore { prefix: u8, k: K, value: &Vec, - ) -> Result<(), store::lmdb::Error> { + ) -> Result { let batch = self.db.batch()?; - batch.put(&store::to_key(prefix, k)[..], &value[..])?; - batch.commit() + let key = store::to_key(prefix, k); + if batch.exists(&key[..])? { + Ok(false) + } else { + batch.put(&key[..], &value[..])?; + batch.commit()?; + Ok(true) + } } /// Reads a single value by key @@ -124,16 +133,44 @@ impl SwapStore { } /// Saves a swap to the database - #[allow(dead_code)] pub fn save_swap(&self, s: &SwapData) -> Result<(), StoreError> { let data = ser::ser_vec(&s, ProtocolVersion::local())?; - self.write(SWAP_PREFIX, &s.output_commit, &data) - .map_err(StoreError::WriteError) + let saved = self + .write(SWAP_PREFIX, &s.input.commit, &data) + .map_err(StoreError::WriteError)?; + if !saved { + Err(StoreError::AlreadyExists(s.input.commit.clone())) + } else { + Ok(()) + } + } + + /// Iterator over all swaps. + pub fn swaps_iter(&self) -> Result, StoreError> { + let key = store::to_key(SWAP_PREFIX, ""); + let protocol_version = self.db.protocol_version(); + self.db + .iter(&key[..], move |_, mut v| { + ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) + .map_err(From::from) + }) + .map_err(|e| StoreError::ReadError(e)) + } + + /// Checks if a matching swap exists in the database + #[allow(dead_code)] + pub fn swap_exists(&self, input_commit: &Commitment) -> Result { + let key = store::to_key(SWAP_PREFIX, input_commit); + self.db + .batch() + .map_err(StoreError::ReadError)? + .exists(&key[..]) + .map_err(StoreError::ReadError) } /// Reads a swap from the database #[allow(dead_code)] - pub fn get_swap(&self, commit: &Commitment) -> Result { - self.read(SWAP_PREFIX, commit) + pub fn get_swap(&self, input_commit: &Commitment) -> Result { + self.read(SWAP_PREFIX, input_commit) } } From d00362b02e9442c2c3301a96d431c0976e726dc4 Mon Sep 17 00:00:00 2001 From: scilio Date: Wed, 14 Sep 2022 12:55:27 -0400 Subject: [PATCH 4/5] add SwapStatus to keep track of progress of each swap --- src/onion.rs | 33 +++++++++- src/secp.rs | 33 ++++++++++ src/server.rs | 71 ++++++++++++++------- src/store.rs | 173 ++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 279 insertions(+), 31 deletions(-) diff --git a/src/onion.rs b/src/onion.rs index a81c066..80309da 100644 --- a/src/onion.rs +++ b/src/onion.rs @@ -129,7 +129,9 @@ impl Readable for Onion { let mut enc_payloads: Vec = Vec::new(); let len = reader.read_u64()?; for _ in 0..len { - enc_payloads.push(RawBytes::read(reader)?); + let size = reader.read_u64()?; + let bytes = reader.read_fixed_bytes(size as usize)?; + enc_payloads.push(bytes); } Ok(Onion { ephemeral_pubkey, @@ -265,11 +267,13 @@ impl From for OnionError { #[cfg(test)] pub mod test_util { use super::{Onion, OnionError, RawBytes}; - use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; + use crate::secp::test_util::{rand_commit, rand_proof, rand_pubkey}; + use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; use crate::types::Payload; - use crate::secp; use chacha20::cipher::StreamCipher; + use grin_core::core::FeeFields; + use rand::RngCore; #[derive(Clone)] pub struct Hop { @@ -315,6 +319,29 @@ pub mod test_util { Ok(onion) } + pub fn rand_onion() -> Onion { + let commit = rand_commit(); + let mut hops = Vec::new(); + let k = (rand::thread_rng().next_u64() % 5) + 1; + for i in 0..k { + let hop = Hop { + pubkey: rand_pubkey(), + payload: Payload { + excess: secp::random_secret(), + fee: FeeFields::from(rand::thread_rng().next_u32()), + rangeproof: if i == (k - 1) { + Some(rand_proof()) + } else { + None + }, + }, + }; + hops.push(hop); + } + + create_onion(&commit, &hops).unwrap() + } + /// Calculates the expected next ephemeral pubkey after peeling a layer off of the Onion. pub fn next_ephemeral_pubkey( onion: &Onion, diff --git a/src/secp.rs b/src/secp.rs index cff866c..815654d 100644 --- a/src/secp.rs +++ b/src/secp.rs @@ -216,6 +216,39 @@ pub fn sign(sk: &SecretKey, msg: &Message) -> Result Commitment { + secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap() + } + + pub fn rand_hash() -> Hash { + Hash::from_hex(secp::random_secret().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(), + secp::random_secret(), + secp::random_secret(), + None, + None, + ) + } + + pub fn rand_pubkey() -> PublicKey { + let secp = Secp256k1::new(); + PublicKey::from_secret_key(&secp, &secp::random_secret()).unwrap() + } +} + #[cfg(test)] mod tests { use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; diff --git a/src/server.rs b/src/server.rs index a082443..e12e2ec 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,10 +2,11 @@ use crate::config::ServerConfig; use crate::node::{self, GrinNode}; use crate::onion::{Onion, OnionError}; use crate::secp::{ComSignature, Commitment, Secp256k1, SecretKey}; -use crate::store::{StoreError, SwapData, SwapStore}; +use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; use crate::wallet::{self, Wallet}; -use grin_core::core::{Input, Output, OutputFeatures, TransactionBody}; +use grin_core::core::hash::Hashed; +use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody}; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use itertools::Itertools; use std::result::Result; @@ -46,7 +47,7 @@ pub trait Server: Send + Sync { /// and assemble the coinswap transaction, posting the transaction to the configured node. /// /// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes. - fn execute_round(&self) -> Result<(), Box>; + fn execute_round(&self) -> Result, Box>; } /// The standard MWixnet server implementation @@ -137,14 +138,18 @@ impl Server for ServerImpl { let locked = self.store.lock().unwrap(); locked - .save_swap(&SwapData { - excess: peeled.0.excess, - output_commit: peeled.1.commit, - rangeproof: peeled.0.rangeproof, - input, - fee, - onion: peeled.1, - }) + .save_swap( + &SwapData { + excess: peeled.0.excess, + output_commit: peeled.1.commit, + rangeproof: peeled.0.rangeproof, + input, + fee, + onion: peeled.1, + status: SwapStatus::Unprocessed, + }, + false, + ) .map_err(|e| match e { StoreError::AlreadyExists(_) => SwapError::AlreadySwapped { commit: onion.commit.clone(), @@ -154,13 +159,17 @@ impl Server for ServerImpl { Ok(()) } - fn execute_round(&self) -> Result<(), Box> { + fn execute_round(&self) -> Result, Box> { let locked_store = self.store.lock().unwrap(); let next_block_height = self.node.get_chain_height()? + 1; let spendable: Vec = locked_store .swaps_iter()? .unique_by(|s| s.output_commit) + .filter(|s| match s.status { + SwapStatus::Unprocessed => true, + _ => false, + }) .filter(|s| { node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false) }) @@ -168,7 +177,7 @@ impl Server for ServerImpl { .collect(); if spendable.len() == 0 { - return Ok(()); + return Ok(None); } let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum(); @@ -203,9 +212,15 @@ impl Server for ServerImpl { )?; self.node.post_tx(&tx)?; - // todo: Update swap statuses - Ok(()) + // Update status to in process + let kernel_hash = tx.kernels().first().unwrap().hash(); + for mut swap in spendable { + swap.status = SwapStatus::InProcess { kernel_hash }; + locked_store.save_swap(&swap, true)?; + } + + Ok(Some(tx)) } } @@ -215,6 +230,7 @@ pub mod mock { use crate::onion::Onion; use crate::secp::ComSignature; + use grin_core::core::Transaction; use std::collections::HashMap; pub struct MockServer { @@ -242,8 +258,8 @@ pub mod mock { Ok(()) } - fn execute_round(&self) -> Result<(), Box> { - Ok(()) + fn execute_round(&self) -> Result, Box> { + Ok(None) } } } @@ -258,10 +274,11 @@ mod tests { self, ComSignature, Commitment, PublicKey, RangeProof, Secp256k1, SecretKey, }; use crate::server::{Server, ServerImpl, SwapError}; - use crate::store::{SwapData, SwapStore}; + use crate::store::{SwapData, SwapStatus, SwapStore}; use crate::types::Payload; use crate::wallet::mock::MockWallet; + use grin_core::core::hash::Hashed; use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction, Weighting}; use grin_core::global::{self, ChainTypes}; use std::net::TcpListener; @@ -379,6 +396,7 @@ mod tests { commit: output_commit.clone(), enc_payloads: vec![], }, + status: SwapStatus::Unprocessed, }; { @@ -388,13 +406,18 @@ mod tests { assert_eq!(expected, store.get_swap(&input_commit).unwrap()); } - server.execute_round()?; + let tx = server.execute_round()?; + assert!(tx.is_some()); - // todo: Make sure entry is removed from server.submissions - // assert_eq!( - // 0, - // server.store.lock().unwrap().swaps_iter().unwrap().count() - // ); + { + // check that status was updated + let store = server.store.lock().unwrap(); + assert!(match store.get_swap(&input_commit)?.status { + SwapStatus::InProcess { kernel_hash } => + kernel_hash == tx.unwrap().kernels().first().unwrap().hash(), + _ => false, + }); + } // check that the transaction was posted let posted_txns = node.get_posted_txns(); diff --git a/src/store.rs b/src/store.rs index 2c8b4c5..76b4c7a 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,6 +1,7 @@ use crate::onion::Onion; use crate::secp::{self, Commitment, RangeProof, SecretKey}; use crate::types::{read_optional, write_optional}; +use grin_core::core::hash::Hash; use grin_core::core::Input; use grin_core::ser::{ @@ -16,6 +17,62 @@ const STORE_SUBPATH: &str = "swaps"; const CURRENT_VERSION: u8 = 0; const SWAP_PREFIX: u8 = b'S'; +/// Swap statuses +#[derive(Clone, Debug, PartialEq)] +pub enum SwapStatus { + Unprocessed, + InProcess { kernel_hash: Hash }, + Completed { kernel_hash: Hash, block_hash: Hash }, +} + +impl Writeable for SwapStatus { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + match self { + SwapStatus::Unprocessed => { + writer.write_u8(0)?; + } + SwapStatus::InProcess { kernel_hash } => { + writer.write_u8(1)?; + kernel_hash.write(writer)?; + } + SwapStatus::Completed { + kernel_hash, + block_hash, + } => { + writer.write_u8(2)?; + kernel_hash.write(writer)?; + block_hash.write(writer)?; + } + }; + + Ok(()) + } +} + +impl Readable for SwapStatus { + fn read(reader: &mut R) -> Result { + let status = match reader.read_u8()? { + 0 => SwapStatus::Unprocessed, + 1 => { + let kernel_hash = Hash::read(reader)?; + SwapStatus::InProcess { kernel_hash } + } + 2 => { + let kernel_hash = Hash::read(reader)?; + let block_hash = Hash::read(reader)?; + SwapStatus::Completed { + kernel_hash, + block_hash, + } + } + _ => { + return Err(ser::Error::CorruptedData); + } + }; + Ok(status) + } +} + /// Data needed to swap a single output. #[derive(Clone, Debug, PartialEq)] pub struct SwapData { @@ -31,7 +88,8 @@ pub struct SwapData { pub fee: u64, /// The remaining onion after peeling off our layer pub onion: Onion, - // todo: include a SwapStatus enum value + /// The status of the swap + pub status: SwapStatus, } impl Writeable for SwapData { @@ -43,6 +101,7 @@ impl Writeable for SwapData { self.input.write(writer)?; writer.write_u64(self.fee.into())?; self.onion.write(writer)?; + self.status.write(writer)?; Ok(()) } @@ -61,6 +120,7 @@ impl Readable for SwapData { let input = Input::read(reader)?; let fee = reader.read_u64()?; let onion = Onion::read(reader)?; + let status = SwapStatus::read(reader)?; Ok(SwapData { excess, output_commit, @@ -68,6 +128,7 @@ impl Readable for SwapData { input, fee, onion, + status, }) } } @@ -112,10 +173,11 @@ impl SwapStore { prefix: u8, k: K, value: &Vec, + overwrite: bool, ) -> Result { let batch = self.db.batch()?; let key = store::to_key(prefix, k); - if batch.exists(&key[..])? { + if !overwrite && batch.exists(&key[..])? { Ok(false) } else { batch.put(&key[..], &value[..])?; @@ -133,10 +195,10 @@ impl SwapStore { } /// Saves a swap to the database - pub fn save_swap(&self, s: &SwapData) -> Result<(), StoreError> { + pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> { let data = ser::ser_vec(&s, ProtocolVersion::local())?; let saved = self - .write(SWAP_PREFIX, &s.input.commit, &data) + .write(SWAP_PREFIX, &s.input.commit, &data, overwrite) .map_err(StoreError::WriteError)?; if !saved { Err(StoreError::AlreadyExists(s.input.commit.clone())) @@ -174,3 +236,106 @@ impl SwapStore { self.read(SWAP_PREFIX, input_commit) } } + +#[cfg(test)] +mod tests { + use crate::onion::test_util::rand_onion; + use crate::secp::test_util::{rand_commit, rand_hash, rand_proof}; + use crate::store::{SwapData, SwapStatus, SwapStore}; + use crate::{secp, StoreError}; + use grin_core::core::{Input, OutputFeatures}; + use grin_core::global::{self, ChainTypes}; + use rand::RngCore; + use std::cmp::Ordering; + + fn new_store(test_name: &str) -> SwapStore { + global::set_local_chain_type(ChainTypes::AutomatedTesting); + let db_root = format!("./target/tmp/.{}", test_name); + let _ = std::fs::remove_dir_all(db_root.as_str()); + SwapStore::new(db_root.as_str()).unwrap() + } + + fn rand_swap_with_status(status: SwapStatus) -> SwapData { + SwapData { + excess: secp::random_secret(), + output_commit: rand_commit(), + rangeproof: Some(rand_proof()), + input: Input::new(OutputFeatures::Plain, rand_commit()), + fee: rand::thread_rng().next_u64(), + onion: rand_onion(), + status, + } + } + + fn rand_swap() -> SwapData { + let s = rand::thread_rng().next_u64() % 3; + let status = if s == 0 { + SwapStatus::Unprocessed + } else if s == 1 { + SwapStatus::InProcess { + kernel_hash: rand_hash(), + } + } else { + SwapStatus::Completed { + kernel_hash: rand_hash(), + block_hash: rand_hash(), + } + }; + rand_swap_with_status(status) + } + + #[test] + fn swap_iter() -> Result<(), Box> { + let store = new_store("swap_iter"); + let mut swaps: Vec = Vec::new(); + for _ in 0..5 { + let swap = rand_swap(); + store.save_swap(&swap, false)?; + swaps.push(swap); + } + + swaps.sort_by(|a, b| { + if a.input.commit < b.input.commit { + Ordering::Less + } else if a.input.commit == b.input.commit { + Ordering::Equal + } else { + Ordering::Greater + } + }); + + let mut i: usize = 0; + for swap in store.swaps_iter()? { + assert_eq!(swap, *swaps.get(i).unwrap()); + i += 1; + } + + Ok(()) + } + + #[test] + fn save_swap() -> Result<(), Box> { + let store = new_store("save_swap"); + + let mut swap = rand_swap_with_status(SwapStatus::Unprocessed); + assert!(!store.swap_exists(&swap.input.commit)?); + + store.save_swap(&swap, false)?; + assert_eq!(swap, store.get_swap(&swap.input.commit)?); + assert!(store.swap_exists(&swap.input.commit)?); + + swap.status = SwapStatus::InProcess { + kernel_hash: rand_hash(), + }; + let result = store.save_swap(&swap, false); + assert_eq!( + Err(StoreError::AlreadyExists(swap.input.commit.clone())), + result + ); + + store.save_swap(&swap, true)?; + assert_eq!(swap, store.get_swap(&swap.input.commit)?); + + Ok(()) + } +} From 2cd450cfa91d0a0155ce38e4983ec19f5622f341 Mon Sep 17 00:00:00 2001 From: scilio Date: Wed, 14 Sep 2022 13:20:45 -0400 Subject: [PATCH 5/5] call set_local_chain_type --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 26eb8ad..27810c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use wallet::HttpWallet; use crate::store::StoreError; use clap::App; +use grin_core::global; use grin_core::global::ChainTypes; use grin_util::{StopState, ZeroingString}; use rpassword; @@ -40,6 +41,7 @@ fn real_main() -> Result<(), Box> { } else { ChainTypes::Mainnet }; + global::set_local_chain_type(chain_type); let config_path = match args.value_of("config_file") { Some(path) => PathBuf::from(path),