Merge pull request #16 from scilio/main

Use lmdb store for swap requests
This commit is contained in:
phyro 2022-09-16 19:27:42 +02:00 committed by GitHub
commit dae4f87eaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 596 additions and 103 deletions

1
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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() {

View file

@ -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,

View file

@ -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 {

View file

@ -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};

View file

@ -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
View 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(())
}
}

View file

@ -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(())
}
}