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..27810c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ use config::ServerConfig; use node::HttpGrinNode; +use store::SwapStore; 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; @@ -19,6 +22,7 @@ mod onion; mod rpc; mod secp; mod server; +mod store; mod types; mod wallet; @@ -37,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), @@ -142,6 +147,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(); @@ -152,7 +167,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/onion.rs b/src/onion.rs index 935778f..80309da 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,25 @@ 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 { + 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 @@ -248,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 { @@ -298,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/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/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 99dc49d..e12e2ec 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,33 +1,18 @@ 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, 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::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 +32,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), } @@ -60,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 @@ -69,7 +56,7 @@ pub struct ServerImpl { server_config: ServerConfig, wallet: Arc, node: Arc, - submissions: Arc>>, + store: Arc>, } impl ServerImpl { @@ -78,12 +65,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,44 +135,49 @@ 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 { - excess: peeled.0.excess, - output_commit: peeled.1.commit, - rangeproof: peeled.0.rangeproof, - input, - fee, - onion: peeled.1, - }, - ); + locked + .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(), + }, + _ => SwapError::StoreError(e), + })?; Ok(()) } - fn execute_round(&self) -> Result<(), Box> { - let mut locked_state = self.submissions.lock().unwrap(); + 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_state - .values() - .into_iter() + 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) }) .filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true)) - .cloned() .collect(); if spendable.len() == 0 { - return Ok(()); + return Ok(None); } let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum(); @@ -219,9 +212,15 @@ impl Server for ServerImpl { )?; self.node.post_tx(&tx)?; - locked_state.clear(); - 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)) } } @@ -231,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 { @@ -258,8 +258,8 @@ pub mod mock { Ok(()) } - fn execute_round(&self) -> Result<(), Box> { - Ok(()) + fn execute_round(&self) -> Result, Box> { + Ok(None) } } } @@ -273,10 +273,12 @@ 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, 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; @@ -294,9 +296,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 +322,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 +378,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), @@ -388,19 +396,28 @@ mod tests { commit: output_commit.clone(), enc_payloads: vec![], }, + status: SwapStatus::Unprocessed, }; { - 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()?; + let tx = server.execute_round()?; + assert!(tx.is_some()); - // Make sure entry is removed from server.submissions - assert!(server.submissions.lock().unwrap().is_empty()); + { + // 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(); @@ -410,7 +427,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 +449,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 +460,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 +487,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 +521,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 +550,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 +580,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 +589,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 +614,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 +647,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 +673,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 new file mode 100644 index 0000000..76b4c7a --- /dev/null +++ b/src/store.rs @@ -0,0 +1,341 @@ +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::{ + self, DeserializationMode, 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'; + +/// 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 { + /// 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, + /// The status of the swap + pub status: SwapStatus, +} + +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)?; + write_optional(writer, &self.rangeproof)?; + self.input.write(writer)?; + writer.write_u64(self.fee.into())?; + self.onion.write(writer)?; + self.status.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 = read_optional(reader)?; + 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, + rangeproof, + input, + fee, + onion, + status, + }) + } +} + +/// Storage facility for swap data. +pub struct SwapStore { + db: Store, +} + +/// 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}")] + 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 + 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, + overwrite: bool, + ) -> Result { + let batch = self.db.batch()?; + let key = store::to_key(prefix, k); + if !overwrite && batch.exists(&key[..])? { + Ok(false) + } else { + batch.put(&key[..], &value[..])?; + batch.commit()?; + Ok(true) + } + } + + /// 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 + 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, overwrite) + .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, input_commit: &Commitment) -> Result { + 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(()) + } +} 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(()) } }