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 mwixnet::store::StoreError;
use rpassword; use rpassword;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::{Arc, Mutex};
use std::thread::{sleep, spawn}; use std::thread::{sleep, spawn};
use std::time::Duration; use std::time::Duration;
use grin_core::core::Transaction;
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
@ -26,311 +27,339 @@ extern crate clap;
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
real_main()?; real_main()?;
std::process::exit(0); std::process::exit(0);
} }
fn real_main() -> Result<(), Box<dyn std::error::Error>> { fn real_main() -> Result<(), Box<dyn std::error::Error>> {
let yml = load_yaml!("mwixnet.yml"); let yml = load_yaml!("mwixnet.yml");
let args = App::from_yaml(yml).get_matches(); let args = App::from_yaml(yml).get_matches();
let chain_type = if args.is_present("testnet") { let chain_type = if args.is_present("testnet") {
ChainTypes::Testnet ChainTypes::Testnet
} else { } else {
ChainTypes::Mainnet ChainTypes::Mainnet
}; };
global::set_local_chain_type(chain_type); global::set_local_chain_type(chain_type);
let config_path = match args.value_of("config_file") { let config_path = match args.value_of("config_file") {
Some(path) => PathBuf::from(path), Some(path) => PathBuf::from(path),
None => { None => {
let mut grin_path = config::get_grin_path(&chain_type); let mut grin_path = config::get_grin_path(&chain_type);
grin_path.push("mwixnet-config.toml"); grin_path.push("mwixnet-config.toml");
grin_path grin_path
} }
}; };
let round_time = args let round_time = args
.value_of("round_time") .value_of("round_time")
.map(|t| t.parse::<u32>().unwrap()); .map(|t| t.parse::<u32>().unwrap());
let bind_addr = args.value_of("bind_addr"); let bind_addr = args.value_of("bind_addr");
let socks_addr = args.value_of("socks_addr"); let socks_addr = args.value_of("socks_addr");
let grin_node_url = args.value_of("grin_node_url"); let grin_node_url = args.value_of("grin_node_url");
let grin_node_secret_path = args.value_of("grin_node_secret_path"); 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_url = args.value_of("wallet_owner_url");
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path"); let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
let prev_server = args let prev_server = args
.value_of("prev_server") .value_of("prev_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap()); .map(|p| DalekPublicKey::from_hex(&p).unwrap());
let next_server = args let next_server = args
.value_of("next_server") .value_of("next_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap()); .map(|p| DalekPublicKey::from_hex(&p).unwrap());
// Write a new config file if init-config command is supplied // Write a new config file if init-config command is supplied
if let ("init-config", Some(_)) = args.subcommand() { if let ("init-config", Some(_)) = args.subcommand() {
if config_path.exists() { if config_path.exists() {
panic!( panic!(
"Config file already exists at {}", "Config file already exists at {}",
config_path.to_string_lossy() config_path.to_string_lossy()
); );
} }
let server_config = ServerConfig { let server_config = ServerConfig {
key: crypto::secp::random_secret(), key: crypto::secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?, addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?, socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
grin_node_url: match grin_node_url { grin_node_url: match grin_node_url {
Some(u) => u.parse()?, Some(u) => u.parse()?,
None => config::grin_node_url(&chain_type), None => config::grin_node_url(&chain_type),
}, },
grin_node_secret_path: match grin_node_secret_path { grin_node_secret_path: match grin_node_secret_path {
Some(p) => Some(p.to_owned()), Some(p) => Some(p.to_owned()),
None => config::node_secret_path(&chain_type) None => config::node_secret_path(&chain_type)
.to_str() .to_str()
.map(|p| p.to_owned()), .map(|p| p.to_owned()),
}, },
wallet_owner_url: match wallet_owner_url { wallet_owner_url: match wallet_owner_url {
Some(u) => u.parse()?, Some(u) => u.parse()?,
None => config::wallet_owner_url(&chain_type), None => config::wallet_owner_url(&chain_type),
}, },
wallet_owner_secret_path: match wallet_owner_secret_path { wallet_owner_secret_path: match wallet_owner_secret_path {
Some(p) => Some(p.to_owned()), Some(p) => Some(p.to_owned()),
None => config::wallet_owner_secret_path(&chain_type) None => config::wallet_owner_secret_path(&chain_type)
.to_str() .to_str()
.map(|p| p.to_owned()), .map(|p| p.to_owned()),
}, },
prev_server, prev_server,
next_server, next_server,
}; };
let password = prompt_password_confirm(); let password = prompt_password_confirm();
config::write_config(&config_path, &server_config, &password)?; config::write_config(&config_path, &server_config, &password)?;
println!( println!(
"Config file written to {:?}. Please back this file up in a safe place.", "Config file written to {:?}. Please back this file up in a safe place.",
config_path config_path
); );
return Ok(()); return Ok(());
} }
let password = prompt_password(); let password = prompt_password();
let mut server_config = config::load_config(&config_path, &password)?; let mut server_config = config::load_config(&config_path, &password)?;
// Override grin_node_url, if supplied // Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url { if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?; server_config.grin_node_url = grin_node_url.parse()?;
} }
// Override grin_node_secret_path, if supplied // Override grin_node_secret_path, if supplied
if let Some(grin_node_secret_path) = grin_node_secret_path { if let Some(grin_node_secret_path) = grin_node_secret_path {
server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned()); server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned());
} }
// Override wallet_owner_url, if supplied // Override wallet_owner_url, if supplied
if let Some(wallet_owner_url) = wallet_owner_url { if let Some(wallet_owner_url) = wallet_owner_url {
server_config.wallet_owner_url = wallet_owner_url.parse()?; server_config.wallet_owner_url = wallet_owner_url.parse()?;
} }
// Override wallet_owner_secret_path, if supplied // Override wallet_owner_secret_path, if supplied
if let Some(wallet_owner_secret_path) = wallet_owner_secret_path { if let Some(wallet_owner_secret_path) = wallet_owner_secret_path {
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned()); server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
} }
// Override bind_addr, if supplied // Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr { if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?; server_config.addr = bind_addr.parse()?;
} }
// Override socks_addr, if supplied // Override socks_addr, if supplied
if let Some(socks_addr) = socks_addr { if let Some(socks_addr) = socks_addr {
server_config.socks_proxy_addr = socks_addr.parse()?; server_config.socks_proxy_addr = socks_addr.parse()?;
} }
// Override prev_server, if supplied // Override prev_server, if supplied
if let Some(prev_server) = prev_server { if let Some(prev_server) = prev_server {
server_config.prev_server = Some(prev_server); server_config.prev_server = Some(prev_server);
} }
// Override next_server, if supplied // Override next_server, if supplied
if let Some(next_server) = next_server { if let Some(next_server) = next_server {
server_config.next_server = Some(next_server); server_config.next_server = Some(next_server);
} }
// Create GrinNode // Create GrinNode
let node = HttpGrinNode::new( let node = HttpGrinNode::new(
&server_config.grin_node_url, &server_config.grin_node_url,
&server_config.node_api_secret(), &server_config.node_api_secret(),
); );
// Node API health check // Node API health check
let mut rt = tokio::runtime::Builder::new() let mut rt = tokio::runtime::Builder::new()
.threaded_scheduler() .threaded_scheduler()
.enable_all() .enable_all()
.build()?; .build()?;
if let Err(e) = rt.block_on(node.async_get_chain_height()) { if let Err(e) = rt.block_on(node.async_get_chain_tip()) {
eprintln!("Node communication failure. Is node listening?"); eprintln!("Node communication failure. Is node listening?");
return Err(e.into()); return Err(e.into());
}; };
// Open wallet // Open wallet
let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass")); let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
let wallet = rt.block_on(HttpWallet::async_open_wallet( let wallet = rt.block_on(HttpWallet::async_open_wallet(
&server_config.wallet_owner_url, &server_config.wallet_owner_url,
&server_config.wallet_owner_api_secret(), &server_config.wallet_owner_api_secret(),
&wallet_pass, &wallet_pass,
)); ));
let wallet = match wallet { let wallet = match wallet {
Ok(w) => w, Ok(w) => w,
Err(e) => { Err(e) => {
eprintln!("Wallet communication failure. Is wallet listening?"); eprintln!("Wallet communication failure. Is wallet listening?");
return Err(e.into()); return Err(e.into());
} }
}; };
let mut tor_process = tor::init_tor_listener( let mut tor_process = tor::init_tor_listener(
&config::get_grin_path(&chain_type).to_str().unwrap(), &config::get_grin_path(&chain_type).to_str().unwrap(),
&server_config, &server_config,
)?; )?;
let stop_state = Arc::new(StopState::new()); let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone(); let stop_state_clone = stop_state.clone();
rt.spawn(async move { rt.spawn(async move {
futures::executor::block_on(build_signals_fut()); futures::executor::block_on(build_signals_fut());
let _ = tor_process.kill(); let _ = tor_process.kill();
stop_state_clone.stop(); stop_state_clone.stop();
}); });
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| { let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
let client: Arc<dyn MixClient> = let client: Arc<dyn MixClient> =
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone())); Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
client client
}); });
if server_config.prev_server.is_some() { if server_config.prev_server.is_some() {
// Start the JSON-RPC HTTP 'mix' server // Start the JSON-RPC HTTP 'mix' server
println!( println!(
"Starting MIX server with public key {:?}", "Starting MIX server with public key {:?}",
server_config.server_pubkey().to_hex() server_config.server_pubkey().to_hex()
); );
let (_, http_server) = servers::mix_rpc::listen( let (_, http_server) = servers::mix_rpc::listen(
rt.handle(), rt.handle(),
server_config, server_config,
next_mixer, next_mixer,
Arc::new(wallet), Arc::new(wallet),
Arc::new(node), Arc::new(node),
)?; )?;
let close_handle = http_server.close_handle(); let close_handle = http_server.close_handle();
let round_handle = spawn(move || loop { let round_handle = spawn(move || loop {
if stop_state.is_stopped() { if stop_state.is_stopped() {
close_handle.close(); close_handle.close();
break; break;
} }
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
}); });
http_server.wait(); http_server.wait();
round_handle.join().unwrap(); round_handle.join().unwrap();
} else { } else {
println!( println!(
"Starting SWAP server with public key {:?}", "Starting SWAP server with public key {:?}",
server_config.server_pubkey().to_hex() server_config.server_pubkey().to_hex()
); );
// Open SwapStore // Open SwapStore
let store = SwapStore::new( let store = SwapStore::new(
config::get_grin_path(&chain_type) config::get_grin_path(&chain_type)
.join("db") .join("db")
.to_str() .to_str()
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
"db_root path error".to_string(), "db_root path error".to_string(),
)))?, )))?,
)?; )?;
// Start the mwixnet JSON-RPC HTTP 'swap' server // Start the mwixnet JSON-RPC HTTP 'swap' server
let (swap_server, http_server) = servers::swap_rpc::listen( let (swap_server, http_server) = servers::swap_rpc::listen(
rt.handle(), rt.handle(),
&server_config, &server_config,
next_mixer, next_mixer,
Arc::new(wallet), Arc::new(wallet),
Arc::new(node), Arc::new(node),
store, store,
)?; )?;
let close_handle = http_server.close_handle(); let close_handle = http_server.close_handle();
let round_handle = spawn(move || { let round_handle = spawn(move || {
let mut secs = 0; let mut secs = 0;
loop { let prev_tx = Arc::new(Mutex::new(None));
if stop_state.is_stopped() { let server = swap_server.clone();
close_handle.close();
break;
}
sleep(Duration::from_secs(1)); loop {
secs = (secs + 1) % server_config.interval_s; if stop_state.is_stopped() {
close_handle.close();
break;
}
if secs == 0 { sleep(Duration::from_secs(1));
let server = swap_server.clone(); secs = (secs + 1) % server_config.interval_s;
rt.spawn(async move { server.lock().await.execute_round().await });
//let _ = swap_server.lock().unwrap().execute_round();
}
}
});
http_server.wait(); if secs == 0 {
round_handle.join().unwrap(); 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)] #[cfg(unix)]
async fn build_signals_fut() { async fn build_signals_fut() {
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
// Listen for SIGINT, SIGQUIT, and SIGTERM // Listen for SIGINT, SIGQUIT, and SIGTERM
let mut terminate_signal = let mut terminate_signal =
signal(SignalKind::terminate()).expect("failed to create 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 quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal");
let mut interrupt_signal = let mut interrupt_signal =
signal(SignalKind::interrupt()).expect("failed to create interrupt signal"); signal(SignalKind::interrupt()).expect("failed to create interrupt signal");
futures::future::select_all(vec![ futures::future::select_all(vec![
Box::pin(terminate_signal.recv()), Box::pin(terminate_signal.recv()),
Box::pin(quit_signal.recv()), Box::pin(quit_signal.recv()),
Box::pin(interrupt_signal.recv()), Box::pin(interrupt_signal.recv()),
]) ])
.await; .await;
} }
#[cfg(not(unix))] #[cfg(not(unix))]
async fn build_signals_fut() { async fn build_signals_fut() {
tokio::signal::ctrl_c() tokio::signal::ctrl_c()
.await .await
.expect("failed to install CTRL+C signal handler"); .expect("failed to install CTRL+C signal handler");
} }
fn prompt_password() -> ZeroingString { 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 { fn prompt_password_confirm() -> ZeroingString {
let mut first = "first".to_string(); let mut first = "first".to_string();
let mut second = "second".to_string(); let mut second = "second".to_string();
while first != second { while first != second {
first = rpassword::prompt_password_stdout("Server password: ").unwrap(); first = rpassword::prompt_password_stdout("Server password: ").unwrap();
second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap(); second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap();
} }
ZeroingString::from(first) ZeroingString::from(first)
} }
fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString { fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
match *wallet_pass { match *wallet_pass {
Some(wallet_pass) => ZeroingString::from(wallet_pass), Some(wallet_pass) => ZeroingString::from(wallet_pass),
None => { None => {
ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap()) 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::{client, LocatedTxKernel};
use grin_api::{OutputPrintable, OutputType, Tip}; use grin_api::{OutputPrintable, OutputType, Tip};
use grin_core::consensus::COINBASE_MATURITY; 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 grin_util::ToHex;
use async_trait::async_trait; use async_trait::async_trait;
use serde_json::json; use serde_json::json;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use grin_core::core::hash::Hash;
use thiserror::Error; use thiserror::Error;
#[async_trait] #[async_trait]
@ -21,8 +22,8 @@ pub trait GrinNode: Send + Sync {
output_commit: &Commitment, output_commit: &Commitment,
) -> Result<Option<OutputPrintable>, NodeError>; ) -> Result<Option<OutputPrintable>, NodeError>;
/// Gets the height of the chain tip /// Gets the height and hash of the chain tip
async fn async_get_chain_height(&self) -> Result<u64, NodeError>; async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>;
/// Posts a transaction to the grin node /// Posts a transaction to the grin node
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>; async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>;
@ -108,6 +109,23 @@ pub async fn async_build_input(
Ok(None) 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 /// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
#[derive(Clone)] #[derive(Clone)]
pub struct HttpGrinNode { pub struct HttpGrinNode {
@ -176,7 +194,7 @@ impl GrinNode for HttpGrinNode {
Ok(Some(outputs[0].clone())) 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 params = json!([]);
let tip_json = self let tip_json = self
.async_send_request::<serde_json::Value>("get_tip", &params) .async_send_request::<serde_json::Value>("get_tip", &params)
@ -184,7 +202,7 @@ impl GrinNode for HttpGrinNode {
let tip = let tip =
serde_json::from_value::<Tip>(tip_json).map_err(NodeError::DecodeResponseError)?; 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> { async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> {
@ -226,6 +244,7 @@ pub mod mock {
use grin_onion::crypto::secp::Commitment; use grin_onion::crypto::secp::Commitment;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::RwLock; use std::sync::RwLock;
use grin_core::core::hash::Hash;
/// Implementation of 'GrinNode' trait that mocks a grin node instance. /// Implementation of 'GrinNode' trait that mocks a grin node instance.
/// Use only for testing purposes. /// Use only for testing purposes.
@ -299,8 +318,8 @@ pub mod mock {
Ok(None) Ok(None)
} }
async fn async_get_chain_height(&self) -> Result<u64, NodeError> { async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> {
Ok(100) Ok((100, Hash::default()))
} }
async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> { 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::onion::Onion;
use grin_onion::util::{read_optional, write_optional}; use grin_onion::util::{read_optional, write_optional};
use grin_core::core::Input; use grin_core::core::{Input, Transaction};
use grin_core::ser::{ 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_store::{self as store, Store};
use grin_util::ToHex; use grin_util::ToHex;
@ -14,337 +14,390 @@ use thiserror::Error;
const DB_NAME: &str = "swap"; const DB_NAME: &str = "swap";
const STORE_SUBPATH: &str = "swaps"; const STORE_SUBPATH: &str = "swaps";
const CURRENT_VERSION: u8 = 0; const CURRENT_SWAP_VERSION: u8 = 0;
const SWAP_PREFIX: u8 = b'S'; const SWAP_PREFIX: u8 = b'S';
const CURRENT_TX_VERSION: u8 = 0;
const TX_PREFIX: u8 = b'T';
/// Swap statuses /// Swap statuses
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum SwapStatus { pub enum SwapStatus {
Unprocessed, Unprocessed,
InProcess { InProcess {
kernel_commit: Commitment, kernel_commit: Commitment,
}, },
Completed { Completed {
kernel_commit: Commitment, kernel_commit: Commitment,
block_hash: Hash, block_hash: Hash,
}, },
Failed, Failed,
} }
impl Writeable for SwapStatus { impl Writeable for SwapStatus {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
match self { match self {
SwapStatus::Unprocessed => { SwapStatus::Unprocessed => {
writer.write_u8(0)?; writer.write_u8(0)?;
} }
SwapStatus::InProcess { kernel_commit } => { SwapStatus::InProcess { kernel_commit } => {
writer.write_u8(1)?; writer.write_u8(1)?;
kernel_commit.write(writer)?; kernel_commit.write(writer)?;
} }
SwapStatus::Completed { SwapStatus::Completed {
kernel_commit, kernel_commit,
block_hash, block_hash,
} => { } => {
writer.write_u8(2)?; writer.write_u8(2)?;
kernel_commit.write(writer)?; kernel_commit.write(writer)?;
block_hash.write(writer)?; block_hash.write(writer)?;
} }
SwapStatus::Failed => { SwapStatus::Failed => {
writer.write_u8(3)?; writer.write_u8(3)?;
} }
}; };
Ok(()) Ok(())
} }
} }
impl Readable for SwapStatus { impl Readable for SwapStatus {
fn read<R: Reader>(reader: &mut R) -> Result<SwapStatus, ser::Error> { fn read<R: Reader>(reader: &mut R) -> Result<SwapStatus, ser::Error> {
let status = match reader.read_u8()? { let status = match reader.read_u8()? {
0 => SwapStatus::Unprocessed, 0 => SwapStatus::Unprocessed,
1 => { 1 => {
let kernel_commit = Commitment::read(reader)?; let kernel_commit = Commitment::read(reader)?;
SwapStatus::InProcess { kernel_commit } SwapStatus::InProcess { kernel_commit }
} }
2 => { 2 => {
let kernel_commit = Commitment::read(reader)?; let kernel_commit = Commitment::read(reader)?;
let block_hash = Hash::read(reader)?; let block_hash = Hash::read(reader)?;
SwapStatus::Completed { SwapStatus::Completed {
kernel_commit, kernel_commit,
block_hash, block_hash,
} }
} }
3 => SwapStatus::Failed, 3 => SwapStatus::Failed,
_ => { _ => {
return Err(ser::Error::CorruptedData); return Err(ser::Error::CorruptedData);
} }
}; };
Ok(status) Ok(status)
} }
} }
/// Data needed to swap a single output. /// Data needed to swap a single output.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct SwapData { pub struct SwapData {
/// The total excess for the output commitment /// The total excess for the output commitment
pub excess: SecretKey, pub excess: SecretKey,
/// The derived output commitment after applying excess and fee /// The derived output commitment after applying excess and fee
pub output_commit: Commitment, pub output_commit: Commitment,
/// The rangeproof, included only for the final hop (node N) /// The rangeproof, included only for the final hop (node N)
pub rangeproof: Option<RangeProof>, pub rangeproof: Option<RangeProof>,
/// Transaction input being spent /// Transaction input being spent
pub input: Input, pub input: Input,
/// Transaction fee /// Transaction fee
pub fee: u64, pub fee: u64,
/// The remaining onion after peeling off our layer /// The remaining onion after peeling off our layer
pub onion: Onion, pub onion: Onion,
/// The status of the swap /// The status of the swap
pub status: SwapStatus, pub status: SwapStatus,
} }
impl Writeable for SwapData { impl Writeable for SwapData {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(CURRENT_VERSION)?; writer.write_u8(CURRENT_SWAP_VERSION)?;
writer.write_fixed_bytes(&self.excess)?; writer.write_fixed_bytes(&self.excess)?;
writer.write_fixed_bytes(&self.output_commit)?; writer.write_fixed_bytes(&self.output_commit)?;
write_optional(writer, &self.rangeproof)?; write_optional(writer, &self.rangeproof)?;
self.input.write(writer)?; self.input.write(writer)?;
writer.write_u64(self.fee.into())?; writer.write_u64(self.fee.into())?;
self.onion.write(writer)?; self.onion.write(writer)?;
self.status.write(writer)?; self.status.write(writer)?;
Ok(()) Ok(())
} }
} }
impl Readable for SwapData { impl Readable for SwapData {
fn read<R: Reader>(reader: &mut R) -> Result<SwapData, ser::Error> { fn read<R: Reader>(reader: &mut R) -> Result<SwapData, ser::Error> {
let version = reader.read_u8()?; let version = reader.read_u8()?;
if version != CURRENT_VERSION { if version != CURRENT_SWAP_VERSION {
return Err(ser::Error::UnsupportedProtocolVersion); return Err(ser::Error::UnsupportedProtocolVersion);
} }
let excess = secp::read_secret_key(reader)?; let excess = secp::read_secret_key(reader)?;
let output_commit = Commitment::read(reader)?; let output_commit = Commitment::read(reader)?;
let rangeproof = read_optional(reader)?; let rangeproof = read_optional(reader)?;
let input = Input::read(reader)?; let input = Input::read(reader)?;
let fee = reader.read_u64()?; let fee = reader.read_u64()?;
let onion = Onion::read(reader)?; let onion = Onion::read(reader)?;
let status = SwapStatus::read(reader)?; let status = SwapStatus::read(reader)?;
Ok(SwapData { Ok(SwapData {
excess, excess,
output_commit, output_commit,
rangeproof, rangeproof,
input, input,
fee, fee,
onion, onion,
status, 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. /// Storage facility for swap data.
pub struct SwapStore { pub struct SwapStore {
db: Store, db: Store,
} }
/// Store error types /// Store error types
#[derive(Clone, Error, Debug, PartialEq)] #[derive(Clone, Error, Debug, PartialEq)]
pub enum StoreError { pub enum StoreError {
#[error("Swap entry already exists for '{0:?}'")] #[error("Swap entry already exists for '{0:?}'")]
AlreadyExists(Commitment), AlreadyExists(Commitment),
#[error("Error occurred while attempting to open db: {0}")] #[error("Error occurred while attempting to open db: {0}")]
OpenError(store::lmdb::Error), OpenError(store::lmdb::Error),
#[error("Serialization error occurred: {0}")] #[error("Serialization error occurred: {0}")]
SerializationError(ser::Error), SerializationError(ser::Error),
#[error("Error occurred while attempting to read from db: {0}")] #[error("Error occurred while attempting to read from db: {0}")]
ReadError(store::lmdb::Error), ReadError(store::lmdb::Error),
#[error("Error occurred while attempting to write to db: {0}")] #[error("Error occurred while attempting to write to db: {0}")]
WriteError(store::lmdb::Error), WriteError(store::lmdb::Error),
} }
impl From<ser::Error> for StoreError { impl From<ser::Error> for StoreError {
fn from(e: ser::Error) -> StoreError { fn from(e: ser::Error) -> StoreError {
StoreError::SerializationError(e) StoreError::SerializationError(e)
} }
} }
impl SwapStore { impl SwapStore {
/// Create new chain store /// Create new chain store
pub fn new(db_root: &str) -> Result<SwapStore, StoreError> { pub fn new(db_root: &str) -> Result<SwapStore, StoreError> {
let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None)
.map_err(StoreError::OpenError)?; .map_err(StoreError::OpenError)?;
Ok(SwapStore { db }) Ok(SwapStore { db })
} }
/// Writes a single key-value pair to the database /// Writes a single key-value pair to the database
fn write<K: AsRef<[u8]>>( fn write<K: AsRef<[u8]>>(
&self, &self,
prefix: u8, prefix: u8,
k: K, k: K,
value: &Vec<u8>, value: &Vec<u8>,
overwrite: bool, overwrite: bool,
) -> Result<bool, store::lmdb::Error> { ) -> Result<bool, store::lmdb::Error> {
let batch = self.db.batch()?; let batch = self.db.batch()?;
let key = store::to_key(prefix, k); let key = store::to_key(prefix, k);
if !overwrite && batch.exists(&key[..])? { if !overwrite && batch.exists(&key[..])? {
Ok(false) Ok(false)
} else { } else {
batch.put(&key[..], &value[..])?; batch.put(&key[..], &value[..])?;
batch.commit()?; batch.commit()?;
Ok(true) Ok(true)
} }
} }
/// Reads a single value by key /// Reads a single value by key
fn read<K: AsRef<[u8]> + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result<V, StoreError> { 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), || { store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k)[..], None), || {
format!("{}:{}", prefix, k.to_hex()) format!("{}:{}", prefix, k.to_hex())
}) })
.map_err(StoreError::ReadError) .map_err(StoreError::ReadError)
} }
/// Saves a swap to the database /// Saves a swap to the database
pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> { pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> {
let data = ser::ser_vec(&s, ProtocolVersion::local())?; let data = ser::ser_vec(&s, ProtocolVersion::local())?;
let saved = self let saved = self
.write(SWAP_PREFIX, &s.input.commit, &data, overwrite) .write(SWAP_PREFIX, &s.input.commit, &data, overwrite)
.map_err(StoreError::WriteError)?; .map_err(StoreError::WriteError)?;
if !saved { if !saved {
Err(StoreError::AlreadyExists(s.input.commit.clone())) Err(StoreError::AlreadyExists(s.input.commit.clone()))
} else { } else {
Ok(()) Ok(())
} }
} }
/// Iterator over all swaps. /// Iterator over all swaps.
pub fn swaps_iter(&self) -> Result<impl Iterator<Item = SwapData>, StoreError> { pub fn swaps_iter(&self) -> Result<impl Iterator<Item=SwapData>, StoreError> {
let key = store::to_key(SWAP_PREFIX, ""); let key = store::to_key(SWAP_PREFIX, "");
let protocol_version = self.db.protocol_version(); let protocol_version = self.db.protocol_version();
self.db self.db
.iter(&key[..], move |_, mut v| { .iter(&key[..], move |_, mut v| {
ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) ser::deserialize(&mut v, protocol_version, DeserializationMode::default())
.map_err(From::from) .map_err(From::from)
}) })
.map_err(|e| StoreError::ReadError(e)) .map_err(|e| StoreError::ReadError(e))
} }
/// Checks if a matching swap exists in the database /// Checks if a matching swap exists in the database
#[allow(dead_code)] #[allow(dead_code)]
pub fn swap_exists(&self, input_commit: &Commitment) -> Result<bool, StoreError> { pub fn swap_exists(&self, input_commit: &Commitment) -> Result<bool, StoreError> {
let key = store::to_key(SWAP_PREFIX, input_commit); let key = store::to_key(SWAP_PREFIX, input_commit);
self.db self.db
.batch() .batch()
.map_err(StoreError::ReadError)? .map_err(StoreError::ReadError)?
.exists(&key[..]) .exists(&key[..])
.map_err(StoreError::ReadError) .map_err(StoreError::ReadError)
} }
/// Reads a swap from the database /// Reads a swap from the database
#[allow(dead_code)] #[allow(dead_code)]
pub fn get_swap(&self, input_commit: &Commitment) -> Result<SwapData, StoreError> { pub fn get_swap(&self, input_commit: &Commitment) -> Result<SwapData, StoreError> {
self.read(SWAP_PREFIX, input_commit) 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)] #[cfg(test)]
mod tests { mod tests {
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
use grin_core::core::{Input, OutputFeatures}; use grin_core::core::{Input, OutputFeatures};
use grin_core::global::{self, ChainTypes}; use grin_core::global::{self, ChainTypes};
use grin_onion::crypto::secp; use grin_onion::crypto::secp;
use grin_onion::test_util as onion_test_util; use grin_onion::test_util as onion_test_util;
use rand::RngCore; use rand::RngCore;
use std::cmp::Ordering; use std::cmp::Ordering;
fn new_store(test_name: &str) -> SwapStore { fn new_store(test_name: &str) -> SwapStore {
global::set_local_chain_type(ChainTypes::AutomatedTesting); global::set_local_chain_type(ChainTypes::AutomatedTesting);
let db_root = format!("./target/tmp/.{}", test_name); let db_root = format!("./target/tmp/.{}", test_name);
let _ = std::fs::remove_dir_all(db_root.as_str()); let _ = std::fs::remove_dir_all(db_root.as_str());
SwapStore::new(db_root.as_str()).unwrap() SwapStore::new(db_root.as_str()).unwrap()
} }
fn rand_swap_with_status(status: SwapStatus) -> SwapData { fn rand_swap_with_status(status: SwapStatus) -> SwapData {
SwapData { SwapData {
excess: secp::random_secret(), excess: secp::random_secret(),
output_commit: onion_test_util::rand_commit(), output_commit: onion_test_util::rand_commit(),
rangeproof: Some(onion_test_util::rand_proof()), rangeproof: Some(onion_test_util::rand_proof()),
input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()), input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()),
fee: rand::thread_rng().next_u64(), fee: rand::thread_rng().next_u64(),
onion: onion_test_util::rand_onion(), onion: onion_test_util::rand_onion(),
status, status,
} }
} }
fn rand_swap() -> SwapData { fn rand_swap() -> SwapData {
let s = rand::thread_rng().next_u64() % 3; let s = rand::thread_rng().next_u64() % 3;
let status = if s == 0 { let status = if s == 0 {
SwapStatus::Unprocessed SwapStatus::Unprocessed
} else if s == 1 { } else if s == 1 {
SwapStatus::InProcess { SwapStatus::InProcess {
kernel_commit: onion_test_util::rand_commit(), kernel_commit: onion_test_util::rand_commit(),
} }
} else { } else {
SwapStatus::Completed { SwapStatus::Completed {
kernel_commit: onion_test_util::rand_commit(), kernel_commit: onion_test_util::rand_commit(),
block_hash: onion_test_util::rand_hash(), block_hash: onion_test_util::rand_hash(),
} }
}; };
rand_swap_with_status(status) rand_swap_with_status(status)
} }
#[test] #[test]
fn swap_iter() -> Result<(), Box<dyn std::error::Error>> { fn swap_iter() -> Result<(), Box<dyn std::error::Error>> {
let store = new_store("swap_iter"); let store = new_store("swap_iter");
let mut swaps: Vec<SwapData> = Vec::new(); let mut swaps: Vec<SwapData> = Vec::new();
for _ in 0..5 { for _ in 0..5 {
let swap = rand_swap(); let swap = rand_swap();
store.save_swap(&swap, false)?; store.save_swap(&swap, false)?;
swaps.push(swap); swaps.push(swap);
} }
swaps.sort_by(|a, b| { swaps.sort_by(|a, b| {
if a.input.commit < b.input.commit { if a.input.commit < b.input.commit {
Ordering::Less Ordering::Less
} else if a.input.commit == b.input.commit { } else if a.input.commit == b.input.commit {
Ordering::Equal Ordering::Equal
} else { } else {
Ordering::Greater Ordering::Greater
} }
}); });
let mut i: usize = 0; let mut i: usize = 0;
for swap in store.swaps_iter()? { for swap in store.swaps_iter()? {
assert_eq!(swap, *swaps.get(i).unwrap()); assert_eq!(swap, *swaps.get(i).unwrap());
i += 1; i += 1;
} }
Ok(()) Ok(())
} }
#[test] #[test]
fn save_swap() -> Result<(), Box<dyn std::error::Error>> { fn save_swap() -> Result<(), Box<dyn std::error::Error>> {
let store = new_store("save_swap"); let store = new_store("save_swap");
let mut swap = rand_swap_with_status(SwapStatus::Unprocessed); let mut swap = rand_swap_with_status(SwapStatus::Unprocessed);
assert!(!store.swap_exists(&swap.input.commit)?); assert!(!store.swap_exists(&swap.input.commit)?);
store.save_swap(&swap, false)?; store.save_swap(&swap, false)?;
assert_eq!(swap, store.get_swap(&swap.input.commit)?); assert_eq!(swap, store.get_swap(&swap.input.commit)?);
assert!(store.swap_exists(&swap.input.commit)?); assert!(store.swap_exists(&swap.input.commit)?);
swap.status = SwapStatus::InProcess { swap.status = SwapStatus::InProcess {
kernel_commit: onion_test_util::rand_commit(), kernel_commit: onion_test_util::rand_commit(),
}; };
let result = store.save_swap(&swap, false); let result = store.save_swap(&swap, false);
assert_eq!( assert_eq!(
Err(StoreError::AlreadyExists(swap.input.commit.clone())), Err(StoreError::AlreadyExists(swap.input.commit.clone())),
result result
); );
store.save_swap(&swap, true)?; store.save_swap(&swap, true)?;
assert_eq!(swap, store.get_swap(&swap.input.commit)?); assert_eq!(swap, store.get_swap(&swap.input.commit)?);
Ok(()) Ok(())
} }
} }