basic reorg protection

(cherry picked from commit e3696ed73c2012f20205b98099f397ad799fcff4)
This commit is contained in:
scilio 2024-04-02 17:27:08 -04:00
parent 389581d759
commit d1ae6863f8
4 changed files with 1422 additions and 1272 deletions

View file

@ -16,9 +16,10 @@ use mwixnet::node::GrinNode;
use mwixnet::store::StoreError;
use rpassword;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::thread::{sleep, spawn};
use std::time::Duration;
use grin_core::core::Transaction;
#[macro_use]
extern crate clap;
@ -26,311 +27,339 @@ extern crate clap;
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
fn main() -> Result<(), Box<dyn std::error::Error>> {
real_main()?;
std::process::exit(0);
real_main()?;
std::process::exit(0);
}
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
let yml = load_yaml!("mwixnet.yml");
let args = App::from_yaml(yml).get_matches();
let chain_type = if args.is_present("testnet") {
ChainTypes::Testnet
} else {
ChainTypes::Mainnet
};
global::set_local_chain_type(chain_type);
let yml = load_yaml!("mwixnet.yml");
let args = App::from_yaml(yml).get_matches();
let chain_type = if args.is_present("testnet") {
ChainTypes::Testnet
} else {
ChainTypes::Mainnet
};
global::set_local_chain_type(chain_type);
let config_path = match args.value_of("config_file") {
Some(path) => PathBuf::from(path),
None => {
let mut grin_path = config::get_grin_path(&chain_type);
grin_path.push("mwixnet-config.toml");
grin_path
}
};
let config_path = match args.value_of("config_file") {
Some(path) => PathBuf::from(path),
None => {
let mut grin_path = config::get_grin_path(&chain_type);
grin_path.push("mwixnet-config.toml");
grin_path
}
};
let round_time = args
.value_of("round_time")
.map(|t| t.parse::<u32>().unwrap());
let bind_addr = args.value_of("bind_addr");
let socks_addr = args.value_of("socks_addr");
let grin_node_url = args.value_of("grin_node_url");
let grin_node_secret_path = args.value_of("grin_node_secret_path");
let wallet_owner_url = args.value_of("wallet_owner_url");
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
let prev_server = args
.value_of("prev_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
let next_server = args
.value_of("next_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
let round_time = args
.value_of("round_time")
.map(|t| t.parse::<u32>().unwrap());
let bind_addr = args.value_of("bind_addr");
let socks_addr = args.value_of("socks_addr");
let grin_node_url = args.value_of("grin_node_url");
let grin_node_secret_path = args.value_of("grin_node_secret_path");
let wallet_owner_url = args.value_of("wallet_owner_url");
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
let prev_server = args
.value_of("prev_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
let next_server = args
.value_of("next_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
// Write a new config file if init-config command is supplied
if let ("init-config", Some(_)) = args.subcommand() {
if config_path.exists() {
panic!(
"Config file already exists at {}",
config_path.to_string_lossy()
);
}
// Write a new config file if init-config command is supplied
if let ("init-config", Some(_)) = args.subcommand() {
if config_path.exists() {
panic!(
"Config file already exists at {}",
config_path.to_string_lossy()
);
}
let server_config = ServerConfig {
key: crypto::secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
grin_node_url: match grin_node_url {
Some(u) => u.parse()?,
None => config::grin_node_url(&chain_type),
},
grin_node_secret_path: match grin_node_secret_path {
Some(p) => Some(p.to_owned()),
None => config::node_secret_path(&chain_type)
.to_str()
.map(|p| p.to_owned()),
},
wallet_owner_url: match wallet_owner_url {
Some(u) => u.parse()?,
None => config::wallet_owner_url(&chain_type),
},
wallet_owner_secret_path: match wallet_owner_secret_path {
Some(p) => Some(p.to_owned()),
None => config::wallet_owner_secret_path(&chain_type)
.to_str()
.map(|p| p.to_owned()),
},
prev_server,
next_server,
};
let server_config = ServerConfig {
key: crypto::secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
grin_node_url: match grin_node_url {
Some(u) => u.parse()?,
None => config::grin_node_url(&chain_type),
},
grin_node_secret_path: match grin_node_secret_path {
Some(p) => Some(p.to_owned()),
None => config::node_secret_path(&chain_type)
.to_str()
.map(|p| p.to_owned()),
},
wallet_owner_url: match wallet_owner_url {
Some(u) => u.parse()?,
None => config::wallet_owner_url(&chain_type),
},
wallet_owner_secret_path: match wallet_owner_secret_path {
Some(p) => Some(p.to_owned()),
None => config::wallet_owner_secret_path(&chain_type)
.to_str()
.map(|p| p.to_owned()),
},
prev_server,
next_server,
};
let password = prompt_password_confirm();
config::write_config(&config_path, &server_config, &password)?;
println!(
"Config file written to {:?}. Please back this file up in a safe place.",
config_path
);
return Ok(());
}
let password = prompt_password_confirm();
config::write_config(&config_path, &server_config, &password)?;
println!(
"Config file written to {:?}. Please back this file up in a safe place.",
config_path
);
return Ok(());
}
let password = prompt_password();
let mut server_config = config::load_config(&config_path, &password)?;
let password = prompt_password();
let mut server_config = config::load_config(&config_path, &password)?;
// Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?;
}
// Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?;
}
// Override grin_node_secret_path, if supplied
if let Some(grin_node_secret_path) = grin_node_secret_path {
server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned());
}
// Override grin_node_secret_path, if supplied
if let Some(grin_node_secret_path) = grin_node_secret_path {
server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned());
}
// Override wallet_owner_url, if supplied
if let Some(wallet_owner_url) = wallet_owner_url {
server_config.wallet_owner_url = wallet_owner_url.parse()?;
}
// Override wallet_owner_url, if supplied
if let Some(wallet_owner_url) = wallet_owner_url {
server_config.wallet_owner_url = wallet_owner_url.parse()?;
}
// Override wallet_owner_secret_path, if supplied
if let Some(wallet_owner_secret_path) = wallet_owner_secret_path {
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
}
// Override wallet_owner_secret_path, if supplied
if let Some(wallet_owner_secret_path) = wallet_owner_secret_path {
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
}
// Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?;
}
// Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?;
}
// Override socks_addr, if supplied
if let Some(socks_addr) = socks_addr {
server_config.socks_proxy_addr = socks_addr.parse()?;
}
// Override socks_addr, if supplied
if let Some(socks_addr) = socks_addr {
server_config.socks_proxy_addr = socks_addr.parse()?;
}
// Override prev_server, if supplied
if let Some(prev_server) = prev_server {
server_config.prev_server = Some(prev_server);
}
// Override prev_server, if supplied
if let Some(prev_server) = prev_server {
server_config.prev_server = Some(prev_server);
}
// Override next_server, if supplied
if let Some(next_server) = next_server {
server_config.next_server = Some(next_server);
}
// Override next_server, if supplied
if let Some(next_server) = next_server {
server_config.next_server = Some(next_server);
}
// Create GrinNode
let node = HttpGrinNode::new(
&server_config.grin_node_url,
&server_config.node_api_secret(),
);
// Create GrinNode
let node = HttpGrinNode::new(
&server_config.grin_node_url,
&server_config.node_api_secret(),
);
// Node API health check
let mut rt = tokio::runtime::Builder::new()
.threaded_scheduler()
.enable_all()
.build()?;
if let Err(e) = rt.block_on(node.async_get_chain_height()) {
eprintln!("Node communication failure. Is node listening?");
return Err(e.into());
};
// Node API health check
let mut rt = tokio::runtime::Builder::new()
.threaded_scheduler()
.enable_all()
.build()?;
if let Err(e) = rt.block_on(node.async_get_chain_tip()) {
eprintln!("Node communication failure. Is node listening?");
return Err(e.into());
};
// Open wallet
let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
let wallet = rt.block_on(HttpWallet::async_open_wallet(
&server_config.wallet_owner_url,
&server_config.wallet_owner_api_secret(),
&wallet_pass,
));
let wallet = match wallet {
Ok(w) => w,
Err(e) => {
eprintln!("Wallet communication failure. Is wallet listening?");
return Err(e.into());
}
};
// Open wallet
let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
let wallet = rt.block_on(HttpWallet::async_open_wallet(
&server_config.wallet_owner_url,
&server_config.wallet_owner_api_secret(),
&wallet_pass,
));
let wallet = match wallet {
Ok(w) => w,
Err(e) => {
eprintln!("Wallet communication failure. Is wallet listening?");
return Err(e.into());
}
};
let mut tor_process = tor::init_tor_listener(
&config::get_grin_path(&chain_type).to_str().unwrap(),
&server_config,
)?;
let mut tor_process = tor::init_tor_listener(
&config::get_grin_path(&chain_type).to_str().unwrap(),
&server_config,
)?;
let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone();
let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone();
rt.spawn(async move {
futures::executor::block_on(build_signals_fut());
let _ = tor_process.kill();
stop_state_clone.stop();
});
rt.spawn(async move {
futures::executor::block_on(build_signals_fut());
let _ = tor_process.kill();
stop_state_clone.stop();
});
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
let client: Arc<dyn MixClient> =
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
client
});
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
let client: Arc<dyn MixClient> =
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
client
});
if server_config.prev_server.is_some() {
// Start the JSON-RPC HTTP 'mix' server
println!(
"Starting MIX server with public key {:?}",
server_config.server_pubkey().to_hex()
);
if server_config.prev_server.is_some() {
// Start the JSON-RPC HTTP 'mix' server
println!(
"Starting MIX server with public key {:?}",
server_config.server_pubkey().to_hex()
);
let (_, http_server) = servers::mix_rpc::listen(
rt.handle(),
server_config,
next_mixer,
Arc::new(wallet),
Arc::new(node),
)?;
let (_, http_server) = servers::mix_rpc::listen(
rt.handle(),
server_config,
next_mixer,
Arc::new(wallet),
Arc::new(node),
)?;
let close_handle = http_server.close_handle();
let round_handle = spawn(move || loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
let close_handle = http_server.close_handle();
let round_handle = spawn(move || loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
sleep(Duration::from_millis(100));
});
sleep(Duration::from_millis(100));
});
http_server.wait();
round_handle.join().unwrap();
} else {
println!(
"Starting SWAP server with public key {:?}",
server_config.server_pubkey().to_hex()
);
http_server.wait();
round_handle.join().unwrap();
} else {
println!(
"Starting SWAP server with public key {:?}",
server_config.server_pubkey().to_hex()
);
// Open SwapStore
let store = SwapStore::new(
config::get_grin_path(&chain_type)
.join("db")
.to_str()
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
"db_root path error".to_string(),
)))?,
)?;
// Open SwapStore
let store = SwapStore::new(
config::get_grin_path(&chain_type)
.join("db")
.to_str()
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
"db_root path error".to_string(),
)))?,
)?;
// Start the mwixnet JSON-RPC HTTP 'swap' server
let (swap_server, http_server) = servers::swap_rpc::listen(
rt.handle(),
&server_config,
next_mixer,
Arc::new(wallet),
Arc::new(node),
store,
)?;
// Start the mwixnet JSON-RPC HTTP 'swap' server
let (swap_server, http_server) = servers::swap_rpc::listen(
rt.handle(),
&server_config,
next_mixer,
Arc::new(wallet),
Arc::new(node),
store,
)?;
let close_handle = http_server.close_handle();
let round_handle = spawn(move || {
let mut secs = 0;
loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
let close_handle = http_server.close_handle();
let round_handle = spawn(move || {
let mut secs = 0;
let prev_tx = Arc::new(Mutex::new(None));
let server = swap_server.clone();
sleep(Duration::from_secs(1));
secs = (secs + 1) % server_config.interval_s;
loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
if secs == 0 {
let server = swap_server.clone();
rt.spawn(async move { server.lock().await.execute_round().await });
//let _ = swap_server.lock().unwrap().execute_round();
}
}
});
sleep(Duration::from_secs(1));
secs = (secs + 1) % server_config.interval_s;
http_server.wait();
round_handle.join().unwrap();
}
if secs == 0 {
let prev_tx_clone = prev_tx.clone();
let server_clone = server.clone();
rt.spawn(async move {
let result = server_clone.lock().await.execute_round().await;
let mut prev_tx_lock = prev_tx_clone.lock().unwrap();
*prev_tx_lock = match result {
Ok(Some(tx)) => Some(tx),
_ => None,
};
});
} else if secs % 30 == 0 {
let prev_tx_clone = prev_tx.clone();
let server_clone = server.clone();
rt.spawn(async move {
let tx_option = {
let prev_tx_lock = prev_tx_clone.lock().unwrap();
prev_tx_lock.clone()
}; // Lock is dropped here
Ok(())
if let Some(tx) = tx_option {
let result = server_clone.lock().await.check_reorg(tx).await;
let mut prev_tx_lock = prev_tx_clone.lock().unwrap();
*prev_tx_lock = match result {
Ok(Some(tx)) => Some(tx),
_ => None,
};
}
});
}
}
});
http_server.wait();
round_handle.join().unwrap();
}
Ok(())
}
#[cfg(unix)]
async fn build_signals_fut() {
use tokio::signal::unix::{signal, SignalKind};
use tokio::signal::unix::{signal, SignalKind};
// Listen for SIGINT, SIGQUIT, and SIGTERM
let mut terminate_signal =
signal(SignalKind::terminate()).expect("failed to create terminate signal");
let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal");
let mut interrupt_signal =
signal(SignalKind::interrupt()).expect("failed to create interrupt signal");
// Listen for SIGINT, SIGQUIT, and SIGTERM
let mut terminate_signal =
signal(SignalKind::terminate()).expect("failed to create terminate signal");
let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal");
let mut interrupt_signal =
signal(SignalKind::interrupt()).expect("failed to create interrupt signal");
futures::future::select_all(vec![
Box::pin(terminate_signal.recv()),
Box::pin(quit_signal.recv()),
Box::pin(interrupt_signal.recv()),
])
.await;
futures::future::select_all(vec![
Box::pin(terminate_signal.recv()),
Box::pin(quit_signal.recv()),
Box::pin(interrupt_signal.recv()),
])
.await;
}
#[cfg(not(unix))]
async fn build_signals_fut() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
fn prompt_password() -> ZeroingString {
ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap())
ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap())
}
fn prompt_password_confirm() -> ZeroingString {
let mut first = "first".to_string();
let mut second = "second".to_string();
while first != second {
first = rpassword::prompt_password_stdout("Server password: ").unwrap();
second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap();
}
ZeroingString::from(first)
let mut first = "first".to_string();
let mut second = "second".to_string();
while first != second {
first = rpassword::prompt_password_stdout("Server password: ").unwrap();
second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap();
}
ZeroingString::from(first)
}
fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
match *wallet_pass {
Some(wallet_pass) => ZeroingString::from(wallet_pass),
None => {
ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap())
}
}
match *wallet_pass {
Some(wallet_pass) => ZeroingString::from(wallet_pass),
None => {
ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap())
}
}
}

View file

@ -4,13 +4,14 @@ use grin_api::json_rpc::{build_request, Request, Response};
use grin_api::{client, LocatedTxKernel};
use grin_api::{OutputPrintable, OutputType, Tip};
use grin_core::consensus::COINBASE_MATURITY;
use grin_core::core::{Input, OutputFeatures, Transaction};
use grin_core::core::{Committed, Input, OutputFeatures, Transaction};
use grin_util::ToHex;
use async_trait::async_trait;
use serde_json::json;
use std::net::SocketAddr;
use std::sync::Arc;
use grin_core::core::hash::Hash;
use thiserror::Error;
#[async_trait]
@ -21,8 +22,8 @@ pub trait GrinNode: Send + Sync {
output_commit: &Commitment,
) -> Result<Option<OutputPrintable>, NodeError>;
/// Gets the height of the chain tip
async fn async_get_chain_height(&self) -> Result<u64, NodeError>;
/// Gets the height and hash of the chain tip
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>;
/// Posts a transaction to the grin node
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>;
@ -108,6 +109,23 @@ pub async fn async_build_input(
Ok(None)
}
pub async fn async_is_tx_valid(node: &Arc<dyn GrinNode>, tx: &Transaction) -> Result<bool, NodeError> {
let next_block_height = node.async_get_chain_tip().await?.0 + 1;
for input_commit in &tx.inputs_committed() {
if !async_is_spendable(&node, &input_commit, next_block_height).await? {
return Ok(false);
}
}
for output_commit in &tx.outputs_committed() {
if async_is_unspent(&node, &output_commit).await? {
return Ok(false);
}
}
Ok(true)
}
/// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
#[derive(Clone)]
pub struct HttpGrinNode {
@ -176,7 +194,7 @@ impl GrinNode for HttpGrinNode {
Ok(Some(outputs[0].clone()))
}
async fn async_get_chain_height(&self) -> Result<u64, NodeError> {
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
let params = json!([]);
let tip_json = self
.async_send_request::<serde_json::Value>("get_tip", &params)
@ -184,7 +202,7 @@ impl GrinNode for HttpGrinNode {
let tip =
serde_json::from_value::<Tip>(tip_json).map_err(NodeError::DecodeResponseError)?;
Ok(tip.height)
Ok((tip.height, Hash::from_hex(tip.last_block_pushed.as_str()).unwrap()))
}
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
@ -226,6 +244,7 @@ pub mod mock {
use grin_onion::crypto::secp::Commitment;
use std::collections::HashMap;
use std::sync::RwLock;
use grin_core::core::hash::Hash;
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
/// Use only for testing purposes.
@ -299,8 +318,8 @@ pub mod mock {
Ok(None)
}
async fn async_get_chain_height(&self) -> Result<u64, NodeError> {
Ok(100)
async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
Ok((100, Hash::default()))
}
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,9 @@ use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey};
use grin_onion::onion::Onion;
use grin_onion::util::{read_optional, write_optional};
use grin_core::core::Input;
use grin_core::core::{Input, Transaction};
use grin_core::ser::{
self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer,
self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer,
};
use grin_store::{self as store, Store};
use grin_util::ToHex;
@ -14,337 +14,390 @@ use thiserror::Error;
const DB_NAME: &str = "swap";
const STORE_SUBPATH: &str = "swaps";
const CURRENT_VERSION: u8 = 0;
const CURRENT_SWAP_VERSION: u8 = 0;
const SWAP_PREFIX: u8 = b'S';
const CURRENT_TX_VERSION: u8 = 0;
const TX_PREFIX: u8 = b'T';
/// Swap statuses
#[derive(Clone, Debug, PartialEq)]
pub enum SwapStatus {
Unprocessed,
InProcess {
kernel_commit: Commitment,
},
Completed {
kernel_commit: Commitment,
block_hash: Hash,
},
Failed,
Unprocessed,
InProcess {
kernel_commit: Commitment,
},
Completed {
kernel_commit: Commitment,
block_hash: Hash,
},
Failed,
}
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_commit } => {
writer.write_u8(1)?;
kernel_commit.write(writer)?;
}
SwapStatus::Completed {
kernel_commit,
block_hash,
} => {
writer.write_u8(2)?;
kernel_commit.write(writer)?;
block_hash.write(writer)?;
}
SwapStatus::Failed => {
writer.write_u8(3)?;
}
};
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
match self {
SwapStatus::Unprocessed => {
writer.write_u8(0)?;
}
SwapStatus::InProcess { kernel_commit } => {
writer.write_u8(1)?;
kernel_commit.write(writer)?;
}
SwapStatus::Completed {
kernel_commit,
block_hash,
} => {
writer.write_u8(2)?;
kernel_commit.write(writer)?;
block_hash.write(writer)?;
}
SwapStatus::Failed => {
writer.write_u8(3)?;
}
};
Ok(())
}
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_commit = Commitment::read(reader)?;
SwapStatus::InProcess { kernel_commit }
}
2 => {
let kernel_commit = Commitment::read(reader)?;
let block_hash = Hash::read(reader)?;
SwapStatus::Completed {
kernel_commit,
block_hash,
}
}
3 => SwapStatus::Failed,
_ => {
return Err(ser::Error::CorruptedData);
}
};
Ok(status)
}
fn read<R: Reader>(reader: &mut R) -> Result<SwapStatus, ser::Error> {
let status = match reader.read_u8()? {
0 => SwapStatus::Unprocessed,
1 => {
let kernel_commit = Commitment::read(reader)?;
SwapStatus::InProcess { kernel_commit }
}
2 => {
let kernel_commit = Commitment::read(reader)?;
let block_hash = Hash::read(reader)?;
SwapStatus::Completed {
kernel_commit,
block_hash,
}
}
3 => SwapStatus::Failed,
_ => {
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,
/// 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)?;
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(CURRENT_SWAP_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(())
}
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);
}
fn read<R: Reader>(reader: &mut R) -> Result<SwapData, ser::Error> {
let version = reader.read_u8()?;
if version != CURRENT_SWAP_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,
})
}
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,
})
}
}
/// A transaction created as part of a swap round.
#[derive(Clone, Debug, PartialEq)]
pub struct SwapTx {
pub tx: Transaction,
pub chain_tip: (u64, Hash),
// TODO: Include status
}
impl Writeable for SwapTx {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(CURRENT_TX_VERSION)?;
self.tx.write(writer)?;
writer.write_u64(self.chain_tip.0)?;
self.chain_tip.1.write(writer)?;
Ok(())
}
}
impl Readable for SwapTx {
fn read<R: Reader>(reader: &mut R) -> Result<SwapTx, ser::Error> {
let version = reader.read_u8()?;
if version != CURRENT_TX_VERSION {
return Err(ser::Error::UnsupportedProtocolVersion);
}
let tx = Transaction::read(reader)?;
let height = reader.read_u64()?;
let block_hash = Hash::read(reader)?;
Ok(SwapTx {
tx,
chain_tip: (height, block_hash),
})
}
}
/// Storage facility for swap data.
pub struct SwapStore {
db: Store,
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),
#[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)
}
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 })
}
/// 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)
}
}
/// 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)
}
/// 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(())
}
}
/// 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))
}
/// 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)
}
/// 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)
}
/// 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)
}
/// Saves a swap transaction to the database
pub fn save_swap_tx(&self, s: &SwapTx) -> Result<(), StoreError> {
let data = ser::ser_vec(&s, ProtocolVersion::local())?;
self
.write(TX_PREFIX, &s.tx.kernels().first().unwrap().excess, &data, true)
.map_err(StoreError::WriteError)?;
Ok(())
}
/// Reads a swap tx from the database
pub fn get_swap_tx(&self, kernel_excess: &Commitment) -> Result<SwapTx, StoreError> {
self.read(TX_PREFIX, kernel_excess)
}
}
#[cfg(test)]
mod tests {
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
use grin_core::core::{Input, OutputFeatures};
use grin_core::global::{self, ChainTypes};
use grin_onion::crypto::secp;
use grin_onion::test_util as onion_test_util;
use rand::RngCore;
use std::cmp::Ordering;
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
use grin_core::core::{Input, OutputFeatures};
use grin_core::global::{self, ChainTypes};
use grin_onion::crypto::secp;
use grin_onion::test_util as onion_test_util;
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 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: onion_test_util::rand_commit(),
rangeproof: Some(onion_test_util::rand_proof()),
input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()),
fee: rand::thread_rng().next_u64(),
onion: onion_test_util::rand_onion(),
status,
}
}
fn rand_swap_with_status(status: SwapStatus) -> SwapData {
SwapData {
excess: secp::random_secret(),
output_commit: onion_test_util::rand_commit(),
rangeproof: Some(onion_test_util::rand_proof()),
input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()),
fee: rand::thread_rng().next_u64(),
onion: onion_test_util::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_commit: onion_test_util::rand_commit(),
}
} else {
SwapStatus::Completed {
kernel_commit: onion_test_util::rand_commit(),
block_hash: onion_test_util::rand_hash(),
}
};
rand_swap_with_status(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_commit: onion_test_util::rand_commit(),
}
} else {
SwapStatus::Completed {
kernel_commit: onion_test_util::rand_commit(),
block_hash: onion_test_util::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);
}
#[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
}
});
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;
}
let mut i: usize = 0;
for swap in store.swaps_iter()? {
assert_eq!(swap, *swaps.get(i).unwrap());
i += 1;
}
Ok(())
}
Ok(())
}
#[test]
fn save_swap() -> Result<(), Box<dyn std::error::Error>> {
let store = new_store("save_swap");
#[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)?);
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)?);
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_commit: onion_test_util::rand_commit(),
};
let result = store.save_swap(&swap, false);
assert_eq!(
Err(StoreError::AlreadyExists(swap.input.commit.clone())),
result
);
swap.status = SwapStatus::InProcess {
kernel_commit: onion_test_util::rand_commit(),
};
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)?);
store.save_swap(&swap, true)?;
assert_eq!(swap, store.get_swap(&swap.input.commit)?);
Ok(())
}
Ok(())
}
}