mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-01-20 19:11:09 +03:00
Merge pull request #16 from scilio/main
Use lmdb store for swap requests
This commit is contained in:
commit
dae4f87eaf
9 changed files with 596 additions and 103 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
23
src/main.rs
23
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<dyn std::error::Error>> {
|
|||
} 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<dyn std::error::Error>> {
|
|||
&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<dyn std::error::Error>> {
|
|||
});
|
||||
|
||||
// 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() {
|
||||
|
|
50
src/onion.rs
50
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<R: Reader>(reader: &mut R) -> Result<Onion, ser::Error> {
|
||||
let ephemeral_pubkey = PublicKey::read(reader)?;
|
||||
let commit = Commitment::read(reader)?;
|
||||
let mut enc_payloads: Vec<RawBytes> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -248,11 +267,13 @@ impl From<ser::Error> 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,
|
||||
|
|
|
@ -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<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
store: SwapStore,
|
||||
stop_state: Arc<StopState>,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone());
|
||||
let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone(), store);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
let rpc_server = RPCServer {
|
||||
|
|
33
src/secp.rs
33
src/secp.rs
|
@ -216,6 +216,39 @@ pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature, secp256k1zkp::Er
|
|||
Ok(sig)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_util {
|
||||
use crate::secp::{self, Commitment, PublicKey, RangeProof, Secp256k1};
|
||||
use grin_core::core::hash::Hash;
|
||||
use grin_util::ToHex;
|
||||
use rand::RngCore;
|
||||
|
||||
pub fn rand_commit() -> 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};
|
||||
|
|
199
src/server.rs
199
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<RangeProof>,
|
||||
/// 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<dyn std::error::Error>>;
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
/// The standard MWixnet server implementation
|
||||
|
@ -69,7 +56,7 @@ pub struct ServerImpl {
|
|||
server_config: ServerConfig,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
submissions: Arc<Mutex<HashMap<Commitment, Submission>>>,
|
||||
store: Arc<Mutex<SwapStore>>,
|
||||
}
|
||||
|
||||
impl ServerImpl {
|
||||
|
@ -78,12 +65,13 @@ impl ServerImpl {
|
|||
server_config: ServerConfig,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
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<dyn std::error::Error>> {
|
||||
let mut locked_state = self.submissions.lock().unwrap();
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
||||
let locked_store = self.store.lock().unwrap();
|
||||
let next_block_height = self.node.get_chain_height()? + 1;
|
||||
|
||||
let spendable: Vec<Submission> = locked_state
|
||||
.values()
|
||||
.into_iter()
|
||||
let spendable: Vec<SwapData> = 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<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
||||
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<MockGrinNode>) {
|
||||
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 {
|
||||
|
|
341
src/store.rs
Normal file
341
src/store.rs
Normal file
|
@ -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<W: Writer>(&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<R: Reader>(reader: &mut R) -> Result<SwapStatus, ser::Error> {
|
||||
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<RangeProof>,
|
||||
/// 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<W: Writer>(&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<R: Reader>(reader: &mut R) -> Result<SwapData, ser::Error> {
|
||||
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<ser::Error> for StoreError {
|
||||
fn from(e: ser::Error) -> StoreError {
|
||||
StoreError::SerializationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl SwapStore {
|
||||
/// Create new chain store
|
||||
pub fn new(db_root: &str) -> Result<SwapStore, StoreError> {
|
||||
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<K: AsRef<[u8]>>(
|
||||
&self,
|
||||
prefix: u8,
|
||||
k: K,
|
||||
value: &Vec<u8>,
|
||||
overwrite: bool,
|
||||
) -> Result<bool, store::lmdb::Error> {
|
||||
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<K: AsRef<[u8]> + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result<V, StoreError> {
|
||||
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<impl Iterator<Item = SwapData>, 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<bool, StoreError> {
|
||||
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<SwapData, StoreError> {
|
||||
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<dyn std::error::Error>> {
|
||||
let store = new_store("swap_iter");
|
||||
let mut swaps: Vec<SwapData> = 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<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
||||
}
|
47
src/types.rs
47
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<O: Writeable, W: Writer>(
|
||||
writer: &mut W,
|
||||
o: &Option<O>,
|
||||
) -> 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<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>, 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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue