From e37782d264fce86616b1d6b36dac707f200cb7ba Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sun, 9 Apr 2023 09:22:04 +0200 Subject: [PATCH 01/15] abandon teos-common dbm --- teos/src/dbm.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index ca6dabff..ccc72530 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -6,16 +6,17 @@ use std::iter::FromIterator; use std::path::PathBuf; use std::str::FromStr; +use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; use rusqlite::limits::Limit; -use rusqlite::{params, params_from_iter, Connection, Error as SqliteError}; +use rusqlite::{params, params_from_iter, Connection, Error as SqliteError, ErrorCode, Params}; use bitcoin::consensus; use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; use bitcoin::BlockHash; -use teos_common::appointment::{Appointment, Locator}; -use teos_common::dbm::{DatabaseConnection, DatabaseManager, Error}; +use teos_common::appointment::{compute_appointment_slots, Appointment, Locator}; +use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; use teos_common::UserId; use crate::extended_appointment::{ExtendedAppointment, UUID}; @@ -64,6 +65,16 @@ const TABLES: [&str; 6] = [ )", ]; +/// Packs the errors than can raise when interacting with the underlying database. +#[derive(Debug)] +pub enum Error { + AlreadyExists, + MissingForeignKey, + MissingField, + NotFound, + Unknown(SqliteError), +} + /// Component in charge of interacting with the underlying database. /// /// Currently works for `SQLite`. `PostgreSQL` should also be added in the future. @@ -73,7 +84,7 @@ pub struct DBM { connection: Connection, } -impl DatabaseConnection for DBM { +impl DBM { fn get_connection(&self) -> &Connection { &self.connection } @@ -81,6 +92,48 @@ impl DatabaseConnection for DBM { fn get_mut_connection(&mut self) -> &mut Connection { &mut self.connection } + + /// Creates the database tables if not present. + fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { + let tx = self.get_mut_connection().transaction().unwrap(); + for table in tables.iter() { + tx.execute(table, [])?; + } + tx.commit() + } + + /// Generic method to store data into the database. + fn store_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.get_connection().execute(query, params) { + Ok(_) => Ok(()), + Err(e) => match e { + SqliteError::SqliteFailure(ie, _) => match ie.code { + ErrorCode::ConstraintViolation => match ie.extended_code { + SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), + SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + } + } + + /// Generic method to remove data from the database. + fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.get_connection().execute(query, params).unwrap() { + 0 => Err(Error::NotFound), + _ => Ok(()), + } + } + + /// Generic method to update data from the database. + fn update_data(&self, query: &str, params: P) -> Result<(), Error> { + // Updating data is fundamentally the same as deleting it in terms of interface. + // A query is sent and either no row is modified or some rows are + self.remove_data(query, params) + } } impl DBM { From 8bbe5faa35d140e1055c0fb0aaa72fae5178f0d5 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sun, 9 Apr 2023 14:57:56 +0200 Subject: [PATCH 02/15] Use an actor to provide async runtime for the tower components --- teos/src/chain_monitor.rs | 27 +-------- teos/src/dbm.rs | 3 +- teos/src/gatekeeper.rs | 15 +++++ teos/src/lib.rs | 1 + teos/src/listener_actor.rs | 117 +++++++++++++++++++++++++++++++++++++ teos/src/main.rs | 10 +++- teos/src/responder.rs | 14 +++++ teos/src/watcher.rs | 14 +++++ 8 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 teos/src/listener_actor.rs diff --git a/teos/src/chain_monitor.rs b/teos/src/chain_monitor.rs index 35534013..9bb6c10c 100644 --- a/teos/src/chain_monitor.rs +++ b/teos/src/chain_monitor.rs @@ -11,8 +11,6 @@ use lightning::chain; use lightning_block_sync::poll::{ChainTip, Poll, ValidatedBlockHeader}; use lightning_block_sync::{BlockSourceErrorKind, Cache, SpvClient}; -use crate::dbm::DBM; - /// Component in charge of monitoring the chain for new blocks. /// /// Takes care of polling `bitcoind` for new tips and hand it to subscribers. @@ -28,8 +26,6 @@ where spv_client: SpvClient<'a, P, C, L>, /// The lat known block header by the [ChainMonitor]. last_known_block_header: ValidatedBlockHeader, - /// A [DBM] (database manager) instance. Used to persist block data into disk. - dbm: Arc>, /// The time between polls. polling_delta: time::Duration, /// A signal from the main thread indicating the tower is shuting down. @@ -49,7 +45,6 @@ where pub async fn new( spv_client: SpvClient<'a, P, C, L>, last_known_block_header: ValidatedBlockHeader, - dbm: Arc>, polling_delta_sec: u16, shutdown_signal: Listener, bitcoind_reachable: Arc<(Mutex, Condvar)>, @@ -57,7 +52,6 @@ where ChainMonitor { spv_client, last_known_block_header, - dbm, polling_delta: time::Duration::from_secs(polling_delta_sec as u64), shutdown_signal, bitcoind_reachable, @@ -75,11 +69,6 @@ where ChainTip::Better(new_best) => { log::debug!("Updating best tip: {}", new_best.header.block_hash()); self.last_known_block_header = new_best; - self.dbm - .lock() - .unwrap() - .store_last_known_block(&new_best.header.block_hash()) - .unwrap(); } ChainTip::Worse(worse) => { // This would happen both if a block has less chainwork than the previous one, or if it has the same chainwork @@ -179,7 +168,6 @@ mod tests { let mut chain = Blockchain::default().with_height(START_HEIGHT); let tip = chain.tip(); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let (_, shutdown_signal) = triggered::trigger(); let listener = DummyListener::new(); @@ -189,7 +177,7 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let mut cm = - ChainMonitor::new(spv_client, tip, dbm, 1, shutdown_signal, bitcoind_reachable).await; + ChainMonitor::new(spv_client, tip, 1, shutdown_signal, bitcoind_reachable).await; // If there's no new block nothing gets connected nor disconnected cm.poll_best_tip().await; @@ -203,7 +191,6 @@ mod tests { let new_tip = chain.tip(); let old_tip = chain.at_height(START_HEIGHT - 1); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let (_, shutdown_signal) = triggered::trigger(); let listener = DummyListener::new(); @@ -215,7 +202,6 @@ mod tests { let mut cm = ChainMonitor::new( spv_client, old_tip, - dbm, 1, shutdown_signal, bitcoind_reachable, @@ -226,7 +212,7 @@ mod tests { cm.poll_best_tip().await; assert_eq!(cm.last_known_block_header, new_tip); assert_eq!( - cm.dbm.lock().unwrap().load_last_known_block().unwrap(), + cm.last_known_block_header.header.block_hash(), new_tip.deref().header.block_hash() ); assert!(listener @@ -242,7 +228,6 @@ mod tests { let best_tip = chain.tip(); chain.disconnect_tip(); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let (_, shutdown_signal) = triggered::trigger(); let listener = DummyListener::new(); @@ -254,7 +239,6 @@ mod tests { let mut cm = ChainMonitor::new( spv_client, best_tip, - dbm, 1, shutdown_signal, bitcoind_reachable, @@ -264,7 +248,6 @@ mod tests { // If a new (worse, just one) block gets mined, nothing gets connected nor disconnected cm.poll_best_tip().await; assert_eq!(cm.last_known_block_header, best_tip); - assert!(cm.dbm.lock().unwrap().load_last_known_block().is_none()); assert!(listener.connected_blocks.borrow().is_empty()); assert!(listener.disconnected_blocks.borrow().is_empty()); } @@ -281,7 +264,6 @@ mod tests { let new_best = chain.tip(); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let (_, shutdown_signal) = triggered::trigger(); let listener = DummyListener::new(); @@ -293,7 +275,6 @@ mod tests { let mut cm = ChainMonitor::new( spv_client, old_best, - dbm, 1, shutdown_signal, bitcoind_reachable, @@ -304,7 +285,7 @@ mod tests { cm.poll_best_tip().await; assert_eq!(cm.last_known_block_header, new_best); assert_eq!( - cm.dbm.lock().unwrap().load_last_known_block().unwrap(), + cm.last_known_block_header.header.block_hash(), new_best.deref().header.block_hash() ); assert_eq!(*listener.connected_blocks.borrow(), new_blocks); @@ -320,7 +301,6 @@ mod tests { let chain_offline = chain.unreachable.clone(); let tip = chain.tip(); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let (_, shutdown_signal) = triggered::trigger(); let listener = DummyListener::new(); @@ -332,7 +312,6 @@ mod tests { let mut cm = ChainMonitor::new( spv_client, tip, - dbm, 1, shutdown_signal, bitcoind_reachable.clone(), diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index ccc72530..05374224 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -15,8 +15,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; use bitcoin::BlockHash; -use teos_common::appointment::{compute_appointment_slots, Appointment, Locator}; -use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; +use teos_common::appointment::{Appointment, Locator}; use teos_common::UserId; use crate::extended_appointment::{ExtendedAppointment, UUID}; diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index e5d490e3..b4e7d600 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -332,6 +332,21 @@ impl chain::Listen for Gatekeeper { } } +use bitcoin::Block; +use bitcoin::BlockHeader; +use crate::listener_actor::AsyncListen; + +#[tonic::async_trait] +impl AsyncListen for Gatekeeper { + async fn block_connected(&self, block: &Block, height: u32) { + chain::Listen::block_connected(self, block, height); + } + + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { + chain::Listen::block_disconnected(self, header, height); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/teos/src/lib.rs b/teos/src/lib.rs index 2e87bcf1..2418991c 100644 --- a/teos/src/lib.rs +++ b/teos/src/lib.rs @@ -14,6 +14,7 @@ pub mod chain_monitor; pub mod cli_config; pub mod config; pub mod dbm; +pub mod listener_actor; #[doc(hidden)] mod errors; mod extended_appointment; diff --git a/teos/src/listener_actor.rs b/teos/src/listener_actor.rs new file mode 100644 index 00000000..64fcf1bd --- /dev/null +++ b/teos/src/listener_actor.rs @@ -0,0 +1,117 @@ +use crate::dbm::DBM; + +use std::marker::{Send, Sync}; +use std::sync::{Arc, Mutex}; + +use bitcoin::{Block, BlockHeader}; +use lightning::chain; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + +#[tonic::async_trait] +pub trait AsyncListen: Send + Sync { + async fn block_connected(&self, block: &Block, height: u32); + async fn block_disconnected(&self, header: &BlockHeader, height: u32); +} + +#[tonic::async_trait] +impl AsyncListen for Arc { + async fn block_connected(&self, block: &Block, height: u32) { + (**self).block_connected(block, height).await; + } + + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { + (**self).block_disconnected(header, height).await; + } +} + +#[tonic::async_trait] +impl AsyncListen for (F, S) { + async fn block_connected(&self, block: &Block, height: u32) { + self.0.block_connected(block, height).await; + self.1.block_connected(block, height).await; + } + + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { + self.0.block_disconnected(header, height).await; + self.1.block_disconnected(header, height).await; + } +} + +#[derive(Debug)] +enum BlockListenerAction { + BlockConnected(Block, u32), + BlockDisconnected(BlockHeader, u32), +} + +pub struct AsyncBlockListener { + listener: L, + dbm: Arc>, + rx: UnboundedReceiver, +} + +impl AsyncBlockListener +where + L: AsyncListen + 'static, +{ + pub fn new(listener: L, dbm: Arc>) -> SyncBlockListener { + let (tx, rx) = unbounded_channel(); + let actor = AsyncBlockListener { listener, dbm, rx }; + actor.run_actor_in_bg(); + SyncBlockListener { tx } + } + + fn run_actor_in_bg(mut self: Self) { + tokio::spawn(async move { + while let Some(action) = self.rx.recv().await { + match action { + BlockListenerAction::BlockConnected(block, height) => { + self.listener.block_connected(&block, height).await; + // We can update the last known block after all the listeners have received it. + self.dbm + .lock() + .unwrap() + .store_last_known_block(&block.block_hash()) + .unwrap(); + } + BlockListenerAction::BlockDisconnected(header, height) => { + self.listener.block_disconnected(&header, height).await; + } + } + } + }); + } +} + +#[derive(Debug)] +pub struct SyncBlockListener { + tx: UnboundedSender, +} + +impl chain::Listen for SyncBlockListener { + fn block_connected(&self, block: &Block, height: u32) { + self.tx + .send(BlockListenerAction::BlockConnected(block.clone(), height)) + .unwrap(); + } + + fn block_disconnected(&self, header: &BlockHeader, height: u32) { + self.tx + .send(BlockListenerAction::BlockDisconnected(*header, height)) + .unwrap(); + } + + fn filtered_block_connected( + &self, + header: &BlockHeader, + txdata: &chain::transaction::TransactionData, + height: u32, + ) { + let block = Block { + header: header.clone(), + txdata: txdata.iter().map(|&(_, tx)| tx.clone()).collect(), + }; + self.tx + .send(BlockListenerAction::BlockConnected(block, height)) + .unwrap(); + } +} diff --git a/teos/src/main.rs b/teos/src/main.rs index bdd30e06..870feeac 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -26,6 +26,7 @@ use teos::chain_monitor::ChainMonitor; use teos::config::{self, Config, Opt}; use teos::dbm::DBM; use teos::gatekeeper::Gatekeeper; +use teos::listener_actor::AsyncBlockListener; use teos::protos as msgs; use teos::protos::private_tower_services_server::PrivateTowerServicesServer; use teos::protos::public_tower_services_server::PublicTowerServicesServer; @@ -311,13 +312,16 @@ async fn main() { // The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called // first so it updates the users' states and both the Watcher and the Responder operate only on registered users. - let listener = &(gatekeeper, &(watcher.clone(), responder)); + let listeners = (gatekeeper, (watcher.clone(), responder)); + // This spawns a separate async actor that will be fed new blocks from a sync block listener. + // In this way we can have our components listen to blocks in an async manner from the async actor. + let listener = AsyncBlockListener::new(listeners, dbm); + let cache = &mut UnboundedCache::new(); - let spv_client = SpvClient::new(tip, poller, cache, listener); + let spv_client = SpvClient::new(tip, poller, cache, &listener); let mut chain_monitor = ChainMonitor::new( spv_client, tip, - dbm, conf.polling_delta, shutdown_signal_cm, bitcoind_reachable.clone(), diff --git a/teos/src/responder.rs b/teos/src/responder.rs index b7412cca..1345a0bb 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -466,6 +466,20 @@ impl chain::Listen for Responder { } } +use bitcoin::Block; +use crate::listener_actor::AsyncListen; + +#[tonic::async_trait] +impl AsyncListen for Responder { + async fn block_connected(&self, block: &Block, height: u32) { + chain::Listen::block_connected(self, block, height); + } + + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { + chain::Listen::block_disconnected(self, header, height); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index 90606fcf..cfd4e6f3 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -533,6 +533,20 @@ impl chain::Listen for Watcher { } } +use bitcoin::Block; +use crate::listener_actor::AsyncListen; + +#[tonic::async_trait] +impl AsyncListen for Watcher { + async fn block_connected(&self, block: &Block, height: u32) { + chain::Listen::block_connected(self, block, height); + } + + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { + chain::Listen::block_disconnected(self, header, height); + } +} + #[cfg(test)] mod tests { use super::*; From 60e3eb723883c5c4c6bb4d69cfd6dcfb5ff0d730 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 26 Jul 2023 14:22:19 +0300 Subject: [PATCH 03/15] Upgrade to rusqlite 0.29 This is temporary to avoid a dependency conflict that arises in `libsqlite3-sys` --- Cargo.lock | 1864 ++++++++++++++++++++++------------ teos-common/Cargo.toml | 2 +- teos/Cargo.toml | 3 +- watchtower-plugin/Cargo.toml | 2 +- 4 files changed, 1223 insertions(+), 648 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89eb3492..f098e808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aead" version = "0.4.3" @@ -13,24 +28,31 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "getrandom 0.2.5", + "cfg-if 1.0.0", + "getrandom 0.2.10", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "ansi_term" version = "0.12.1" @@ -42,9 +64,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert-json-diff" @@ -58,34 +80,44 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite 0.2.13", ] [[package]] name = "async-stream-impl" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", ] [[package]] @@ -112,11 +144,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.5", + "getrandom 0.2.10", "instant", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "rand 0.8.5", - "tokio 1.25.0", + "tokio 1.32.0", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", ] [[package]] @@ -127,15 +174,15 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64-compat" @@ -146,6 +193,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bech32" version = "0.8.1" @@ -154,9 +207,9 @@ checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" [[package]] name = "bitcoin" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +checksum = "d4d30fb43d287492017964a1fd7d3f82e8cc760818471c6ef2d44111e317d5c3" dependencies = [ "base64-compat", "bech32", @@ -204,6 +257,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -216,9 +278,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -231,18 +293,19 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bstr" -version = "0.2.17" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", + "serde", ] [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" @@ -258,15 +321,18 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -282,21 +348,21 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.7.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08493fa7707effc63254c66c6ea908675912493cd67952eda23c09fae2610b1" +checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" dependencies = [ "cfg-if 1.0.0", "cipher", - "cpufeatures", + "cpufeatures 0.1.5", "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.8.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6547abe025f4027edacd9edaa357aded014eecec42a5070d9b885c3c334aba2" +checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" dependencies = [ "aead", "chacha20", @@ -307,19 +373,18 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ - "num-integer", "num-traits", ] [[package]] name = "chunked_transfer" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" [[package]] name = "cipher" @@ -338,7 +403,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", "unicode-width", @@ -347,33 +412,39 @@ dependencies = [ [[package]] name = "cln-plugin" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49be99e6e5ad55d420884b5b2a68aca890bcd1a1540ed6d2892363623a60f538" +checksum = "1098794b7562120ec5caa7b768847655fd5249088676a8d8ba9110a01becf97b" dependencies = [ "anyhow", - "bytes 1.1.0", + "bytes 1.5.0", "env_logger", "futures", "log", "serde", "serde_json", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-stream", - "tokio-util 0.7.0", + "tokio-util 0.7.8", ] [[package]] name = "colored" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ - "atty", + "is-terminal", "lazy_static", - "winapi 0.3.9", + "windows-sys", ] +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "convert_case" version = "0.4.0" @@ -392,24 +463,67 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -427,9 +541,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -440,29 +554,21 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] -name = "deadpool" -version = "0.9.5" +name = "der" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ - "async-trait", - "deadpool-runtime", - "num_cpus", - "retain_mut", - "tokio 1.25.0", + "const-oid", + "pem-rfc7468", + "zeroize", ] -[[package]] -name = "deadpool-runtime" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" - [[package]] name = "der-oid-macro" version = "0.5.0" @@ -471,7 +577,7 @@ checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436" dependencies = [ "num-bigint", "num-traits", - "syn", + "syn 1.0.109", ] [[package]] @@ -487,6 +593,12 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derive_more" version = "0.99.17" @@ -497,7 +609,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.109", ] [[package]] @@ -515,17 +627,25 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.4", + "const-oid", "crypto-common", + "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", ] [[package]] @@ -538,21 +658,24 @@ dependencies = [ "ed25519", "rand 0.7.3", "serde", - "sha2", + "sha2 0.9.9", "zeroize", ] [[package]] name = "either" -version = "1.6.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] @@ -570,15 +693,21 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" -version = "0.2.8" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "winapi 0.3.9", + "windows-sys", ] [[package]] @@ -591,6 +720,23 @@ dependencies = [ "libc", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.0", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -605,18 +751,33 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.7.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] name = "fixedbitset" -version = "0.4.1" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flume" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.8", +] [[package]] name = "fnv" @@ -641,11 +802,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "matches", "percent-encoding", ] @@ -661,7 +821,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fuchsia-zircon-sys", ] @@ -673,9 +833,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -688,9 +848,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -698,55 +858,66 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.12.1", +] + [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -755,16 +926,16 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -783,20 +954,26 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.5" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "globset" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ "aho-corasick", "bstr", @@ -817,7 +994,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio 0.2.25", "tokio-util 0.3.1", @@ -827,55 +1004,61 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", - "tokio 1.25.0", - "tokio-util 0.6.9", + "tokio 1.32.0", + "tokio-util 0.7.8", "tracing", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.0", ] [[package]] name = "headers" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.0", - "bitflags", - "bytes 1.1.0", + "base64 0.21.4", + "bytes 1.5.0", "headers-core", "http", - "httpdate 1.0.2", + "httpdate 1.0.3", "mime", - "sha-1", + "sha1", ] [[package]] @@ -896,6 +1079,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -907,12 +1099,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -923,6 +1112,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.11.0" @@ -933,24 +1131,33 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "winapi 0.3.9", + "windows-sys", ] [[package]] name = "http" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "fnv", - "itoa 1.0.1", + "itoa 1.0.9", ] [[package]] @@ -965,20 +1172,20 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "http", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", ] [[package]] name = "httparse" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -988,9 +1195,9 @@ checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1024,23 +1231,23 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", - "h2 0.3.11", + "h2 0.3.21", "http", - "http-body 0.4.4", + "http-body 0.4.5", "httparse", - "httpdate 1.0.2", - "itoa 1.0.1", - "pin-project-lite 0.2.8", - "socket2 0.4.4", - "tokio 1.25.0", + "httpdate 1.0.3", + "itoa 1.0.9", + "pin-project-lite 0.2.13", + "socket2 0.4.9", + "tokio 1.32.0", "tower-service", "tracing", "want", @@ -1052,9 +1259,9 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.18", - "pin-project-lite 0.2.8", - "tokio 1.25.0", + "hyper 0.14.27", + "pin-project-lite 0.2.13", + "tokio 1.32.0", "tokio-io-timeout", ] @@ -1064,51 +1271,50 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.1.0", - "hyper 0.14.18", + "bytes 1.5.0", + "hyper 0.14.27", "native-tls", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-native-tls", ] [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] -name = "instant" -version = "0.1.12" +name = "indexmap" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ - "cfg-if 1.0.0", + "equivalent", + "hashbrown 0.14.0", ] [[package]] -name = "io-lifetimes" -version = "1.0.3" +name = "instant" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "libc", - "windows-sys 0.42.0", + "cfg-if 1.0.0", ] [[package]] @@ -1122,27 +1328,35 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.2.6", - "io-lifetimes", + "hermit-abi 0.3.2", "rustix", - "windows-sys 0.42.0", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] name = "itertools" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -1155,15 +1369,15 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1230,9 +1444,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures 0.2.9", +] [[package]] name = "kernel32-sys" @@ -1249,18 +1466,27 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" -version = "0.2.139" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" -version = "0.23.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", @@ -1298,50 +1524,51 @@ checksum = "2f0170619152c4d6b947d5ed0de427b85691482a293e0cae52d4336a2220a776" dependencies = [ "bitcoin", "lightning", - "tokio 1.25.0", + "tokio 1.32.0", ] [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.16" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] -name = "matches" -version = "0.1.9" +name = "md-5" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] [[package]] name = "memchr" -version = "2.4.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -1359,6 +1586,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.6.23" @@ -1380,14 +1616,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys", ] [[package]] @@ -1404,16 +1639,14 @@ dependencies = [ [[package]] name = "mockito" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa08cccbc31b07113e4322d79df4464e0ed888032fc67ec56136325cd3afecf" +checksum = "406f43768da5a859ce19bb0978fd8dc2167a7d9a52f3935c6a187242e1a4ff9f" dependencies = [ "assert-json-diff", - "async-trait", "colored", - "deadpool", "futures", - "hyper 0.14.18", + "hyper 0.14.27", "lazy_static", "log", "rand 0.8.5", @@ -1421,7 +1654,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "similar", - "tokio 1.25.0", + "tokio 1.32.0", ] [[package]] @@ -1430,7 +1663,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "encoding_rs", "futures-util", "http", @@ -1450,9 +1683,9 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "native-tls" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", @@ -1468,9 +1701,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.37" +version = "0.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" dependencies = [ "cfg-if 0.1.10", "libc", @@ -1479,9 +1712,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -1489,15 +1722,32 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1508,34 +1758,55 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.3.2", "libc", ] [[package]] name = "num_threads" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.2.0" @@ -1547,9 +1818,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -1559,11 +1830,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags", + "bitflags 2.4.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1574,13 +1845,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] @@ -1591,11 +1862,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.74" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -1610,7 +1880,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core 0.8.5", + "parking_lot_core 0.8.6", ] [[package]] @@ -1620,79 +1890,94 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.6", + "parking_lot_core 0.9.8", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi 0.3.9", ] [[package]] name = "parking_lot_core" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.42.0", + "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", ] [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" -version = "0.6.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.0.0", ] [[package]] name = "pin-project" -version = "1.0.10" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] @@ -1703,9 +1988,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1713,11 +1998,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "poly1305" @@ -1725,16 +2031,16 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.9", "opaque-debug", "universal-hash", ] [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" @@ -1745,7 +2051,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -1762,9 +2068,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1775,7 +2081,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "prost-derive 0.8.0", ] @@ -1785,7 +2091,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "prost-derive 0.9.0", ] @@ -1795,9 +2101,9 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.1.0", - "heck", - "itertools", + "bytes 1.5.0", + "heck 0.3.3", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -1816,10 +2122,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1829,10 +2135,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1841,15 +2147,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "prost 0.9.0", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1888,7 +2194,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -1908,7 +2214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -1937,11 +2243,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.5", + "getrandom 0.2.10", ] [[package]] @@ -1977,18 +2283,39 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -1997,9 +2324,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "remove_dir_all" @@ -2012,32 +2339,32 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.13.0", - "bytes 1.1.0", + "base64 0.21.4", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.11", + "h2 0.3.21", "http", - "http-body 0.4.4", - "hyper 0.14.18", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "serde", "serde_json", "serde_urlencoded", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-native-tls", "tokio-socks", "tower-service", @@ -2048,12 +2375,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "retain_mut" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" - [[package]] name = "ring" version = "0.16.20" @@ -2069,21 +2390,48 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature 2.1.0", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" -version = "0.26.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags", + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "memchr", "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2104,16 +2452,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.6" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ - "bitflags", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -2122,7 +2469,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "log", "ring", "sct", @@ -2135,36 +2482,35 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", ] [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys", ] [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -2178,9 +2524,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" dependencies = [ "secp256k1-sys", "serde", @@ -2197,11 +2543,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2210,9 +2556,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -2220,38 +2566,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ - "indexmap", - "itoa 1.0.1", + "indexmap 2.0.0", + "itoa 1.0.9", "ryu", "serde", ] @@ -2263,22 +2609,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.1", + "itoa 1.0.9", "ryu", "serde", ] -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.10.5" @@ -2286,7 +2621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.2.9", "digest 0.10.7", ] @@ -2298,11 +2633,22 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures", + "cpufeatures 0.2.9", "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures 0.2.9", + "digest 0.10.7", +] + [[package]] name = "sha3" version = "0.9.1" @@ -2317,18 +2663,28 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "signature" -version = "1.5.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] [[package]] name = "similar" @@ -2338,9 +2694,9 @@ checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" [[package]] name = "simple_logger" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" +checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7" dependencies = [ "atty", "colored", @@ -2351,15 +2707,18 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -2374,14 +2733,24 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -2393,6 +2762,236 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes 1.5.0", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.7", + "smallvec", + "sqlformat", + "thiserror", + "tokio 1.32.0", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.7", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio 1.32.0", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "bytes 1.5.0", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa 1.0.9", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa 1.0.9", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "strsim" @@ -2417,11 +3016,11 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2432,9 +3031,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2442,15 +3041,14 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "91e02e55d62894af2a08aca894c6577281f76769ba47c94d5756bec8ac6e7373" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2465,16 +3063,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "redox_syscall 0.3.5", + "rustix", + "windows-sys", ] [[package]] @@ -2497,10 +3094,11 @@ dependencies = [ "serde", "serde_json", "simple_logger", + "sqlx", "structopt", "tempdir", "teos-common", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-stream", "toml", "tonic 0.6.2", @@ -2529,9 +3127,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2547,56 +3145,68 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "time" -version = "0.3.7" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ - "itoa 1.0.1", + "deranged", + "itoa 1.0.9", "libc", "num_threads", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + [[package]] name = "time-macros" -version = "0.2.3" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" @@ -2618,22 +3228,21 @@ dependencies = [ [[package]] name = "tokio" -version = "1.25.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", - "bytes 1.1.0", + "backtrace", + "bytes 1.5.0", "libc", - "memchr", - "mio 0.8.4", + "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "signal-hook-registry", - "socket2 0.4.4", + "socket2 0.5.4", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -2642,29 +3251,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.8", - "tokio 1.25.0", + "pin-project-lite 0.2.13", + "tokio 1.32.0", ] [[package]] name = "tokio-macros" -version = "1.7.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.25.0", + "tokio 1.32.0", ] [[package]] @@ -2674,7 +3283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", - "tokio 1.25.0", + "tokio 1.32.0", "webpki", ] @@ -2687,18 +3296,18 @@ dependencies = [ "either", "futures-util", "thiserror", - "tokio 1.25.0", + "tokio 1.32.0", ] [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.8", - "tokio 1.25.0", + "pin-project-lite 0.2.13", + "tokio 1.32.0", ] [[package]] @@ -2709,7 +3318,7 @@ checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" dependencies = [ "futures-util", "log", - "tokio 1.25.0", + "tokio 1.32.0", "tungstenite", ] @@ -2729,37 +3338,37 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.8", - "tokio 1.25.0", + "pin-project-lite 0.2.13", + "tokio 1.32.0", ] [[package]] name = "tokio-util" -version = "0.7.0" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "futures-core", "futures-sink", - "log", - "pin-project-lite 0.2.8", - "tokio 1.25.0", + "pin-project-lite 0.2.13", + "tokio 1.32.0", + "tracing", ] [[package]] name = "toml" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] @@ -2772,23 +3381,23 @@ checksum = "796c5e1cd49905e65dd8e700d4cb1dffcbfdb4fc9d017de08c1a537afd83627c" dependencies = [ "async-stream", "async-trait", - "base64 0.13.0", - "bytes 1.1.0", + "base64 0.13.1", + "bytes 1.5.0", "futures-core", "futures-util", - "h2 0.3.11", + "h2 0.3.21", "http", - "http-body 0.4.4", - "hyper 0.14.18", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", "prost 0.8.0", "prost-derive 0.8.0", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-rustls", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.6.10", "tower", "tower-layer", "tower-service", @@ -2804,23 +3413,23 @@ checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" dependencies = [ "async-stream", "async-trait", - "base64 0.13.0", - "bytes 1.1.0", + "base64 0.13.1", + "bytes 1.5.0", "futures-core", "futures-util", - "h2 0.3.11", + "h2 0.3.21", "http", - "http-body 0.4.4", - "hyper 0.14.18", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", "prost 0.9.0", "prost-derive 0.9.0", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-rustls", "tokio-stream", - "tokio-util 0.6.9", + "tokio-util 0.6.10", "tower", "tower-layer", "tower-service", @@ -2837,7 +3446,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2847,34 +3456,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" dependencies = [ "base32", - "base64 0.13.0", + "base64 0.13.1", "derive_more", "ed25519-dalek", "hex", - "hmac", + "hmac 0.11.0", "rand 0.7.3", "serde", "serde_derive", - "sha2", + "sha2 0.9.9", "sha3", - "tokio 1.25.0", + "tokio 1.32.0", ] [[package]] name = "tower" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "rand 0.8.5", "slab", - "tokio 1.25.0", - "tokio-util 0.7.0", + "tokio 1.32.0", + "tokio-util 0.7.8", "tower-layer", "tower-service", "tracing", @@ -2882,47 +3491,47 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.31" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.8", + "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.19" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", ] [[package]] name = "tracing-core" -version = "0.1.22" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -2943,9 +3552,9 @@ checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" @@ -2953,9 +3562,9 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "byteorder", - "bytes 1.1.0", + "bytes 1.5.0", "http", "httparse", "log", @@ -2968,57 +3577,57 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode_categories" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "universal-hash" @@ -3038,13 +3647,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -3074,11 +3682,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -3088,12 +3695,12 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" dependencies = [ - "bytes 1.1.0", + "bytes 1.5.0", "futures-channel", "futures-util", "headers", "http", - "hyper 0.14.18", + "hyper 0.14.27", "log", "mime", "mime_guess", @@ -3105,10 +3712,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio 1.25.0", + "tokio 1.32.0", "tokio-stream", "tokio-tungstenite", - "tokio-util 0.7.0", + "tokio-util 0.7.8", "tower-service", "tracing", ] @@ -3119,12 +3726,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3133,9 +3734,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3143,24 +3744,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.36", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3170,9 +3771,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3180,22 +3781,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.36", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "watchtower-plugin" @@ -3214,15 +3815,15 @@ dependencies = [ "serde_json", "tempdir", "teos-common", - "tokio 1.25.0", + "tokio 1.32.0", "tonic 0.5.2", ] [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -3240,15 +3841,22 @@ dependencies = [ [[package]] name = "which" -version = "4.2.4" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "lazy_static", - "libc", + "home", + "once_cell", + "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.2.8" @@ -3294,111 +3902,78 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys", ] [[package]] @@ -3417,7 +3992,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc90836a84cb72e6934137b1504d0cae304ef5d83904beb0c8d773bbfe256ed" dependencies = [ - "base64 0.13.0", + "base64 0.13.1", "chrono", "data-encoding", "der-parser", @@ -3440,21 +4015,20 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.36", ] diff --git a/teos-common/Cargo.toml b/teos-common/Cargo.toml index be0cb489..83c12817 100644 --- a/teos-common/Cargo.toml +++ b/teos-common/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" # General hex = { version = "0.4.3", features = [ "serde" ] } prost = "0.9" -rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } +rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = "1.0" tonic = "0.6" diff --git a/teos/Cargo.toml b/teos/Cargo.toml index f01ab5ca..18c0250a 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -21,7 +21,8 @@ home = "0.5.3" log = "0.4" prost = "0.9" rcgen = { version = "0.8", features = ["pem", "x509-parser"] } -rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } +rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } +sqlx = { version = "0.7", features = ["runtime-tokio", "tls-native-tls"] } serde = "1.0.130" serde_json = "1.0" simple_logger = "2.1.0" diff --git a/watchtower-plugin/Cargo.toml b/watchtower-plugin/Cargo.toml index 976f8e9d..6e02d8e9 100755 --- a/watchtower-plugin/Cargo.toml +++ b/watchtower-plugin/Cargo.toml @@ -18,7 +18,7 @@ hex = { version = "0.4.3", features = [ "serde" ] } home = "0.5.3" reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] } log = "0.4.16" -rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } +rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = { version = "1.0", features = [ "preserve_order" ] } tonic = { version = "^0.5", features = [ "tls", "transport" ] } From b5ba42bcaced892ab326017c86816574410e68b7 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 5 Aug 2023 09:27:18 +0300 Subject: [PATCH 04/15] Implementing and using the sqlx drived DBM The two DBMs are now in use, this is an intermediary commit and `dbm.rs` should be deleted and replaced with `dbm_new.rs`, and the tower components should be adjusted accordingly (drop the mutex around the dbm since the pool handles it by itself & await on dbm methods since they are now async) --- Cargo.lock | 1 + teos/Cargo.toml | 8 +- teos/build.rs | 9 +- teos/migrations/README.md | 10 + teos/migrations/postgres/000_init.sql | 51 + teos/migrations/sqlite/000_init.sql | 40 + .../sqlite/001_datatypes_correction.sql | 51 + .../sqlite/002_more_datatypes_corrections.sql | 17 + teos/migrations/sqlite/003_keys_rename.sql | 2 + teos/migrations/sqlite/004_locator_index.sql | 5 + .../{listener_actor.rs => async_listener.rs} | 30 +- teos/src/chain_monitor.rs | 30 +- teos/src/config.rs | 16 +- teos/src/dbm.rs | 14 +- teos/src/dbm_new.rs | 1590 +++++++++++++++++ teos/src/gatekeeper.rs | 80 +- teos/src/lib.rs | 3 +- teos/src/main.rs | 35 +- teos/src/responder.rs | 54 +- teos/src/watcher.rs | 79 +- 20 files changed, 1939 insertions(+), 186 deletions(-) create mode 100644 teos/migrations/README.md create mode 100644 teos/migrations/postgres/000_init.sql create mode 100644 teos/migrations/sqlite/000_init.sql create mode 100644 teos/migrations/sqlite/001_datatypes_correction.sql create mode 100644 teos/migrations/sqlite/002_more_datatypes_corrections.sql create mode 100644 teos/migrations/sqlite/003_keys_rename.sql create mode 100644 teos/migrations/sqlite/004_locator_index.sql rename teos/src/{listener_actor.rs => async_listener.rs} (71%) create mode 100644 teos/src/dbm_new.rs diff --git a/Cargo.lock b/Cargo.lock index f098e808..82714f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2872,6 +2872,7 @@ dependencies = [ "sha2 0.10.7", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", "syn 1.0.109", "tempfile", diff --git a/teos/Cargo.toml b/teos/Cargo.toml index 18c0250a..0348ec79 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -14,6 +14,12 @@ path = "src/cli.rs" name = "teosd" path = "src/main.rs" +[features] +# By default, enable both SQLite snd PostgreSQL in the output binary. +default = ["sqlite", "postgres"] +sqlite = ["sqlx/sqlite"] +postgres = ["sqlx/postgres"] + [dependencies] # General hex = { version = "0.4.3", features = [ "serde" ] } @@ -22,7 +28,7 @@ log = "0.4" prost = "0.9" rcgen = { version = "0.8", features = ["pem", "x509-parser"] } rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } -sqlx = { version = "0.7", features = ["runtime-tokio", "tls-native-tls"] } +sqlx = { version = "0.7", features = ["runtime-tokio", "tls-native-tls", "migrate", "any"] } serde = "1.0.130" serde_json = "1.0" simple_logger = "2.1.0" diff --git a/teos/build.rs b/teos/build.rs index aa9031fd..6fd333fa 100644 --- a/teos/build.rs +++ b/teos/build.rs @@ -1,4 +1,6 @@ -fn main() -> Result<(), Box> { +fn main() { + // trigger recompilation when a new migration is added without a change in the source code. + println!("cargo:rerun-if-changed=migrations"); tonic_build::configure() .extern_path(".common.teos.v2", "::teos-common::protos") .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") @@ -23,7 +25,6 @@ fn main() -> Result<(), Box> { "proto/teos/v2/user.proto", ], &["proto/teos/v2", "../teos-common/proto/"], - )?; - - Ok(()) + ) + .unwrap(); } diff --git a/teos/migrations/README.md b/teos/migrations/README.md new file mode 100644 index 00000000..4bbc7fa9 --- /dev/null +++ b/teos/migrations/README.md @@ -0,0 +1,10 @@ +# Structure: +- `postgres`: Contains `.sql` migrations for postgres databases. +- `sqlite`: Contains `.sql` migrations for sqlite databases. + + +# Migrations Extra Documentation (`migrations/*/*.md`): + +Migrations cannot be edited once applied to the database. Thus, writing/editing any comments or explanations in the `.sql` files would break the tower for users who have applied those migrations. + +Any additional comments that we need to add after a migration has been applied should be in `MID_MNAME.md` instead. diff --git a/teos/migrations/postgres/000_init.sql b/teos/migrations/postgres/000_init.sql new file mode 100644 index 00000000..5da33a11 --- /dev/null +++ b/teos/migrations/postgres/000_init.sql @@ -0,0 +1,51 @@ +-- INT is 4 bytes signed integer (i32) in PostgreSQL. +-- Many fields in here map to (u32)s in Rust, which has double the capacity of (i32)s on the positive side. +-- Some database calls might break because of this. +-- A solution for this could be either one of: +-- 1- Find a one to one mapping between the Rust (u32)s and PostgreSQL's (i32)s since they are essentially the same size. +-- 2- Use PostgreSQL's BIGINT which is equivalent to an i64. + +CREATE TABLE IF NOT EXISTS users ( + user_id BYTEA PRIMARY KEY, + available_slots BIGINT NOT NULL, + subscription_start BIGINT NOT NULL, + subscription_expiry BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS appointments ( + UUID BYTEA PRIMARY KEY, + locator BYTEA NOT NULL, + encrypted_blob BYTEA NOT NULL, + to_self_delay BIGINT NOT NULL, + user_signature TEXT NOT NULL, + start_block BIGINT NOT NULL, + user_id BYTEA NOT NULL, + FOREIGN KEY(user_id) + REFERENCES users(user_id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS trackers ( + UUID BYTEA PRIMARY KEY, + dispute_tx BYTEA NOT NULL, + penalty_tx BYTEA NOT NULL, + height BIGINT NOT NULL, + confirmed BIGINT NOT NULL, + FOREIGN KEY(UUID) + REFERENCES appointments(UUID) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS last_known_block ( + id INT PRIMARY KEY, + block_hash BYTEA NOT NULL +); + +CREATE TABLE IF NOT EXISTS keys ( + id SERIAL PRIMARY KEY, + secret_key TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS locators_index ON appointments ( + locator +); diff --git a/teos/migrations/sqlite/000_init.sql b/teos/migrations/sqlite/000_init.sql new file mode 100644 index 00000000..a9ef373d --- /dev/null +++ b/teos/migrations/sqlite/000_init.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS users ( + user_id INT PRIMARY KEY, + available_slots INT NOT NULL, + subscription_start INT NOT NULL, + subscription_expiry INT NOT NULL +); + +CREATE TABLE IF NOT EXISTS appointments ( + UUID INT PRIMARY KEY, + locator INT NOT NULL, + encrypted_blob BLOB NOT NULL, + to_self_delay INT NOT NULL, + user_signature BLOB NOT NULL, + start_block INT NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY(user_id) + REFERENCES users(user_id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS trackers ( + UUID INT PRIMARY KEY, + dispute_tx BLOB NOT NULL, + penalty_tx BLOB NOT NULL, + height INT NOT NULL, + confirmed BOOL NOT NULL, + FOREIGN KEY(UUID) + REFERENCES appointments(UUID) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS last_known_block ( + id INT PRIMARY KEY, + block_hash INT NOT NULL +); + +CREATE TABLE IF NOT EXISTS keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key INT NOT NULL +); diff --git a/teos/migrations/sqlite/001_datatypes_correction.sql b/teos/migrations/sqlite/001_datatypes_correction.sql new file mode 100644 index 00000000..3bee4f47 --- /dev/null +++ b/teos/migrations/sqlite/001_datatypes_correction.sql @@ -0,0 +1,51 @@ +-- Change `user_id` from INT to BLOB. +CREATE TABLE tmp_users ( + user_id BLOB PRIMARY KEY, + available_slots INT NOT NULL, + subscription_start INT NOT NULL, + subscription_expiry INT NOT NULL +); +INSERT INTO tmp_users SELECT * FROM users; +-- We couldn't drop `users` before copying `appointments`, as the former will cascade delete the latter. +-- Same for `trackers` and `appointments`. +-- DROP TABLE users; + +-- Change `UUID` & `locator` & `user_id` from INT to BLOB. +-- Change `user_signature` from BLOB to TEXT. +CREATE TABLE tmp_appointments ( + UUID BLOB PRIMARY KEY, + locator BLOB NOT NULL, + encrypted_blob BLOB NOT NULL, + to_self_delay INT NOT NULL, + user_signature TEXT NOT NULL, + start_block INT NOT NULL, + user_id BLOB NOT NULL, + FOREIGN KEY(user_id) + REFERENCES tmp_users(user_id) + ON DELETE CASCADE +); +INSERT INTO tmp_appointments SELECT * FROM appointments; + +-- Change `UUID` from INT to BLOB. +-- Change `confirmed` from BOOL to INT (due to https://github.com/launchbadge/sqlx/issues/2657). +CREATE TABLE tmp_trackers ( + UUID BLOB PRIMARY KEY, + dispute_tx BLOB NOT NULL, + penalty_tx BLOB NOT NULL, + height INT NOT NULL, + confirmed INT NOT NULL, + FOREIGN KEY(UUID) + REFERENCES tmp_appointments(UUID) + ON DELETE CASCADE +); +INSERT INTO tmp_trackers SELECT * FROM trackers; + +-- We can drop these now after all the data has been copied. +DROP TABLE users; +DROP TABLE appointments; +DROP TABLE trackers; + +-- Foreign key references are automatically adjusted (tmp_* -> *). +ALTER TABLE tmp_users RENAME TO users; +ALTER TABLE tmp_appointments RENAME TO appointments; +ALTER TABLE tmp_trackers RENAME TO trackers; diff --git a/teos/migrations/sqlite/002_more_datatypes_corrections.sql b/teos/migrations/sqlite/002_more_datatypes_corrections.sql new file mode 100644 index 00000000..0076dddb --- /dev/null +++ b/teos/migrations/sqlite/002_more_datatypes_corrections.sql @@ -0,0 +1,17 @@ +-- Change `block_hash` from INT to BLOB. +CREATE TABLE tmp_last_known_block ( + id INT PRIMARY KEY, + block_hash BLOB NOT NULL +); +INSERT INTO tmp_last_known_block SELECT * FROM last_known_block; +DROP TABLE last_known_block; +ALTER TABLE tmp_last_known_block RENAME TO last_known_block; + +-- Change `key` from INT to TEXT. +CREATE TABLE tmp_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL +); +INSERT INTO tmp_keys SELECT * FROM keys; +DROP TABLE keys; +ALTER TABLE tmp_keys RENAME TO keys; diff --git a/teos/migrations/sqlite/003_keys_rename.sql b/teos/migrations/sqlite/003_keys_rename.sql new file mode 100644 index 00000000..b7943ed7 --- /dev/null +++ b/teos/migrations/sqlite/003_keys_rename.sql @@ -0,0 +1,2 @@ +-- Rename `key` to `secret_key` as the word `key` is reserved in some databases. +ALTER TABLE keys RENAME key TO secret_key; diff --git a/teos/migrations/sqlite/004_locator_index.sql b/teos/migrations/sqlite/004_locator_index.sql new file mode 100644 index 00000000..c9d3dcf0 --- /dev/null +++ b/teos/migrations/sqlite/004_locator_index.sql @@ -0,0 +1,5 @@ +-- This index greatly enhances the performance of locator based selection queries: +-- "SELECT ... FROM appointments WHERE locator = ..." +CREATE INDEX IF NOT EXISTS locators_index ON appointments ( + locator +); diff --git a/teos/src/listener_actor.rs b/teos/src/async_listener.rs similarity index 71% rename from teos/src/listener_actor.rs rename to teos/src/async_listener.rs index 64fcf1bd..de0006ff 100644 --- a/teos/src/listener_actor.rs +++ b/teos/src/async_listener.rs @@ -1,3 +1,6 @@ +//! Contains the [AsyncListen] trait that's analogous to the [chain::Listen] from LDK but runs +//! inside an asynchronous context. + use crate::dbm::DBM; use std::marker::{Send, Sync}; @@ -7,6 +10,7 @@ use bitcoin::{Block, BlockHeader}; use lightning::chain; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +/// A trait similar to LDK's [chain::Listen] but runs asynchronously. #[tonic::async_trait] pub trait AsyncListen: Send + Sync { async fn block_connected(&self, block: &Block, height: u32); @@ -43,24 +47,31 @@ enum BlockListenerAction { BlockDisconnected(BlockHeader, u32), } -pub struct AsyncBlockListener { +/// A helper struct that wraps a listener that implements [AsyncListen] and feeds it connected and disconnected +/// blocks received from [UnboundedReceiver] in the background. +pub struct AsyncBlockListener { listener: L, dbm: Arc>, rx: UnboundedReceiver, } -impl AsyncBlockListener -where - L: AsyncListen + 'static, -{ - pub fn new(listener: L, dbm: Arc>) -> SyncBlockListener { +impl AsyncBlockListener { + /// Takes a `listener` that implements [AsyncListen] and returns a listener that implements [chain::Listen]. + /// + /// These two listeners are connected. That is, blocks connected-to/disconnected-from the [chain::Listen] + /// listener are forwarded to the [AsyncListen] listener. + /// + /// The [AsyncListen] listener will be actively listening for actions in a background tokio task. + pub fn wrap_listener(listener: L, dbm: Arc>) -> SyncBlockListener { let (tx, rx) = unbounded_channel(); let actor = AsyncBlockListener { listener, dbm, rx }; actor.run_actor_in_bg(); SyncBlockListener { tx } } - fn run_actor_in_bg(mut self: Self) { + /// Spawns a forever living task that listens for [BlockListenerAction] and feeds them to the + /// listener in an asynchronous context. + fn run_actor_in_bg(mut self) { tokio::spawn(async move { while let Some(action) = self.rx.recv().await { match action { @@ -82,7 +93,8 @@ where } } -#[derive(Debug)] +/// A block listener that implements the sync [chain::Listen] trait. All it does is forward the blocks received +/// another (async) block listener through an [UnboundedSender]. pub struct SyncBlockListener { tx: UnboundedSender, } @@ -107,7 +119,7 @@ impl chain::Listen for SyncBlockListener { height: u32, ) { let block = Block { - header: header.clone(), + header: *header, txdata: txdata.iter().map(|&(_, tx)| tx.clone()).collect(), }; self.tx diff --git a/teos/src/chain_monitor.rs b/teos/src/chain_monitor.rs index 9bb6c10c..4b255249 100644 --- a/teos/src/chain_monitor.rs +++ b/teos/src/chain_monitor.rs @@ -199,14 +199,8 @@ mod tests { let spv_client = SpvClient::new(old_tip, poller, cache, &listener); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let mut cm = ChainMonitor::new( - spv_client, - old_tip, - 1, - shutdown_signal, - bitcoind_reachable, - ) - .await; + let mut cm = + ChainMonitor::new(spv_client, old_tip, 1, shutdown_signal, bitcoind_reachable).await; // If a new (best) block gets mined, it should be connected cm.poll_best_tip().await; @@ -236,14 +230,8 @@ mod tests { let spv_client = SpvClient::new(best_tip, poller, cache, &listener); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let mut cm = ChainMonitor::new( - spv_client, - best_tip, - 1, - shutdown_signal, - bitcoind_reachable, - ) - .await; + let mut cm = + ChainMonitor::new(spv_client, best_tip, 1, shutdown_signal, bitcoind_reachable).await; // If a new (worse, just one) block gets mined, nothing gets connected nor disconnected cm.poll_best_tip().await; @@ -272,14 +260,8 @@ mod tests { let spv_client = SpvClient::new(old_best, poller, cache, &listener); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let mut cm = ChainMonitor::new( - spv_client, - old_best, - 1, - shutdown_signal, - bitcoind_reachable, - ) - .await; + let mut cm = + ChainMonitor::new(spv_client, old_best, 1, shutdown_signal, bitcoind_reachable).await; // If a a reorg is found (tip is disconnected and a new best is found), both data should be connected and disconnected cm.poll_best_tip().await; diff --git a/teos/src/config.rs b/teos/src/config.rs index 5436725f..6597dbc4 100644 --- a/teos/src/config.rs +++ b/teos/src/config.rs @@ -86,6 +86,10 @@ pub struct Opt { #[structopt(long, default_value = "~/.teos")] pub data_dir: String, + /// Database connection string [default: managed (managed SQLite database)] + #[structopt(long)] + pub database_url: Option, + /// Runs teos in debug mode #[structopt(long)] pub debug: bool, @@ -140,6 +144,9 @@ pub struct Config { pub btc_rpc_connect: String, pub btc_rpc_port: u16, + // Database + pub database_url: String, + // Flags pub debug: bool, pub deps_debug: bool, @@ -193,6 +200,9 @@ impl Config { if options.btc_rpc_port.is_some() { self.btc_rpc_port = options.btc_rpc_port.unwrap(); } + if options.database_url.is_some() { + self.database_url = options.database_url.unwrap(); + } if options.tor_control_port.is_some() { self.tor_control_port = options.tor_control_port.unwrap(); } @@ -254,7 +264,7 @@ impl Config { pub fn log_non_default_options(&self) { let json_default_config = serde_json::json!(&Config::default()); let json_config = serde_json::json!(&self); - let sensitive_args = ["btc_rpc_user", "btc_rpc_password"]; + let sensitive_args = ["btc_rpc_user", "btc_rpc_password", "database_url"]; for (key, value) in json_config.as_object().unwrap().iter() { if *value != json_default_config[key] { @@ -293,7 +303,7 @@ impl Default for Config { btc_rpc_password: String::new(), btc_rpc_connect: "localhost".into(), btc_rpc_port: 0, - + database_url: "managed".into(), debug: false, deps_debug: false, overwrite_key: false, @@ -329,7 +339,7 @@ mod tests { btc_rpc_connect: None, btc_rpc_port: None, data_dir: String::from("~/.teos"), - + database_url: None, debug: false, deps_debug: false, overwrite_key: false, diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 05374224..5ecd9768 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -84,17 +84,9 @@ pub struct DBM { } impl DBM { - fn get_connection(&self) -> &Connection { - &self.connection - } - - fn get_mut_connection(&mut self) -> &mut Connection { - &mut self.connection - } - /// Creates the database tables if not present. fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); + let tx = self.connection.transaction().unwrap(); for table in tables.iter() { tx.execute(table, [])?; } @@ -103,7 +95,7 @@ impl DBM { /// Generic method to store data into the database. fn store_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params) { + match self.connection.execute(query, params) { Ok(_) => Ok(()), Err(e) => match e { SqliteError::SqliteFailure(ie, _) => match ie.code { @@ -121,7 +113,7 @@ impl DBM { /// Generic method to remove data from the database. fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params).unwrap() { + match self.connection.execute(query, params).unwrap() { 0 => Err(Error::NotFound), _ => Ok(()), } diff --git a/teos/src/dbm_new.rs b/teos/src/dbm_new.rs new file mode 100644 index 00000000..d7300b45 --- /dev/null +++ b/teos/src/dbm_new.rs @@ -0,0 +1,1590 @@ +//! Logic related to the tower database manager (DBM), component in charge of persisting data on disk. +#![allow(dead_code)] + +use std::collections::HashMap; +use std::str::FromStr; + +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::SecretKey; +use bitcoin::{consensus, BlockHash, Txid}; +use sqlx::any::{install_drivers, AnyRow}; +use sqlx::{AnyPool, Row}; + +use teos_common::appointment::{Appointment, Locator}; +use teos_common::UserId; + +use crate::extended_appointment::{ExtendedAppointment, UUID}; +use crate::gatekeeper::UserInfo; +use crate::responder::{ConfirmationStatus, TransactionTracker}; +use crate::watcher::Breach; + +type Error = sqlx::Error; + +#[cfg(not(test))] +/// The maximum number of bind variables per SQL query. +/// `32766` for SQLite & `65535` for PostgreSQL. Picking `32766` for both since it's big enough already. +const SQL_VARIABLE_LIMIT: usize = 32766; +#[cfg(test)] +/// The maximum number of bind variables per SQL query. Set to `10` for testing. +const SQL_VARIABLE_LIMIT: usize = 10; + +/// Checks if the database type (`$db_type`) matches with the connection string (`$connection_string`). +/// If the connection string matches the database type, this macro does: +/// - Try to install the driver for the passed database type. +/// - Try to connect to the database. +/// - Try to run the migrations specified in `$migrations_path`. +/// - Return [Ok(DBM)] if everything succeeds, [Err(String)] otherwise explaining the error. +macro_rules! return_db_if_matching { + ($connection_string:expr, $db_type:tt, $migrations_path:literal) => { + if $connection_string.starts_with(concat!(stringify!($db_type), ":")) { + install_drivers(&[sqlx::$db_type::any::DRIVER]) + // Just log the warning but try to connect anyways. `install_drivers` might fail if the driver is already installed. + .map_err(|e| log::error!("Failed to install database drivers for {}: {e}", stringify!($db_type))) + .ok(); + let pool = AnyPool::connect($connection_string) + .await + .map_err(|e| format!("Couldn't connect to {}: {e}", $connection_string))?; + sqlx::migrate!($migrations_path) + .run(&pool) + .await + .map_err(|e| format!("Failed to run database migrations: {e}"))?; + return Ok(Self { pool }); + } + } +} + +/// Component in charge of interacting with the underlying database. +#[derive(Debug)] +pub struct DBM { + pool: AnyPool, +} + +impl DBM { + /// Creates a new [DBM] instance. + pub async fn new(connection_string: &str) -> Result { + #[cfg(not(any(feature = "sqlite", feature = "postgres")))] + compile_error!("Can't compile with no database drivers."); + + // Note that sqlx initiates `PRAGMA foreign_keys = ON` connections by default. + #[cfg(feature = "sqlite")] + return_db_if_matching!(connection_string, sqlite, "migrations/sqlite"); + + #[cfg(feature = "postgres")] + return_db_if_matching!(connection_string, postgres, "migrations/postgres"); + + let supported_dbs = vec![ + #[cfg(feature = "sqlite")] + "SQLite", + #[cfg(feature = "postgres")] + "PostgreSQL", + ]; + + Err(format!( + "The database connection string ({connection_string}) is invalid or isn't supported. Supported databases are: {supported_dbs:?}." + )) + } + + /// Stores a user ([UserInfo]) into the database. + pub(crate) async fn store_user( + &self, + user_id: UserId, + user_info: &UserInfo, + ) -> Result<(), Error> { + let sql = "INSERT INTO users (user_id, available_slots, subscription_start, subscription_expiry) VALUES ($1, $2, $3, $4)"; + sqlx::query(sql) + .bind(user_id.to_vec()) + .bind(user_info.available_slots as i64) + .bind(user_info.subscription_start as i64) + .bind(user_info.subscription_expiry as i64) + .execute(&self.pool) + .await + .map(|_| ()) + } + + /// Updates an existing user ([UserInfo]) in the database. + pub(crate) async fn update_user( + &self, + user_id: UserId, + user_info: &UserInfo, + ) -> Result<(), Error> { + let sql = "UPDATE users SET available_slots=($1), subscription_start=($2), subscription_expiry=($3) WHERE user_id=($4)"; + let updated_rows = sqlx::query(sql) + .bind(user_info.available_slots as i64) + .bind(user_info.subscription_start as i64) + .bind(user_info.subscription_expiry as i64) + .bind(user_id.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); + + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) + } + + /// Loads the associated locators ([Locator]) of a given user ([UserId]). + pub(crate) async fn load_user_locators(&self, user_id: UserId) -> Vec { + sqlx::query("SELECT locator FROM appointments WHERE user_id=($1)") + .bind(user_id.to_vec()) + .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap() + } + + /// Loads all users from the database. + pub(crate) async fn load_all_users(&self) -> HashMap { + sqlx::query( + "SELECT user_id, available_slots, subscription_start, subscription_expiry FROM users", + ) + .map(|row: AnyRow| { + ( + UserId::from_slice(row.get("user_id")).unwrap(), + UserInfo::new( + row.get::("available_slots") as u32, + row.get::("subscription_start") as u32, + row.get::("subscription_expiry") as u32, + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() + } + + /// Removes some users from the database in batch. + pub(crate) async fn batch_remove_users(&self, users: &Vec) -> usize { + let users: Vec<_> = users.iter().map(|uuid| uuid.to_vec()).collect(); + + for chunk in users.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("DELETE FROM users WHERE user_id IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for user_id in chunk { + query = query.bind(user_id); + } + query.execute(&self.pool).await.unwrap(); + } + + (users.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize + } + + /// Get the number of stored appointments. + pub(crate) async fn get_appointments_count(&self) -> usize { + let sql = "SELECT COUNT(*) FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL"; + sqlx::query(sql) + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await + .unwrap() + } + + /// Get the number of stored trackers. + pub(crate) async fn get_trackers_count(&self) -> usize { + sqlx::query("SELECT COUNT(*) FROM trackers") + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await + .unwrap() + } + + /// Stores an [Appointment] into the database. + pub(crate) async fn store_appointment( + &self, + uuid: UUID, + appointment: &ExtendedAppointment, + ) -> Result<(), Error> { + let sql = "INSERT INTO appointments (UUID, locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .bind(appointment.locator().to_vec()) + .bind(appointment.encrypted_blob()) + .bind(appointment.to_self_delay() as i64) + .bind(&appointment.user_signature) + .bind(appointment.start_block as i64) + .bind(appointment.user_id.to_vec()) + .execute(&self.pool) + .await + .map(|_| ()) + } + + /// Updates an existing [Appointment] in the database. + pub(crate) async fn update_appointment( + &self, + uuid: UUID, + appointment: &ExtendedAppointment, + ) -> Result<(), Error> { + // DISCUSS: Check what fields we'd like to make updatable. e_blob and signature are the obvious, to_self_delay and start_block may not be necessary (or even risky) + let sql = "UPDATE appointments SET encrypted_blob=($1), to_self_delay=($2), user_signature=($3), start_block=($4) WHERE UUID=($5)"; + let updated_rows = sqlx::query(sql) + .bind(appointment.encrypted_blob()) + .bind(appointment.to_self_delay() as i64) + .bind(&appointment.user_signature) + .bind(appointment.start_block as i64) + .bind(uuid.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); + + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) + } + + /// Loads an [Appointment] from the database. + pub(crate) async fn load_appointment(&self, uuid: UUID) -> Option { + let sql = "SELECT locator, encrypted_blob, to_self_delay, user_id, user_signature, start_block FROM appointments WHERE UUID=($1)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + ExtendedAppointment::new( + Appointment::new( + Locator::from_slice(row.get("locator")).unwrap(), + row.get("encrypted_blob"), + row.get::("to_self_delay") as u32, + ), + UserId::from_slice(row.get("user_id")).unwrap(), + row.get("user_signature"), + row.get::("start_block") as u32, + ) + }) + .fetch_one(&self.pool) + .await + .ok() + } + + /// Check if an appointment with `uuid` exists. + pub(crate) async fn appointment_exists(&self, uuid: UUID) -> bool { + sqlx::query("SELECT UUID FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .fetch_one(&self.pool) + .await + .is_ok() + } + + /// Gets the length of an appointment (the length of `appointment.encrypted_blob`). + pub(crate) async fn get_appointment_length(&self, uuid: UUID) -> Result { + sqlx::query("SELECT length(encrypted_blob) FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await + } + + /// Gets the [`UserId`] of the owner of the appointment along with the appointment + /// length (same as [DBM::get_appointment_length]) for `uuid`. + pub(crate) async fn get_appointment_user_and_length( + &self, + uuid: UUID, + ) -> Result<(UserId, usize), Error> { + sqlx::query("SELECT user_id, length(encrypted_blob) FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + ( + UserId::from_slice(row.get("user_id")).unwrap(), + row.get::(1) as usize, + ) + }) + .fetch_one(&self.pool) + .await + } + + /// Loads appointments from the database. If a locator is given, this method loads only the appointments + /// matching this locator. If no locator is given, all the appointments in the database would be returned. + pub(crate) async fn load_appointments( + &self, + locator: Option, + ) -> HashMap { + let mut sql = "SELECT a.UUID, a.locator, a.encrypted_blob, a.to_self_delay, a.user_id, a.user_signature, a.start_block FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL".to_string(); + + // If a locator was passed, filter based on it. + let query = if let Some(locator) = locator { + sql.push_str(" AND a.locator=($1)"); + sqlx::query(&sql).bind(locator.to_vec()) + } else { + sqlx::query(&sql) + }; + + query + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + ExtendedAppointment::new( + Appointment::new( + Locator::from_slice(row.get(1)).unwrap(), + row.get(2), + row.get::(3) as u32, + ), + UserId::from_slice(row.get(4)).unwrap(), + row.get(5), + row.get::(6) as u32, + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() + } + + /// Removes an [Appointment] from the database. + pub(crate) async fn remove_appointment(&self, uuid: UUID) { + if let Err(e) = sqlx::query("DELETE FROM appointments WHERE UUID=($1)") + .execute(&self.pool) + .await + { + log::error!("Failed to remove appointment ({uuid}) due to: {e}") + } + } + + /// Removes some appointments from the database in batch and updates the associated users + /// (giving back freed appointment slots) in one transaction so that the deletion and the + /// update is atomic. + pub(crate) async fn batch_remove_appointments( + &self, + appointments: &Vec, + updated_users: &HashMap, + ) -> usize { + let uuids: Vec<_> = appointments.iter().map(|uuid| uuid.to_vec()).collect(); + let mut tx = self.pool.begin().await.unwrap(); + + for chunk in uuids.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("DELETE FROM appointments WHERE UUID IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for uuid in chunk { + query = query.bind(uuid); + } + query.execute(&mut *tx).await.unwrap(); + } + + for (user_id, info) in updated_users.iter() { + sqlx::query("UPDATE users SET available_slots=($1) WHERE user_id=($2)") + .bind(info.available_slots as i64) + .bind(user_id.to_vec()) + .execute(&mut *tx) + .await + .unwrap(); + } + + if let Err(e) = tx.commit().await { + log::error!("Failed to remove appointments in batch: {e}") + } + + (uuids.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize + } + + /// Loads the [`UUID`]s of appointments triggered by `locator`. + pub(crate) async fn load_uuids(&self, locator: Locator) -> Vec { + sqlx::query("SELECT UUID from appointments WHERE locator=($1)") + .bind(locator.to_vec()) + .map(|row: AnyRow| UUID::from_slice(row.get("UUID")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap() + } + + /// Filters the given set of [`Locator`]s by including only the ones which trigger any of our stored appointments. + pub(crate) async fn batch_check_locators_exist(&self, locators: Vec<&Locator>) -> Vec { + let mut registered_locators = Vec::new(); + let locators: Vec<_> = locators.iter().map(|l| l.to_vec()).collect(); + + for chunk in locators.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("SELECT locator FROM appointments WHERE locator IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for locator in chunk { + query = query.bind(locator); + } + registered_locators.extend( + query + .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap(), + ) + } + + registered_locators + } + + /// Stores a [TransactionTracker] into the database. + pub(crate) async fn store_tracker( + &self, + uuid: UUID, + tracker: &TransactionTracker, + ) -> Result<(), Error> { + let (height, confirmed) = tracker + .status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; + let sql = "INSERT INTO trackers (UUID, dispute_tx, penalty_tx, height, confirmed) VALUES ($1, $2, $3, $4, $5)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .bind(consensus::serialize(&tracker.dispute_tx)) + .bind(consensus::serialize(&tracker.penalty_tx)) + .bind(height as i64) + .bind(confirmed as i64) + .execute(&self.pool) + .await + .map(|_| ()) + } + + /// Updates the tracker information in the database. + /// + /// The only updatable fields are `height` and `confirmed`. + pub(crate) async fn update_tracker_status( + &self, + uuid: UUID, + status: &ConfirmationStatus, + ) -> Result<(), Error> { + let (height, confirmed) = status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; + let updated_rows = + sqlx::query("UPDATE trackers SET height=($1), confirmed=($2) WHERE UUID=($3)") + .bind(height as i64) + .bind(confirmed as i64) + .bind(uuid.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); + + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) + } + + /// Loads a [TransactionTracker] from the database. + pub(crate) async fn load_tracker(&self, uuid: UUID) -> Option { + let sql = "SELECT t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=($1)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + TransactionTracker::new( + Breach::new( + consensus::deserialize(row.get(0)).unwrap(), + consensus::deserialize(row.get(1)).unwrap(), + ), + UserId::from_slice(row.get(2)).unwrap(), + ConfirmationStatus::from_db_data( + row.get::(3) as u32, + row.get::(4) != 0, + ), + ) + }) + .fetch_one(&self.pool) + .await + .ok() + } + + /// Check if a tracker with `uuid` exists. + pub(crate) async fn tracker_exists(&self, uuid: UUID) -> bool { + sqlx::query("SELECT UUID FROM trackers WHERE UUID=($1)") + .bind(uuid.to_vec()) + .fetch_one(&self.pool) + .await + .is_ok() + } + + /// Loads trackers from the database. If a locator is given, this method loads only the trackers + /// matching this locator. If no locator is given, all the trackers in the database would be returned. + pub(crate) async fn load_trackers( + &self, + locator: Option, + ) -> HashMap { + let mut sql = "SELECT t.UUID, t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID".to_string(); + + // If a locator was passed, filter based on it. + let query = if let Some(locator) = locator { + sql.push_str(" WHERE a.locator=($1)"); + sqlx::query(&sql).bind(locator.to_vec()) + } else { + sqlx::query(&sql) + }; + + query + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + TransactionTracker::new( + Breach::new( + consensus::deserialize(row.get(1)).unwrap(), + consensus::deserialize(row.get(2)).unwrap(), + ), + UserId::from_slice(row.get(3)).unwrap(), + ConfirmationStatus::from_db_data( + row.get::(4) as u32, + row.get::(5) != 0, + ), + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() + } + + /// Loads trackers with the given confirmation status. + /// + /// Note that for [`ConfirmationStatus::InMempoolSince(height)`] variant, this pulls trackers + /// with `h <= height` and not just `h = height`. + pub(crate) async fn load_trackers_with_confirmation_status( + &self, + status: ConfirmationStatus, + ) -> Result, Error> { + let (height, confirmed) = status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; + + sqlx::query(&format!( + "SELECT UUID FROM trackers WHERE confirmed=($1) AND height{}($2)", + if confirmed { "=" } else { "<=" } + )) + .bind(confirmed as i64) + .bind(height as i64) + .map(|row: AnyRow| UUID::from_slice(row.get(0)).unwrap()) + .fetch_all(&self.pool) + .await + } + + /// Loads the transaction IDs of all the penalties and their status from the database. + pub(crate) async fn load_penalties_summaries( + &self, + ) -> HashMap { + let sql = "SELECT t.UUID, t.penalty_tx, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID"; + sqlx::query(sql) + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + ( + consensus::deserialize::(row.get(1)) + .unwrap() + .txid(), + ConfirmationStatus::from_db_data( + row.get::(2) as u32, + row.get::(3) != 0, + ), + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() + } + + /// Stores the last known block into the database. + pub(crate) async fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { + sqlx::query("INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, $1)") + .bind(block_hash.to_vec()) + .execute(&self.pool) + .await + .map(|_| ()) + } + + /// Loads the last known block from the database. + pub async fn load_last_known_block(&self) -> Option { + sqlx::query("SELECT block_hash FROM last_known_block WHERE id=0") + .map(|row: AnyRow| BlockHash::from_slice(row.get("block_hash")).unwrap()) + .fetch_one(&self.pool) + .await + .ok() + } + + /// Stores the tower secret key into the database. + /// + /// When a new key is generated, old keys are not overwritten but are not retrievable from the API either. + pub async fn store_tower_key(&self, sk: &SecretKey) -> Result<(), Error> { + sqlx::query("INSERT INTO keys (secret_key) VALUES ($1)") + .bind(sk.display_secret().to_string()) + .execute(&self.pool) + .await + .map(|_| ()) + } + + /// Loads the last known tower secret key from the database. + /// + /// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed, + /// but they are not accessible from the API either. + pub async fn load_tower_key(&self) -> Option { + sqlx::query("SELECT secret_key FROM keys WHERE id = (SELECT MAX(id) from keys)") + .map(|row: AnyRow| SecretKey::from_str(row.get("secret_key")).unwrap()) + .fetch_one(&self.pool) + .await + .ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::collections::HashSet; + use std::iter::FromIterator; + + use teos_common::cryptography::{get_random_bytes, get_random_keypair}; + use teos_common::test_utils::{get_random_locator, get_random_user_id}; + + use crate::test_utils::{ + generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, + get_random_tracker, get_random_tx, AVAILABLE_SLOTS, SUBSCRIPTION_EXPIRY, + SUBSCRIPTION_START, + }; + + impl DBM { + #[cfg(feature = "sqlite")] + async fn memory() -> Self { + use sqlx::any::AnyPoolOptions; + install_drivers(&[sqlx::sqlite::any::DRIVER]).ok(); + let pool = AnyPoolOptions::new() + // Each new connection actually creates a brand new DB: https://github.com/launchbadge/sqlx/issues/2510 + // So make sure the pool doesn't create more than one connection. + .max_connections(1) + .connect("sqlite::memory:") + .await + .unwrap(); + sqlx::migrate!("migrations/sqlite") + .run(&pool) + .await + .unwrap(); + Self { pool } + } + + #[cfg(feature = "postgres")] + async fn postgres() -> Self { + (|| async { + return_db_if_matching!( + "postgres://user:pass@localhost/teos", + postgres, + "migrations/postgres" + ); + Err(format!("Unreachable (the macro above will always match)")) + })() + .await + .unwrap() + } + + #[allow(unreachable_code)] + /// Returns a new [DBM], preferring sqlite over postgres if available. + async fn test_db() -> Self { + #[cfg(feature = "sqlite")] + return Self::memory().await; + + #[cfg(feature = "postgres")] + return Self::postgres().await; + + panic!("No database driver available. Make sure you compile with sqlite and/or postgres features enabled.") + } + + async fn load_user(&self, user_id: UserId) -> Option { + let sql = "SELECT available_slots, subscription_start, subscription_expiry FROM users WHERE user_id=($1)"; + sqlx::query(sql) + .bind(user_id.to_vec()) + .map(|row: AnyRow| { + UserInfo::new( + row.get::("available_slots") as u32, + row.get::("subscription_start") as u32, + row.get::("subscription_expiry") as u32, + ) + }) + .fetch_one(&self.pool) + .await + .ok() + } + } + + #[tokio::test] + async fn test_store_user() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + + assert!(dbm.load_user(user_id).await.is_none()); + + dbm.store_user(user_id, &user_info).await.unwrap(); + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); + + // Store an existing user should error (should use update_user). + dbm.store_user(user_id, &user_info).await.unwrap_err(); + } + + #[tokio::test] + async fn test_update_user() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let mut user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + + dbm.store_user(user_id, &user_info).await.unwrap(); + + user_info.available_slots *= 2; + dbm.update_user(user_id, &user_info).await.unwrap(); + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); + } + + #[tokio::test] + async fn test_load_user_locators() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user_info).await.unwrap(); + + let mut locators = HashSet::new(); + + // Add some appointments to the user + for _ in 0..10 { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + locators.insert(appointment.locator()); + } + + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); + assert_eq!( + HashSet::from_iter(dbm.load_user_locators(user_id).await), + locators + ); + } + + #[tokio::test] + async fn test_load_all_users() { + let dbm = DBM::test_db().await; + let mut users = HashMap::new(); + + for i in 1..11 { + let user_id = get_random_user_id(); + let user_info = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + users.insert(user_id, user_info.clone()); + dbm.store_user(user_id, &user_info).await.unwrap(); + } + + assert_eq!(dbm.load_all_users().await, users); + } + + #[tokio::test] + async fn test_batch_remove_users() { + let dbm = DBM::test_db().await; + + let mut to_be_deleted = Vec::new(); + let mut rest = HashSet::new(); + + for i in 0..SQL_VARIABLE_LIMIT * 3 { + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + if i % 2 == 0 { + to_be_deleted.push(user_id); + } else { + rest.insert(user_id); + } + } + + // SQL_VARIABLE_LIMIT is 10 for tests, + // Check that deletion had `ceil(10 * 3 / 2) / 10` (2) queries on it + assert_eq!(dbm.batch_remove_users(&to_be_deleted).await, 2); + + // Check user data was deleted + assert_eq!(rest, dbm.load_all_users().await.keys().cloned().collect()); + } + + #[tokio::test] + async fn test_batch_remove_users_cascade() { + // Test that removing users cascade deleted appointments and trackers + let dbm = DBM::test_db().await; + let uuid = generate_uuid(); + let appointment = generate_dummy_appointment(None); + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(appointment.user_id, ConfirmationStatus::ConfirmedIn(100)); + + // Add the user and link an appointment (this is usually done once the appointment) + // is added after the user creation, but for the test purpose it can be done all at once. + let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(appointment.user_id, &info).await.unwrap(); + + // Appointment only + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + dbm.batch_remove_users(&vec![appointment.user_id]).await; + assert!(dbm.load_user(appointment.user_id).await.is_none()); + assert!(dbm.load_appointment(uuid).await.is_none()); + + // Appointment + Tracker + dbm.store_user(appointment.user_id, &info).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + dbm.batch_remove_users(&vec![appointment.user_id]).await; + assert!(dbm.load_user(appointment.user_id).await.is_none()); + assert!(dbm.load_appointment(uuid).await.is_none()); + assert!(dbm.load_tracker(uuid).await.is_none()); + } + + #[tokio::test] + async fn test_batch_remove_nonexistent_users() { + let dbm = DBM::test_db().await; + let users = (0..10).map(|_| get_random_user_id()).collect(); + + // Test it does not fail even if the user does not exist + dbm.batch_remove_users(&users).await; + } + + #[tokio::test] + async fn test_get_appointments_trackers_count() { + let dbm = DBM::test_db().await; + let n_users = 100; + let n_app_per_user = 4; + let n_trk_per_user = 6; + + for _ in 0..n_users { + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + // These are un-triggered appointments. + for _ in 0..n_app_per_user { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + } + + // And these are triggered ones (trackers). + for _ in 0..n_trk_per_user { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + } + } + + assert_eq!(dbm.get_appointments_count().await, n_users * n_app_per_user); + assert_eq!(dbm.get_trackers_count().await, n_users * n_trk_per_user); + } + + #[tokio::test] + async fn test_store_load_appointment() { + let dbm = DBM::test_db().await; + + // In order to add an appointment we need the associated user to be present + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!(dbm.load_appointment(uuid).await.unwrap(), appointment); + + // Appointment info should be updatable but only via the update_appointment method + assert!(dbm + .store_appointment(uuid, &appointment) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_unique_violation()) + } + + #[tokio::test] + async fn test_store_appointment_missing_user() { + let dbm = DBM::test_db().await; + + let uuid = generate_uuid(); + let appointment = generate_dummy_appointment(None); + + assert!(dbm + .store_appointment(uuid, &appointment) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_foreign_key_violation()); + assert!((dbm.load_tracker(uuid).await.is_none())); + } + + #[tokio::test] + async fn test_update_appointment() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // Modify the appointment and update it + let mut modified_appointment = appointment; + modified_appointment.inner.encrypted_blob.reverse(); + + // Not all fields are updatable, create another appointment modifying fields that cannot be + let mut another_modified_appointment = modified_appointment.clone(); + another_modified_appointment.user_id = get_random_user_id(); + + // Check how only the modifiable fields have been updated + dbm.update_appointment(uuid, &another_modified_appointment) + .await + .unwrap(); + assert_eq!( + dbm.load_appointment(uuid).await.unwrap(), + modified_appointment + ); + assert_ne!( + dbm.load_appointment(uuid).await.unwrap(), + another_modified_appointment + ); + } + + #[tokio::test] + async fn test_load_nonexistent_appointment() { + let dbm = DBM::test_db().await; + + let uuid = generate_uuid(); + assert!(dbm.load_appointment(uuid).await.is_none()); + } + + #[tokio::test] + async fn test_appointment_exists() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + assert!(!dbm.appointment_exists(uuid).await); + + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert!(dbm.appointment_exists(uuid).await); + } + + #[tokio::test] + async fn test_get_appointment_length() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!( + dbm.get_appointment_length(uuid).await.unwrap(), + appointment.inner.encrypted_blob.len() + ); + assert!(matches!( + dbm.get_appointment_length(generate_uuid()).await, + Err(Error::RowNotFound) + )); + } + + #[tokio::test] + async fn test_get_appointment_user_and_length() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!( + dbm.get_appointment_user_and_length(uuid).await.unwrap(), + (user_id, appointment.encrypted_blob().len()) + ); + assert!(matches!( + dbm.get_appointment_user_and_length(generate_uuid()).await, + Err(Error::RowNotFound) + )); + } + + #[tokio::test] + async fn test_load_all_appointments() { + let dbm = DBM::test_db().await; + let mut appointments = HashMap::new(); + + for i in 1..11 { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + appointments.insert(uuid, appointment); + } + + assert_eq!(dbm.load_appointments(None).await, appointments); + + // If an appointment has an associated tracker, it should not be loaded since it is seen + // as a triggered appointment + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + // We should get all the appointments back except from the triggered one + assert_eq!(dbm.load_appointments(None).await, appointments); + } + + #[tokio::test] + async fn test_load_appointments_with_locator() { + let dbm = DBM::test_db().await; + let mut appointments = HashMap::new(); + let dispute_tx = get_random_tx(); + let dispute_txid = dispute_tx.txid(); + let locator = Locator::new(dispute_txid); + + for i in 1..11 { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + // Let some appointments belong to a specific dispute tx and some with random ones. + // We will use the locator for that dispute tx to query these appointments. + if i % 2 == 0 { + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + // Store the appointments made using our dispute tx. + appointments.insert(uuid, appointment); + } else { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + } + } + + // Validate that no other appointments than the ones with our locator are returned. + assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); + + // If an appointment has an associated tracker, it should not be loaded since it is seen + // as a triggered appointment + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + // Generate an appointment for our dispute tx, thus it gets the same locator as the ones generated above. + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + // We should get all the appointments matching our locator back except from the triggered one + assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); + } + + #[tokio::test] + async fn test_batch_remove_appointments() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let mut user = UserInfo::new( + AVAILABLE_SLOTS + 123, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let mut rest = HashSet::new(); + for i in 1..6 { + let mut to_be_deleted = Vec::new(); + for j in 0..SQL_VARIABLE_LIMIT * 2 * i { + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + if j % 2 == 0 { + to_be_deleted.push(uuid); + } else { + rest.insert(uuid); + } + } + + // When the appointment are deleted, the user will get back slots based on the deleted data. + // Here we can just make a number up to make sure it matches. + user.available_slots = i as u32; + let updated_users = HashMap::from_iter([(user_id, user.clone())]); + + // Check that the db transaction had i queries on it + assert_eq!( + dbm.batch_remove_appointments(&to_be_deleted, &updated_users) + .await, + i as usize + ); + // Check appointment data was deleted and users properly updated + assert_eq!( + rest, + dbm.load_appointments(None).await.keys().cloned().collect() + ); + assert_eq!( + dbm.load_user(user_id).await.unwrap().available_slots, + user.available_slots + ); + } + } + + #[tokio::test] + async fn test_batch_remove_appointments_cascade() { + let dbm = DBM::test_db().await; + let uuid = generate_uuid(); + let appointment = generate_dummy_appointment(None); + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(appointment.user_id, ConfirmationStatus::ConfirmedIn(21)); + + let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + + // Add the user b/c of FK restrictions + dbm.store_user(appointment.user_id, &info).await.unwrap(); + + println!("{}", appointment.inner.to_self_delay); + // Appointment only + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + dbm.batch_remove_appointments( + &vec![uuid], + &HashMap::from_iter([(appointment.user_id, info.clone())]), + ) + .await; + assert!(dbm.load_appointment(uuid).await.is_none()); + + // Appointment + Tracker + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + dbm.batch_remove_appointments( + &vec![uuid], + &HashMap::from_iter([(appointment.user_id, info)]), + ) + .await; + assert!(dbm.load_appointment(uuid).await.is_none()); + assert!(dbm.load_tracker(uuid).await.is_none()); + } + + #[tokio::test] + async fn test_batch_remove_nonexistent_appointments() { + let dbm = DBM::test_db().await; + let appointments = (0..10).map(|_| generate_uuid()).collect(); + + // Test it does not fail even if the user does not exist + dbm.batch_remove_appointments(&appointments, &HashMap::new()) + .await; + } + + #[tokio::test] + async fn test_load_uuids() { + let dbm = DBM::test_db().await; + + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let dispute_tx = get_random_tx(); + let dispute_txid = dispute_tx.txid(); + let mut uuids = HashSet::new(); + + // Add ten appointments triggered by the same locator. + for _ in 0..10 { + let user_id = get_random_user_id(); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + uuids.insert(uuid); + } + + // Add ten more appointments triggered by different locators. + for _ in 0..10 { + let user_id = get_random_user_id(); + dbm.store_user(user_id, &user).await.unwrap(); + + let dispute_txid = get_random_tx().txid(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + } + + assert_eq!( + HashSet::from_iter(dbm.load_uuids(Locator::new(dispute_txid)).await), + uuids + ); + } + + #[tokio::test] + async fn test_batch_check_locators_exist() { + let dbm = DBM::test_db().await; + // Generate `n_app` appointments which we will store in the DB. + let n_app = 100; + let appointments: Vec<_> = (0..n_app) + .map(|_| generate_dummy_appointment(None)) + .collect(); + + // Register all the users beforehand. + for user_id in appointments.iter().map(|a| a.user_id) { + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + } + + // Store all the `n_app` appointments. + for appointment in appointments.iter() { + dbm.store_appointment( + UUID::new(appointment.locator(), appointment.user_id), + appointment, + ) + .await + .unwrap(); + } + + // Select `n_app / 5` locators as if they appeared in a new block. + let known_locators: HashSet<_> = appointments + .iter() + .take(n_app / 5) + .map(|a| a.locator()) + .collect(); + // And extra `n_app / 5` unknown locators. + let unknown_locators: HashSet<_> = (0..n_app / 5).map(|_| get_random_locator()).collect(); + let all_locators = known_locators + .iter() + .chain(unknown_locators.iter()) + .collect(); + + assert_eq!( + HashSet::from_iter(dbm.batch_check_locators_exist(all_locators).await), + known_locators + ); + } + + #[tokio::test] + async fn test_store_load_tracker() { + let dbm = DBM::test_db().await; + + // In order to add a tracker we need the associated appointment to be present (which + // at the same time requires an associated user to be present) + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(21)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + assert_eq!(dbm.load_tracker(uuid).await.unwrap(), tracker); + } + + #[tokio::test] + async fn test_store_duplicate_tracker() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + // Try to store it again, but it shouldn't go through + assert!(dbm + .store_tracker(uuid, &tracker) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_unique_violation()); + } + + #[tokio::test] + async fn test_store_tracker_missing_appointment() { + let dbm = DBM::test_db().await; + + let uuid = generate_uuid(); + let user_id = get_random_user_id(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); + + // Try to store the tracker with no appointment for it + assert!(dbm + .store_tracker(uuid, &tracker) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_foreign_key_violation()) + } + + #[tokio::test] + async fn test_update_tracker_status() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + // Update the status and check if it's actually updated. + dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(100)) + .await + .unwrap(); + assert_eq!( + dbm.load_tracker(uuid).await.unwrap().status, + ConfirmationStatus::ConfirmedIn(100) + ); + + // Rejected status doesn't have a persistent DB representation. + assert!(matches!( + dbm.update_tracker_status(uuid, &ConfirmationStatus::Rejected(100)) + .await, + Err(Error::Decode(..)) + )); + } + + #[tokio::test] + async fn test_load_nonexistent_tracker() { + let dbm = DBM::test_db().await; + + let uuid = generate_uuid(); + assert!(dbm.load_tracker(uuid).await.is_none()); + } + + #[tokio::test] + async fn test_load_all_trackers() { + let dbm = DBM::test_db().await; + let mut trackers = HashMap::new(); + + for i in 1..11 { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. + let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + trackers.insert(uuid, tracker); + } + + assert_eq!(dbm.load_trackers(None).await, trackers); + } + + #[tokio::test] + async fn test_load_trackers_with_locator() { + let dbm = DBM::test_db().await; + let mut trackers = HashMap::new(); + let dispute_tx = get_random_tx(); + let dispute_txid = dispute_tx.txid(); + let locator = Locator::new(dispute_txid); + let status = ConfirmationStatus::InMempoolSince(42); + + for i in 1..11 { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + let tracker = get_random_tracker(user_id, status); + + // Let some trackers belong to our dispute tx and some belong to random ones. + let (uuid, appointment) = if i % 2 == 0 { + let (uuid, app) = + generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); + // Store the trackers of appointments made with our dispute tx. + trackers.insert(uuid, tracker.clone()); + (uuid, app) + } else { + generate_dummy_appointment_with_user(user_id, None) + }; + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + } + + assert_eq!(dbm.load_trackers(Some(locator)).await, trackers); + } + + #[tokio::test] + async fn test_load_trackers_with_confirmation_status() { + let dbm = DBM::test_db().await; + let n_trackers = 100; + let mut tracker_statuses = HashMap::new(); + + // Store a bunch of trackers. + for i in 0..n_trackers { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // Some trackers confirmed and some aren't. + let status = if i % 2 == 0 { + ConfirmationStatus::InMempoolSince(i) + } else { + ConfirmationStatus::ConfirmedIn(i) + }; + + let tracker = get_random_tracker(user_id, status); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + tracker_statuses.insert(uuid, status); + } + + for i in 0..n_trackers + 10 { + let in_mempool_since_i: HashSet = tracker_statuses + .iter() + .filter_map(|(&uuid, &status)| { + if let ConfirmationStatus::InMempoolSince(x) = status { + // If a tracker was in mempool since x, then it's also in mempool since x + 1, x + 2, etc... + return (x <= i).then(|| uuid); + } + None + }) + .collect(); + assert_eq!( + HashSet::from_iter( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::InMempoolSince( + i + )) + .await + .unwrap() + ), + in_mempool_since_i, + ); + let confirmed_in_i: HashSet = tracker_statuses + .iter() + .filter_map(|(&uuid, &status)| { + if let ConfirmationStatus::ConfirmedIn(x) = status { + return (x == i).then(|| uuid); + } + None + }) + .collect(); + assert_eq!( + HashSet::from_iter( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(i)) + .await + .unwrap() + ), + confirmed_in_i, + ); + } + + assert!(matches!( + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::Rejected(100)) + .await, + Err(Error::Decode(..)) + )); + } + + #[tokio::test] + async fn test_load_penalties_summaries() { + let dbm = DBM::test_db().await; + let n_trackers = 100; + let mut penalties_summaries = HashMap::new(); + + for i in 0..n_trackers { + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + i, + SUBSCRIPTION_START + i, + SUBSCRIPTION_EXPIRY + i, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + let status = if i % 2 == 0 { + ConfirmationStatus::InMempoolSince(i) + } else { + ConfirmationStatus::ConfirmedIn(i) + }; + + let tracker = get_random_tracker(user_id, status); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + + penalties_summaries.insert(uuid, (tracker.penalty_tx.txid(), status)); + } + + assert_eq!(dbm.load_penalties_summaries().await, penalties_summaries); + } + + #[tokio::test] + async fn test_store_load_last_known_block() { + let dbm = DBM::test_db().await; + + let mut block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); + dbm.store_last_known_block(&block_hash).await.unwrap(); + assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); + + // Update with a new hash to check it can be done + block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); + dbm.store_last_known_block(&block_hash).await.unwrap(); + assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); + } + + #[tokio::test] + async fn test_store_load_nonexistent_last_known_block() { + let dbm = DBM::test_db().await; + + assert!(dbm.load_last_known_block().await.is_none()); + } + + #[tokio::test] + async fn test_store_load_tower_key() { + let dbm = DBM::test_db().await; + + assert!(dbm.load_tower_key().await.is_none()); + for _ in 0..7 { + let sk = get_random_keypair().0; + dbm.store_tower_key(&sk).await.unwrap(); + assert_eq!(dbm.load_tower_key().await.unwrap(), sk); + } + } +} diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index b4e7d600..4f581870 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -1,6 +1,5 @@ //! Logic related to the Gatekeeper, the component in charge of managing access to the tower resources. -use lightning::chain; use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; @@ -11,6 +10,7 @@ use teos_common::cryptography; use teos_common::receipts::RegistrationReceipt; use teos_common::UserId; +use crate::async_listener::AsyncListen; use crate::dbm::DBM; use crate::extended_appointment::{ExtendedAppointment, UUID}; @@ -291,17 +291,13 @@ impl Gatekeeper { } } -impl chain::Listen for Gatekeeper { +#[tonic::async_trait] +impl AsyncListen for Gatekeeper { /// Handles the monitoring process by the [Gatekeeper]. /// /// This is mainly used to keep track of time and expire / outdate subscriptions when needed. - fn filtered_block_connected( - &self, - header: &bitcoin::BlockHeader, - _: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + async fn block_connected(&self, block: &bitcoin::Block, height: u32) { + log::info!("New block received: {}", block.header.block_hash()); // Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired. let outdated_users = self.get_outdated_users(height); @@ -324,7 +320,7 @@ impl chain::Listen for Gatekeeper { } /// Handles reorgs in the [Gatekeeper]. Simply updates the last_known_block_height. - fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) { + async fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); // There's nothing to be done here but updating the last known block self.last_known_block_height @@ -332,27 +328,11 @@ impl chain::Listen for Gatekeeper { } } -use bitcoin::Block; -use bitcoin::BlockHeader; -use crate::listener_actor::AsyncListen; - -#[tonic::async_trait] -impl AsyncListen for Gatekeeper { - async fn block_connected(&self, block: &Block, height: u32) { - chain::Listen::block_connected(self, block, height); - } - - async fn block_disconnected(&self, header: &BlockHeader, height: u32) { - chain::Listen::block_disconnected(self, header, height); - } -} - #[cfg(test)] mod tests { use super::*; use crate::test_utils::{generate_dummy_appointment_with_user, get_random_tracker, Blockchain}; - use lightning::chain::Listen; use teos_common::cryptography::{get_random_bytes, get_random_keypair}; use teos_common::test_utils::get_random_user_id; @@ -393,8 +373,8 @@ mod tests { Gatekeeper::new(chain.get_block_count(), SLOTS, DURATION, EXPIRY_DELTA, dbm) } - #[test] - fn test_new() { + #[tokio::test] + async fn test_new() { // A fresh gatekeeper has no associated data let chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); @@ -434,8 +414,8 @@ mod tests { assert_eq!(gatekeeper, another_gk); } - #[test] - fn test_authenticate_user() { + #[tokio::test] + async fn test_authenticate_user() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); // Authenticate user returns the UserId if the user is found in the system, or an AuthenticationError otherwise. @@ -465,8 +445,8 @@ mod tests { ); } - #[test] - fn test_add_update_user() { + #[tokio::test] + async fn test_add_update_user() { let mut chain = Blockchain::default().with_height(START_HEIGHT); let gatekeeper = init_gatekeeper(&chain); @@ -534,8 +514,8 @@ mod tests { ); } - #[test] - fn test_add_update_appointment() { + #[tokio::test] + async fn test_add_update_appointment() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); // if a given appointment is not associated with a given user, add_update_appointment adds the appointment user appointments alongside the number os slots it consumes. If the appointment @@ -665,8 +645,8 @@ mod tests { assert_eq!(loaded_user.available_slots, updated_slot_count); } - #[test] - fn test_has_subscription_expired() { + #[tokio::test] + async fn test_has_subscription_expired() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); // If the user is not registered, querying for a subscription expiry check should return an error @@ -698,8 +678,8 @@ mod tests { ); } - #[test] - fn test_get_outdated_users() { + #[tokio::test] + async fn test_get_outdated_users() { let start_height = START_HEIGHT as u32 + EXPIRY_DELTA; let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(start_height as usize)); @@ -720,8 +700,8 @@ mod tests { assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]); } - #[test] - fn test_delete_appointments_without_refund() { + #[tokio::test] + async fn test_delete_appointments_without_refund() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); let n_users = 100; let n_apps = 10; @@ -792,8 +772,8 @@ mod tests { } } - #[test] - fn test_delete_appointments_with_refund() { + #[tokio::test] + async fn test_delete_appointments_with_refund() { let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); let n_users = 100; let n_apps = 10; @@ -876,8 +856,8 @@ mod tests { } } - #[test] - fn test_filtered_block_connected() { + #[tokio::test] + async fn test_block_connected() { // block_connected in the Gatekeeper is used to keep track of time in order to manage the users' subscription expiry. // Remove users that get outdated at the new block's height from registered_users and the database. let mut chain = Blockchain::default().with_height(START_HEIGHT); @@ -893,7 +873,9 @@ mod tests { } // Connect a new block. Outdated users are deleted - gatekeeper.block_connected(&chain.generate(None), chain.get_block_count()); + gatekeeper + .block_connected(&chain.generate(None), chain.get_block_count()) + .await; // Check that users have been removed from registered_users and the database for user_id in &[user1_id, user2_id, user3_id] { @@ -912,8 +894,8 @@ mod tests { ); } - #[test] - fn test_block_disconnected() { + #[tokio::test] + async fn test_block_disconnected() { // Block disconnected simply updates the last known block let chain = Blockchain::default().with_height(START_HEIGHT); let gatekeeper = init_gatekeeper(&chain); @@ -922,7 +904,9 @@ mod tests { let last_known_block_header = chain.tip(); let prev_block_header = chain.at_height((height - 1) as usize); - gatekeeper.block_disconnected(&last_known_block_header.header, height); + gatekeeper + .block_disconnected(&last_known_block_header.header, height) + .await; assert_eq!( gatekeeper.last_known_block_height.load(Ordering::Relaxed), prev_block_header.height diff --git a/teos/src/lib.rs b/teos/src/lib.rs index 2418991c..dbd81b22 100644 --- a/teos/src/lib.rs +++ b/teos/src/lib.rs @@ -14,11 +14,11 @@ pub mod chain_monitor; pub mod cli_config; pub mod config; pub mod dbm; -pub mod listener_actor; #[doc(hidden)] mod errors; mod extended_appointment; pub mod gatekeeper; +pub mod async_listener; pub mod responder; #[doc(hidden)] mod rpc_errors; @@ -26,5 +26,6 @@ pub mod tls; mod tx_index; pub mod watcher; +pub mod dbm_new; #[cfg(test)] mod test_utils; diff --git a/teos/src/main.rs b/teos/src/main.rs index 870feeac..909d77b1 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -20,13 +20,14 @@ use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCa use teos::api::internal::InternalAPI; use teos::api::{http, tor::TorAPI}; +use teos::async_listener::AsyncBlockListener; use teos::bitcoin_cli::BitcoindClient; use teos::carrier::Carrier; use teos::chain_monitor::ChainMonitor; use teos::config::{self, Config, Opt}; use teos::dbm::DBM; +use teos::dbm_new::DBM as NewDBM; use teos::gatekeeper::Gatekeeper; -use teos::listener_actor::AsyncBlockListener; use teos::protos as msgs; use teos::protos::private_tower_services_server::PrivateTowerServicesServer; use teos::protos::public_tower_services_server::PublicTowerServicesServer; @@ -58,9 +59,9 @@ where Ok(last_n_blocks) } -fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) { +async fn create_new_tower_keypair(dbm: &NewDBM) -> (SecretKey, PublicKey) { let (sk, pk) = get_random_keypair(); - db.store_tower_key(&sk).unwrap(); + dbm.store_tower_key(&sk).await.unwrap(); (sk, pk) } @@ -123,6 +124,22 @@ async fn main() { conf.log_non_default_options(); } + let new_dbm = if conf.database_url == "managed" { + teos::dbm_new::DBM::new(&format!( + "sqlite://{}", + path_network + // rwc = Read + Write + Create (creates the database file if not found) + .join("teos_db.sql3?mode=rwc") + .to_str() + .expect("Path to the sqlite DB contains non-UTF-8 characters.") + )) + .await + .unwrap() + } else { + teos::dbm_new::DBM::new(&conf.database_url).await.unwrap() + }; + let new_dbm = Arc::new(new_dbm); + let dbm = Arc::new(Mutex::new( DBM::new(path_network.join("teos_db.sql3")).unwrap(), )); @@ -130,15 +147,14 @@ async fn main() { // Load tower secret key or create a fresh one if none is found. If overwrite key is set, create a new // key straightaway let (tower_sk, tower_pk) = { - let locked_db = dbm.lock().unwrap(); if conf.overwrite_key { log::info!("Overwriting tower keys"); - create_new_tower_keypair(&locked_db) - } else if let Some(sk) = locked_db.load_tower_key() { + create_new_tower_keypair(&new_dbm).await + } else if let Some(sk) = new_dbm.load_tower_key().await { (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)) } else { log::info!("Tower keys not found. Creating a fresh set"); - create_new_tower_keypair(&locked_db) + create_new_tower_keypair(&new_dbm).await } }; log::info!("tower_id: {tower_pk}"); @@ -183,7 +199,7 @@ async fn main() { ); let mut derefed = bitcoin_cli.deref(); // Load last known block from DB if found. Poll it from Bitcoind otherwise. - let last_known_block = dbm.lock().unwrap().load_last_known_block(); + let last_known_block = new_dbm.load_last_known_block().await; let tip = if let Some(block_hash) = last_known_block { let mut last_known_header = derefed .get_header(&block_hash, None) @@ -313,9 +329,10 @@ async fn main() { // The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called // first so it updates the users' states and both the Watcher and the Responder operate only on registered users. let listeners = (gatekeeper, (watcher.clone(), responder)); + // This spawns a separate async actor that will be fed new blocks from a sync block listener. // In this way we can have our components listen to blocks in an async manner from the async actor. - let listener = AsyncBlockListener::new(listeners, dbm); + let listener = AsyncBlockListener::wrap_listener(listeners, dbm); let cache = &mut UnboundedCache::new(); let spv_client = SpvClient::new(tip, poller, cache, &listener); diff --git a/teos/src/responder.rs b/teos/src/responder.rs index 1345a0bb..82d77ac0 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -4,14 +4,14 @@ use std::collections::HashSet; use std::sync::{Arc, Mutex}; use bitcoin::{consensus, BlockHash}; -use bitcoin::{BlockHeader, Transaction, Txid}; -use lightning::chain; +use bitcoin::{Block, BlockHeader, Transaction, Txid}; use lightning_block_sync::poll::ValidatedBlock; use teos_common::constants; use teos_common::protos as common_msgs; use teos_common::UserId; +use crate::async_listener::AsyncListen; use crate::carrier::Carrier; use crate::dbm::DBM; use crate::extended_appointment::UUID; @@ -388,7 +388,8 @@ impl Responder { } /// Listen implementation by the [Responder]. Handles monitoring and reorgs. -impl chain::Listen for Responder { +#[tonic::async_trait] +impl AsyncListen for Responder { /// Handles the monitoring process by the [Responder]. /// /// Watching is performed in a per-block basis. A [TransactionTracker] is tracked until: @@ -399,20 +400,16 @@ impl chain::Listen for Responder { /// Every time a block is received the tracking conditions are checked against the monitored [TransactionTracker]s and /// data deletion is performed accordingly. Moreover, lack of confirmations is check for the tracked transactions and /// rebroadcasting is performed for those that have missed too many. - fn filtered_block_connected( - &self, - header: &BlockHeader, - txdata: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + async fn block_connected(&self, block: &Block, height: u32) { + log::info!("New block received: {}", block.header.block_hash()); self.carrier.lock().unwrap().update_height(height); - let txs = txdata + let txs = block + .txdata .iter() - .map(|(_, tx)| (tx.txid(), header.block_hash())) + .map(|tx| (tx.txid(), block.header.block_hash())) .collect(); - self.tx_index.lock().unwrap().update(*header, &txs); + self.tx_index.lock().unwrap().update(block.header, &txs); // Delete trackers completed at this height if let Some(trackers) = self.check_confirmations(txs.keys().cloned().collect(), height) { @@ -444,7 +441,7 @@ impl chain::Listen for Responder { } /// Handles reorgs in the [Responder]. - fn block_disconnected(&self, header: &BlockHeader, height: u32) { + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); // Update the carrier and our tx_index. self.carrier.lock().unwrap().update_height(height); @@ -466,24 +463,9 @@ impl chain::Listen for Responder { } } -use bitcoin::Block; -use crate::listener_actor::AsyncListen; - -#[tonic::async_trait] -impl AsyncListen for Responder { - async fn block_connected(&self, block: &Block, height: u32) { - chain::Listen::block_connected(self, block, height); - } - - async fn block_disconnected(&self, header: &BlockHeader, height: u32) { - chain::Listen::block_disconnected(self, header, height); - } -} - #[cfg(test)] mod tests { use super::*; - use lightning::chain::Listen; use teos_common::appointment::Locator; use std::collections::HashMap; @@ -1165,14 +1147,14 @@ mod tests { } #[tokio::test] - async fn test_filtered_block_connected() { + async fn test_block_connected() { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let start_height = START_HEIGHT * 2; let mut chain = Blockchain::default().with_height(start_height); let (responder, _s) = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; - // filtered_block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder + // block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder // is keeping track of. // // If there are any trackers, the Responder will: @@ -1342,8 +1324,8 @@ mod tests { )); let height = chain.get_block_count(); // We connect the gatekeeper first so it deletes the outdated users. - responder.gatekeeper.block_connected(&block, height); - responder.block_connected(&block, height); + responder.gatekeeper.block_connected(&block, height).await; + responder.block_connected(&block, height).await; // CARRIER CHECKS assert!(responder @@ -1468,7 +1450,7 @@ mod tests { // Check that trackers are flagged as reorged if the height they were included at gets disconnected for (i, uuid) in block_range.clone().zip(reorged.iter()).rev() { // The header doesn't really matter, just the height - responder.block_disconnected(&chain.tip().header, i as u32); + responder.block_disconnected(&chain.tip().header, i as u32).await; // Check that the proper tracker gets reorged at the proper height assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); // Check that the carrier block_height has been updated @@ -1481,7 +1463,9 @@ mod tests { } // But should be clear after the first block connection - responder.block_connected(&chain.generate(None), block_range.start as u32); + responder + .block_connected(&chain.generate(None), block_range.start as u32) + .await; assert!(responder.reorged_trackers.lock().unwrap().is_empty()); } } diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index cfd4e6f3..7df86b7d 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -5,8 +5,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; use bitcoin::secp256k1::SecretKey; -use bitcoin::{BlockHeader, Transaction}; -use lightning::chain; +use bitcoin::{Block, BlockHeader, Transaction}; use lightning_block_sync::poll::ValidatedBlock; use teos_common::appointment::{Appointment, Locator}; @@ -14,6 +13,7 @@ use teos_common::cryptography; use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; use teos_common::{TowerId, UserId}; +use crate::async_listener::AsyncListen; use crate::dbm::DBM; use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, MaxSlotsReached, UserInfo}; @@ -479,7 +479,8 @@ impl Watcher { } /// Listen implementation by the [Watcher]. Handles monitoring and reorgs. -impl chain::Listen for Watcher { +#[tonic::async_trait] +impl AsyncListen for Watcher { /// Handles the monitoring process by the [Watcher]. /// /// Watching is performed in a per-block basis. Therefore, a breach is only considered (and detected) if seen @@ -491,23 +492,19 @@ impl chain::Listen for Watcher { /// /// This also takes care of updating the [LocatorCache] and removing outdated data from the [Watcher] when /// told by the [Gatekeeper]. - fn filtered_block_connected( - &self, - header: &BlockHeader, - txdata: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + async fn block_connected(&self, block: &Block, height: u32) { + log::info!("New block received: {}", block.header.block_hash()); - let locator_tx_map = txdata + let locator_tx_map = block + .txdata .iter() - .map(|(_, tx)| (Locator::new(tx.txid()), (*tx).clone())) + .map(|tx| (Locator::new(tx.txid()), (*tx).clone())) .collect(); self.locator_cache .lock() .unwrap() - .update(*header, &locator_tx_map); + .update(block.header, &locator_tx_map); // Get the breaches found in this block, handle them, and delete invalid ones. if let Some(invalid_breaches) = self.handle_breaches(self.get_breaches(locator_tx_map)) { @@ -522,7 +519,7 @@ impl chain::Listen for Watcher { /// Handle reorgs in the [Watcher]. /// /// Fixes the [LocatorCache] by removing the disconnected data and updates the last_known_block_height. - fn block_disconnected(&self, header: &BlockHeader, height: u32) { + async fn block_disconnected(&self, header: &BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); self.locator_cache .lock() @@ -533,20 +530,6 @@ impl chain::Listen for Watcher { } } -use bitcoin::Block; -use crate::listener_actor::AsyncListen; - -#[tonic::async_trait] -impl AsyncListen for Watcher { - async fn block_connected(&self, block: &Block, height: u32) { - chain::Listen::block_connected(self, block, height); - } - - async fn block_disconnected(&self, header: &BlockHeader, height: u32) { - chain::Listen::block_disconnected(self, header, height); - } -} - #[cfg(test)] mod tests { use super::*; @@ -567,8 +550,6 @@ mod tests { use bitcoin::secp256k1::{PublicKey, Secp256k1}; - use lightning::chain::Listen; - impl PartialEq for Watcher { fn eq(&self, other: &Self) -> bool { // Same in-memory data. @@ -1229,7 +1210,7 @@ mod tests { } #[tokio::test] - async fn test_filtered_block_connected() { + async fn test_block_connected() { let mut chain = Blockchain::default().with_height(START_HEIGHT); let (watcher, _s) = init_watcher(&mut chain).await; @@ -1242,7 +1223,9 @@ mod tests { watcher.last_known_block_height.load(Ordering::Relaxed), chain.get_block_count() ); - watcher.block_connected(&chain.generate(None), chain.get_block_count()); + watcher + .block_connected(&chain.generate(None), chain.get_block_count()) + .await; assert_eq!( watcher.last_known_block_height.load(Ordering::Relaxed), chain.get_block_count() @@ -1290,8 +1273,11 @@ mod tests { let block = chain.generate(None); watcher .gatekeeper - .block_connected(&block, chain.get_block_count()); - watcher.block_connected(&block, chain.get_block_count()); + .block_connected(&block, chain.get_block_count()) + .await; + watcher + .block_connected(&block, chain.get_block_count()) + .await; // uuid1 and user1 should have been deleted while uuid2 and user2 still exists. assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid1)); @@ -1321,8 +1307,11 @@ mod tests { let block = chain.generate(Some(vec![dispute_tx])); watcher .gatekeeper - .block_connected(&block, chain.get_block_count()); - watcher.block_connected(&block, chain.get_block_count()); + .block_connected(&block, chain.get_block_count()) + .await; + watcher + .block_connected(&block, chain.get_block_count()) + .await; // Data should have been kept in the database assert!(watcher.responder.has_tracker(uuid)); @@ -1339,8 +1328,11 @@ mod tests { let block = chain.generate(Some(vec![dispute_tx])); watcher .gatekeeper - .block_connected(&block, chain.get_block_count()); - watcher.block_connected(&block, chain.get_block_count()); + .block_connected(&block, chain.get_block_count()) + .await; + watcher + .block_connected(&block, chain.get_block_count()) + .await; // Data should have been wiped from the database assert!(!watcher.responder.has_tracker(uuid)); @@ -1364,8 +1356,11 @@ mod tests { let block = chain.generate(Some(vec![dispute_tx])); watcher .gatekeeper - .block_connected(&block, chain.get_block_count()); - watcher.block_connected(&block, chain.get_block_count()); + .block_connected(&block, chain.get_block_count()) + .await; + watcher + .block_connected(&block, chain.get_block_count()) + .await; // Data should have been wiped from the database assert!(!watcher.responder.has_tracker(uuid)); @@ -1388,7 +1383,9 @@ mod tests { .blocks() .contains(&last_block_header.block_hash())); - watcher.block_disconnected(&last_block_header, start_height); + watcher + .block_disconnected(&last_block_header, start_height) + .await; assert_eq!( watcher.last_known_block_height.load(Ordering::Relaxed), From 9c9f7fd01cd3c140ef273edf642ba5562c2d9f89 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 5 Aug 2023 17:55:41 +0300 Subject: [PATCH 05/15] Moving the tower components to async Some mutexed needed their lifetimes be shortened due to the fact that an std mutex guard can't live across await point. one specific mutex on GateKeeper::registered_users was more fitting to be a tokio mutex instead due to the nature of how it is uses beside the database usage (std mutex is generally better/has less overhead than a tokio mutex). tests still need to be patched for async --- teos/src/api/internal.rs | 30 +- teos/src/async_listener.rs | 9 +- teos/src/dbm.rs | 1811 +++++++++++++++++------------------- teos/src/dbm_new.rs | 1590 ------------------------------- teos/src/gatekeeper.rs | 171 ++-- teos/src/lib.rs | 3 +- teos/src/main.rs | 42 +- teos/src/responder.rs | 302 +++--- teos/src/test_utils.rs | 10 +- teos/src/watcher.rs | 216 +++-- 10 files changed, 1216 insertions(+), 2968 deletions(-) delete mode 100644 teos/src/dbm_new.rs diff --git a/teos/src/api/internal.rs b/teos/src/api/internal.rs index fc2085d8..e8695740 100644 --- a/teos/src/api/internal.rs +++ b/teos/src/api/internal.rs @@ -82,7 +82,7 @@ impl PublicTowerServices for Arc { ) })?; - match self.watcher.register(user_id) { + match self.watcher.register(user_id).await { Ok(receipt) => Ok(Response::new(common_msgs::RegisterResponse { user_id: req_data.user_id, available_slots: receipt.available_slots(), @@ -116,6 +116,7 @@ impl PublicTowerServices for Arc { match self .watcher .add_appointment(appointment, req_data.signature) + .await { Ok((receipt, available_slots, subscription_expiry)) => { Ok(Response::new(common_msgs::AddAppointmentResponse { @@ -153,7 +154,11 @@ impl PublicTowerServices for Arc { let req_data = request.into_inner(); let locator = Locator::from_slice(&req_data.locator).unwrap(); - match self.watcher.get_appointment(locator, &req_data.signature) { + match self + .watcher + .get_appointment(locator, &req_data.signature) + .await + { Ok(info) => { let (appointment_data, status) = match info { AppointmentInfo::Appointment(appointment) => ( @@ -207,6 +212,7 @@ impl PublicTowerServices for Arc { let (subscription_info, locators) = self .watcher .get_subscription_info(&request.into_inner().signature) + .await .map_err(|e| match e { GetSubscriptionInfoFailure::AuthenticationFailure => Status::new( Code::Unauthenticated, @@ -244,7 +250,12 @@ impl PrivateTowerServices for Arc { let mut all_appointments = Vec::new(); - for (_, appointment) in self.watcher.get_all_watcher_appointments().into_iter() { + for (_, appointment) in self + .watcher + .get_all_watcher_appointments() + .await + .into_iter() + { all_appointments.push(common_msgs::AppointmentData { appointment_data: Some( common_msgs::appointment_data::AppointmentData::Appointment( @@ -254,7 +265,7 @@ impl PrivateTowerServices for Arc { }) } - for (_, tracker) in self.watcher.get_all_responder_trackers().into_iter() { + for (_, tracker) in self.watcher.get_all_responder_trackers().await.into_iter() { all_appointments.push(common_msgs::AppointmentData { appointment_data: Some(common_msgs::appointment_data::AppointmentData::Tracker( tracker.into(), @@ -291,6 +302,7 @@ impl PrivateTowerServices for Arc { for (_, appointment) in self .watcher .get_watcher_appointments_with_locator(locator) + .await .into_iter() { matching_appointments.push(common_msgs::AppointmentData { @@ -305,6 +317,7 @@ impl PrivateTowerServices for Arc { for (_, tracker) in self .watcher .get_responder_trackers_with_locator(locator) + .await .into_iter() { matching_appointments.push(common_msgs::AppointmentData { @@ -336,9 +349,9 @@ impl PrivateTowerServices for Arc { Ok(Response::new(msgs::GetTowerInfoResponse { tower_id: self.watcher.tower_id.to_vec(), addresses: self.get_addresses().clone(), - n_registered_users: self.watcher.get_registered_users_count() as u32, - n_watcher_appointments: self.watcher.get_appointments_count() as u32, - n_responder_trackers: self.watcher.get_trackers_count() as u32, + n_registered_users: self.watcher.get_registered_users_count().await as u32, + n_watcher_appointments: self.watcher.get_appointments_count().await as u32, + n_responder_trackers: self.watcher.get_trackers_count().await as u32, bitcoind_reachable: self.check_service_unavailable().is_ok(), })) } @@ -359,6 +372,7 @@ impl PrivateTowerServices for Arc { let user_ids = self .watcher .get_user_ids() + .await .iter() .map(|x| x.to_vec()) .collect(); @@ -386,7 +400,7 @@ impl PrivateTowerServices for Arc { ) })?; - match self.watcher.get_user_info(user_id) { + match self.watcher.get_user_info(user_id).await { Some((info, locators)) => Ok(Response::new(msgs::GetUserResponse { available_slots: info.available_slots, subscription_expiry: info.subscription_expiry, diff --git a/teos/src/async_listener.rs b/teos/src/async_listener.rs index de0006ff..09f1baf1 100644 --- a/teos/src/async_listener.rs +++ b/teos/src/async_listener.rs @@ -4,7 +4,7 @@ use crate::dbm::DBM; use std::marker::{Send, Sync}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use bitcoin::{Block, BlockHeader}; use lightning::chain; @@ -51,7 +51,7 @@ enum BlockListenerAction { /// blocks received from [UnboundedReceiver] in the background. pub struct AsyncBlockListener { listener: L, - dbm: Arc>, + dbm: Arc, rx: UnboundedReceiver, } @@ -62,7 +62,7 @@ impl AsyncBlockListener { /// listener are forwarded to the [AsyncListen] listener. /// /// The [AsyncListen] listener will be actively listening for actions in a background tokio task. - pub fn wrap_listener(listener: L, dbm: Arc>) -> SyncBlockListener { + pub fn wrap_listener(listener: L, dbm: Arc) -> SyncBlockListener { let (tx, rx) = unbounded_channel(); let actor = AsyncBlockListener { listener, dbm, rx }; actor.run_actor_in_bg(); @@ -79,9 +79,8 @@ impl AsyncBlockListener { self.listener.block_connected(&block, height).await; // We can update the last known block after all the listeners have received it. self.dbm - .lock() - .unwrap() .store_last_known_block(&block.block_hash()) + .await .unwrap(); } BlockListenerAction::BlockDisconnected(header, height) => { diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 5ecd9768..9cf4787c 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -1,19 +1,14 @@ //! Logic related to the tower database manager (DBM), component in charge of persisting data on disk. -//! +#![allow(dead_code)] use std::collections::HashMap; -use std::iter::FromIterator; -use std::path::PathBuf; use std::str::FromStr; -use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; -use rusqlite::limits::Limit; -use rusqlite::{params, params_from_iter, Connection, Error as SqliteError, ErrorCode, Params}; - -use bitcoin::consensus; use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; -use bitcoin::BlockHash; +use bitcoin::{consensus, BlockHash}; +use sqlx::any::{install_drivers, AnyRow}; +use sqlx::{AnyPool, Row}; use teos_common::appointment::{Appointment, Locator}; use teos_common::UserId; @@ -21,781 +16,611 @@ use teos_common::UserId; use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::UserInfo; use crate::responder::{ConfirmationStatus, PenaltySummary, TransactionTracker}; +use crate::watcher::Breach; -const TABLES: [&str; 6] = [ - "CREATE TABLE IF NOT EXISTS users ( - user_id INT PRIMARY KEY, - available_slots INT NOT NULL, - subscription_start INT NOT NULL, - subscription_expiry INT NOT NULL -)", - "CREATE TABLE IF NOT EXISTS appointments ( - UUID INT PRIMARY KEY, - locator INT NOT NULL, - encrypted_blob BLOB NOT NULL, - to_self_delay INT NOT NULL, - user_signature BLOB NOT NULL, - start_block INT NOT NULL, - user_id INT NOT NULL, - FOREIGN KEY(user_id) - REFERENCES users(user_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS trackers ( - UUID INT PRIMARY KEY, - dispute_tx BLOB NOT NULL, - penalty_tx BLOB NOT NULL, - height INT NOT NULL, - confirmed BOOL NOT NULL, - FOREIGN KEY(UUID) - REFERENCES appointments(UUID) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS last_known_block ( - id INT PRIMARY KEY, - block_hash INT NOT NULL -)", - "CREATE TABLE IF NOT EXISTS keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key INT NOT NULL -)", - "CREATE INDEX IF NOT EXISTS locators_index ON appointments ( - locator -)", -]; - -/// Packs the errors than can raise when interacting with the underlying database. -#[derive(Debug)] -pub enum Error { - AlreadyExists, - MissingForeignKey, - MissingField, - NotFound, - Unknown(SqliteError), +type Error = sqlx::Error; + +#[cfg(not(test))] +/// The maximum number of bind variables per SQL query. +/// `32766` for SQLite & `65535` for PostgreSQL. Picking `32766` for both since it's big enough already. +const SQL_VARIABLE_LIMIT: usize = 32766; +#[cfg(test)] +/// The maximum number of bind variables per SQL query. Set to `10` for testing. +const SQL_VARIABLE_LIMIT: usize = 10; + +/// Checks if the database type (`$db_type`) matches with the connection string (`$connection_string`). +/// If the connection string matches the database type, this macro does: +/// - Try to install the driver for the passed database type. +/// - Try to connect to the database. +/// - Try to run the migrations specified in `$migrations_path`. +/// - Return [Ok(DBM)] if everything succeeds, [Err(String)] otherwise explaining the error. +macro_rules! return_db_if_matching { + ($connection_string:expr, $db_type:tt, $migrations_path:literal) => { + if $connection_string.starts_with(concat!(stringify!($db_type), ":")) { + install_drivers(&[sqlx::$db_type::any::DRIVER]) + // Just log the warning but try to connect anyways. `install_drivers` might fail if the driver is already installed. + .map_err(|e| log::error!("Failed to install database drivers for {}: {e}", stringify!($db_type))) + .ok(); + let pool = AnyPool::connect($connection_string) + .await + .map_err(|e| format!("Couldn't connect to {}: {e}", $connection_string))?; + sqlx::migrate!($migrations_path) + .run(&pool) + .await + .map_err(|e| format!("Failed to run database migrations: {e}"))?; + return Ok(Self { pool }); + } + } } /// Component in charge of interacting with the underlying database. -/// -/// Currently works for `SQLite`. `PostgreSQL` should also be added in the future. #[derive(Debug)] pub struct DBM { - /// The underlying database connection. - connection: Connection, + pool: AnyPool, } impl DBM { - /// Creates the database tables if not present. - fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { - let tx = self.connection.transaction().unwrap(); - for table in tables.iter() { - tx.execute(table, [])?; - } - tx.commit() - } - - /// Generic method to store data into the database. - fn store_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.connection.execute(query, params) { - Ok(_) => Ok(()), - Err(e) => match e { - SqliteError::SqliteFailure(ie, _) => match ie.code { - ErrorCode::ConstraintViolation => match ie.extended_code { - SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), - SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - } - } + /// Creates a new [DBM] instance. + pub async fn new(connection_string: &str) -> Result { + #[cfg(not(any(feature = "sqlite", feature = "postgres")))] + compile_error!("Can't compile with no database drivers."); - /// Generic method to remove data from the database. - fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.connection.execute(query, params).unwrap() { - 0 => Err(Error::NotFound), - _ => Ok(()), - } - } + // Note that sqlx initiates `PRAGMA foreign_keys = ON` connections by default. + #[cfg(feature = "sqlite")] + return_db_if_matching!(connection_string, sqlite, "migrations/sqlite"); - /// Generic method to update data from the database. - fn update_data(&self, query: &str, params: P) -> Result<(), Error> { - // Updating data is fundamentally the same as deleting it in terms of interface. - // A query is sent and either no row is modified or some rows are - self.remove_data(query, params) - } -} + #[cfg(feature = "postgres")] + return_db_if_matching!(connection_string, postgres, "migrations/postgres"); -impl DBM { - /// Creates a new [DBM] instance. - pub fn new(db_path: PathBuf) -> Result { - let connection = Connection::open(db_path)?; - connection.execute("PRAGMA foreign_keys=1;", [])?; - let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; + let supported_dbs = vec![ + #[cfg(feature = "sqlite")] + "SQLite", + #[cfg(feature = "postgres")] + "PostgreSQL", + ]; - Ok(dbm) + Err(format!( + "The database connection string ({connection_string}) is invalid or isn't supported. Supported databases are: {supported_dbs:?}." + )) } /// Stores a user ([UserInfo]) into the database. - pub(crate) fn store_user(&self, user_id: UserId, user_info: &UserInfo) -> Result<(), Error> { - let query = - "INSERT INTO users (user_id, available_slots, subscription_start, subscription_expiry) VALUES (?1, ?2, ?3, ?4)"; - - match self.store_data( - query, - params![ - user_id.to_vec(), - user_info.available_slots, - user_info.subscription_start, - user_info.subscription_expiry, - ], - ) { - Ok(x) => { - log::debug!("User successfully stored: {user_id}"); - Ok(x) - } - Err(e) => { - log::error!("Couldn't store user: {user_id}. Error: {e:?}"); - Err(e) - } - } + pub(crate) async fn store_user( + &self, + user_id: UserId, + user_info: &UserInfo, + ) -> Result<(), Error> { + let sql = "INSERT INTO users (user_id, available_slots, subscription_start, subscription_expiry) VALUES ($1, $2, $3, $4)"; + sqlx::query(sql) + .bind(user_id.to_vec()) + .bind(user_info.available_slots as i64) + .bind(user_info.subscription_start as i64) + .bind(user_info.subscription_expiry as i64) + .execute(&self.pool) + .await + .map(|_| ()) } /// Updates an existing user ([UserInfo]) in the database. - pub(crate) fn update_user(&self, user_id: UserId, user_info: &UserInfo) { - let query = - "UPDATE users SET available_slots=(?1), subscription_start=(?2), subscription_expiry=(?3) WHERE user_id=(?4)"; - match self.update_data( - query, - params![ - user_info.available_slots, - user_info.subscription_start, - user_info.subscription_expiry, - user_id.to_vec(), - ], - ) { - Ok(_) => { - log::debug!("User's info successfully updated: {user_id}"); - } - Err(_) => { - log::error!("User not found, data cannot be updated: {user_id}"); - } - } + pub(crate) async fn update_user( + &self, + user_id: UserId, + user_info: &UserInfo, + ) -> Result<(), Error> { + let sql = "UPDATE users SET available_slots=($1), subscription_start=($2), subscription_expiry=($3) WHERE user_id=($4)"; + let updated_rows = sqlx::query(sql) + .bind(user_info.available_slots as i64) + .bind(user_info.subscription_start as i64) + .bind(user_info.subscription_expiry as i64) + .bind(user_id.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); + + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) } /// Loads the associated locators ([Locator]) of a given user ([UserId]). - pub(crate) fn load_user_locators(&self, user_id: UserId) -> Vec { - let mut stmt = self - .connection - .prepare("SELECT locator FROM appointments WHERE user_id=(?)") - .unwrap(); + pub(crate) async fn load_user_locators(&self, user_id: UserId) -> Vec { + sqlx::query("SELECT locator FROM appointments WHERE user_id=($1)") + .bind(user_id.to_vec()) + .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap() + } - stmt.query_map([user_id.to_vec()], |row| { - let raw_locator: Vec = row.get(0).unwrap(); - let locator = Locator::from_slice(&raw_locator).unwrap(); - Ok(locator) + /// Loads all users from the database. + pub(crate) async fn load_all_users(&self) -> HashMap { + sqlx::query( + "SELECT user_id, available_slots, subscription_start, subscription_expiry FROM users", + ) + .map(|row: AnyRow| { + ( + UserId::from_slice(row.get("user_id")).unwrap(), + UserInfo::new( + row.get::("available_slots") as u32, + row.get::("subscription_start") as u32, + row.get::("subscription_expiry") as u32, + ), + ) }) + .fetch_all(&self.pool) + .await .unwrap() - .map(|res| res.unwrap()) + .into_iter() .collect() } - /// Loads all users from the database. - pub(crate) fn load_all_users(&self) -> HashMap { - let mut users = HashMap::new(); - let mut stmt = self - .connection - .prepare("SELECT user_id, available_slots, subscription_start, subscription_expiry FROM users") - .unwrap(); - let mut rows = stmt.query([]).unwrap(); - - while let Ok(Some(row)) = rows.next() { - let raw_userid: Vec = row.get(0).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - let slots = row.get(1).unwrap(); - let start = row.get(2).unwrap(); - let expiry = row.get(3).unwrap(); - - users.insert(user_id, UserInfo::new(slots, start, expiry)); - } - - users - } - /// Removes some users from the database in batch. - pub(crate) fn batch_remove_users(&mut self, users: &Vec) -> usize { - let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; - let tx = self.connection.transaction().unwrap(); - let iter = users - .iter() - .map(|uuid| uuid.to_vec()) - .collect::>>(); - - for chunk in iter.chunks(limit) { - let query = "DELETE FROM users WHERE user_id IN ".to_owned(); - let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); - - match tx.execute(&format!("{query}{placeholders}"), params_from_iter(chunk)) { - Ok(_) => log::debug!("Users deletion added to db transaction"), - Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {e:?}"), + pub(crate) async fn batch_remove_users(&self, users: &Vec) -> usize { + let users: Vec<_> = users.iter().map(|uuid| uuid.to_vec()).collect(); + + for chunk in users.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("DELETE FROM users WHERE user_id IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for user_id in chunk { + query = query.bind(user_id); } + query.execute(&self.pool).await.unwrap(); } - match tx.commit() { - Ok(_) => log::debug!("Users successfully deleted"), - Err(e) => log::error!("Couldn't delete users. Error: {e:?}"), - } - - (users.len() as f64 / limit as f64).ceil() as usize + (users.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize } /// Get the number of stored appointments. - pub(crate) fn get_appointments_count(&self) -> usize { - let mut stmt = self - .connection - .prepare("SELECT COUNT(*) FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL") - .unwrap(); - stmt.query_row([], |row| row.get(0)).unwrap() + pub(crate) async fn get_appointments_count(&self) -> usize { + let sql = "SELECT COUNT(*) FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL"; + sqlx::query(sql) + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await + .unwrap() } /// Get the number of stored trackers. - pub(crate) fn get_trackers_count(&self) -> usize { - let mut stmt = self - .connection - .prepare("SELECT COUNT(*) FROM trackers") - .unwrap(); - stmt.query_row([], |row| row.get(0)).unwrap() + pub(crate) async fn get_trackers_count(&self) -> usize { + sqlx::query("SELECT COUNT(*) FROM trackers") + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await + .unwrap() } /// Stores an [Appointment] into the database. - pub(crate) fn store_appointment( + pub(crate) async fn store_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, ) -> Result<(), Error> { - let query = "INSERT INTO appointments (UUID, locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"; - match self.store_data( - query, - params![ - uuid.to_vec(), - appointment.locator().to_vec(), - appointment.encrypted_blob(), - appointment.to_self_delay(), - appointment.user_signature, - appointment.start_block, - appointment.user_id.to_vec(), - ], - ) { - Ok(x) => { - log::debug!("Appointment successfully stored: {uuid}"); - Ok(x) - } - Err(e) => { - log::error!("Couldn't store appointment: {uuid}. Error: {e:?}"); - Err(e) - } - } + let sql = "INSERT INTO appointments (UUID, locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .bind(appointment.locator().to_vec()) + .bind(appointment.encrypted_blob()) + .bind(appointment.to_self_delay() as i64) + .bind(&appointment.user_signature) + .bind(appointment.start_block as i64) + .bind(appointment.user_id.to_vec()) + .execute(&self.pool) + .await + .map(|_| ()) } /// Updates an existing [Appointment] in the database. - pub(crate) fn update_appointment( + pub(crate) async fn update_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, ) -> Result<(), Error> { // DISCUSS: Check what fields we'd like to make updatable. e_blob and signature are the obvious, to_self_delay and start_block may not be necessary (or even risky) - let query = - "UPDATE appointments SET encrypted_blob=(?1), to_self_delay=(?2), user_signature=(?3), start_block=(?4) WHERE UUID=(?5)"; - match self.update_data( - query, - params![ - appointment.encrypted_blob(), - appointment.to_self_delay(), - appointment.user_signature, - appointment.start_block, - uuid.to_vec(), - ], - ) { - Ok(_) => { - log::debug!("Appointment successfully updated: {uuid}"); - Ok(()) - } - Err(e) => { - log::error!("Appointment not found, data cannot be updated: {uuid}. Error: {e:?}"); - Err(e) - } - } + let sql = "UPDATE appointments SET encrypted_blob=($1), to_self_delay=($2), user_signature=($3), start_block=($4) WHERE UUID=($5)"; + let updated_rows = sqlx::query(sql) + .bind(appointment.encrypted_blob()) + .bind(appointment.to_self_delay() as i64) + .bind(&appointment.user_signature) + .bind(appointment.start_block as i64) + .bind(uuid.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); + + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) } /// Loads an [Appointment] from the database. - pub(crate) fn load_appointment(&self, uuid: UUID) -> Option { - let key = uuid.to_vec(); - let mut stmt = self - .connection - .prepare( - "SELECT locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id - FROM appointments WHERE UUID=(?)" - ) - .unwrap(); - - stmt.query_row([key], |row| { - let raw_locator: Vec = row.get(0).unwrap(); - let encrypted_blob = row.get(1).unwrap(); - let to_self_delay = row.get(2).unwrap(); - let user_signature = row.get(3).unwrap(); - let start_block = row.get(4).unwrap(); - let raw_userid: Vec = row.get(5).unwrap(); - - let locator = Locator::from_slice(&raw_locator).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - let appointment = Appointment::new(locator, encrypted_blob, to_self_delay); - Ok(ExtendedAppointment::new( - appointment, - user_id, - user_signature, - start_block, - )) - }) - .ok() + pub(crate) async fn load_appointment(&self, uuid: UUID) -> Option { + let sql = "SELECT locator, encrypted_blob, to_self_delay, user_id, user_signature, start_block FROM appointments WHERE UUID=($1)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + ExtendedAppointment::new( + Appointment::new( + Locator::from_slice(row.get("locator")).unwrap(), + row.get("encrypted_blob"), + row.get::("to_self_delay") as u32, + ), + UserId::from_slice(row.get("user_id")).unwrap(), + row.get("user_signature"), + row.get::("start_block") as u32, + ) + }) + .fetch_one(&self.pool) + .await + .ok() } /// Check if an appointment with `uuid` exists. - pub(crate) fn appointment_exists(&self, uuid: UUID) -> bool { - self.connection - .prepare("SELECT UUID FROM appointments WHERE UUID=(?)") - .unwrap() - .exists([uuid.to_vec()]) - .unwrap() + pub(crate) async fn appointment_exists(&self, uuid: UUID) -> bool { + sqlx::query("SELECT UUID FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .fetch_one(&self.pool) + .await + .is_ok() } /// Loads appointments from the database. If a locator is given, this method loads only the appointments /// matching this locator. If no locator is given, all the appointments in the database would be returned. - pub(crate) fn load_appointments( + pub(crate) async fn load_appointments( &self, locator: Option, ) -> HashMap { - let mut appointments = HashMap::new(); + let mut sql = "SELECT a.UUID, a.locator, a.encrypted_blob, a.to_self_delay, a.user_id, a.user_signature, a.start_block FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL".to_string(); - let mut sql = - "SELECT a.UUID, a.locator, a.encrypted_blob, a.to_self_delay, a.user_signature, a.start_block, a.user_id - FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL".to_string(); // If a locator was passed, filter based on it. - if locator.is_some() { - sql.push_str(" AND a.locator=(?)"); - } - let mut stmt = self.connection.prepare(&sql).unwrap(); - - let mut rows = if let Some(locator) = locator { - stmt.query([locator.to_vec()]).unwrap() + let query = if let Some(locator) = locator { + sql.push_str(" AND a.locator=($1)"); + sqlx::query(&sql).bind(locator.to_vec()) } else { - stmt.query([]).unwrap() + sqlx::query(&sql) }; - while let Ok(Some(row)) = rows.next() { - let raw_uuid: Vec = row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid[0..20]).unwrap(); - let raw_locator: Vec = row.get(1).unwrap(); - let locator = Locator::from_slice(&raw_locator).unwrap(); - let raw_userid: Vec = row.get(6).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - - let appointment = Appointment::new(locator, row.get(2).unwrap(), row.get(3).unwrap()); - - appointments.insert( - uuid, - ExtendedAppointment::new( - appointment, - user_id, - row.get(4).unwrap(), - row.get(5).unwrap(), - ), - ); - } - - appointments + query + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + ExtendedAppointment::new( + Appointment::new( + Locator::from_slice(row.get(1)).unwrap(), + row.get(2), + row.get::(3) as u32, + ), + UserId::from_slice(row.get(4)).unwrap(), + row.get(5), + row.get::(6) as u32, + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() } /// Gets the length of an appointment (the length of `appointment.encrypted_blob`). - pub(crate) fn get_appointment_length(&self, uuid: UUID) -> Option { - let mut stmt = self - .connection - .prepare("SELECT length(encrypted_blob) FROM appointments WHERE UUID=(?)") - .unwrap(); - - stmt.query_row([uuid.to_vec()], |row| row.get(0)).ok() + pub(crate) async fn get_appointment_length(&self, uuid: UUID) -> Result { + sqlx::query("SELECT length(encrypted_blob) FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .map(|row: AnyRow| row.get::(0) as usize) + .fetch_one(&self.pool) + .await } /// Gets the [`UserId`] of the owner of the appointment along with the appointment /// length (same as [DBM::get_appointment_length]) for `uuid`. - pub(crate) fn get_appointment_user_and_length(&self, uuid: UUID) -> Option<(UserId, usize)> { - let mut stmt = self - .connection - .prepare("SELECT user_id, length(encrypted_blob) FROM appointments WHERE UUID=(?)") - .unwrap(); - - stmt.query_row([uuid.to_vec()], |row| { - let raw_userid: Vec = row.get(0).unwrap(); - let length = row.get(1).unwrap(); - Ok((UserId::from_slice(&raw_userid).unwrap(), length)) - }) - .ok() + pub(crate) async fn get_appointment_user_and_length( + &self, + uuid: UUID, + ) -> Result<(UserId, usize), Error> { + sqlx::query("SELECT user_id, length(encrypted_blob) FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + ( + UserId::from_slice(row.get("user_id")).unwrap(), + row.get::(1) as usize, + ) + }) + .fetch_one(&self.pool) + .await } /// Removes an [Appointment] from the database. - pub(crate) fn remove_appointment(&self, uuid: UUID) { - let query = "DELETE FROM appointments WHERE UUID=(?)"; - match self.remove_data(query, params![uuid.to_vec()]) { - Ok(_) => { - log::debug!("Appointment successfully removed: {uuid}"); - } - Err(_) => { - log::error!("Appointment not found, data cannot be removed: {uuid}"); - } + pub(crate) async fn remove_appointment(&self, uuid: UUID) { + if let Err(e) = sqlx::query("DELETE FROM appointments WHERE UUID=($1)") + .execute(&self.pool) + .await + { + log::error!("Failed to remove appointment ({uuid}) due to: {e}") } } /// Removes some appointments from the database in batch and updates the associated users /// (giving back freed appointment slots) in one transaction so that the deletion and the /// update is atomic. - pub(crate) fn batch_remove_appointments( - &mut self, + pub(crate) async fn batch_remove_appointments( + &self, appointments: &Vec, updated_users: &HashMap, ) -> usize { - let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; - let tx = self.connection.transaction().unwrap(); - let iter = appointments - .iter() - .map(|uuid| uuid.to_vec()) - .collect::>>(); - - for chunk in iter.chunks(limit) { - let query = "DELETE FROM appointments WHERE UUID IN ".to_owned(); - let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); - - match tx.execute(&format!("{query}{placeholders}"), params_from_iter(chunk)) { - Ok(_) => log::debug!("Appointments deletion added to db transaction"), - Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {e:?}"), + let uuids: Vec<_> = appointments.iter().map(|uuid| uuid.to_vec()).collect(); + let mut tx = self.pool.begin().await.unwrap(); + + for chunk in uuids.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("DELETE FROM appointments WHERE UUID IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for uuid in chunk { + query = query.bind(uuid); } + query.execute(&mut *tx).await.unwrap(); } - for (id, info) in updated_users.iter() { - let query = "UPDATE users SET available_slots=(?1) WHERE user_id=(?2)"; - match tx.execute(query, params![info.available_slots, id.to_vec(),]) { - Ok(_) => log::debug!("User update added to db transaction"), - Err(e) => log::error!("Couldn't add update query to transaction. Error: {e:?}"), - }; + for (user_id, info) in updated_users.iter() { + sqlx::query("UPDATE users SET available_slots=($1) WHERE user_id=($2)") + .bind(info.available_slots as i64) + .bind(user_id.to_vec()) + .execute(&mut *tx) + .await + .unwrap(); } - match tx.commit() { - Ok(_) => log::debug!("Appointments successfully deleted"), - Err(e) => log::error!("Couldn't delete appointments. Error: {e:?}"), + if let Err(e) = tx.commit().await { + log::error!("Failed to remove appointments in batch: {e}") } - (appointments.len() as f64 / limit as f64).ceil() as usize + (uuids.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize } /// Loads the [`UUID`]s of appointments triggered by `locator`. - pub(crate) fn load_uuids(&self, locator: Locator) -> Vec { - let mut stmt = self - .connection - .prepare("SELECT UUID from appointments WHERE locator=(?)") - .unwrap(); - - stmt.query_map([locator.to_vec()], |row| { - let raw_uuid: Vec = row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid).unwrap(); - Ok(uuid) - }) - .unwrap() - .map(|uuid_res| uuid_res.unwrap()) - .collect() + pub(crate) async fn load_uuids(&self, locator: Locator) -> Vec { + sqlx::query("SELECT UUID from appointments WHERE locator=($1)") + .bind(locator.to_vec()) + .map(|row: AnyRow| UUID::from_slice(row.get("UUID")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap() } /// Filters the given set of [`Locator`]s by including only the ones which trigger any of our stored appointments. - pub(crate) fn batch_check_locators_exist(&self, locators: Vec<&Locator>) -> Vec { + pub(crate) async fn batch_check_locators_exist(&self, locators: Vec<&Locator>) -> Vec { let mut registered_locators = Vec::new(); - let locators: Vec> = locators.iter().map(|l| l.to_vec()).collect(); - let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; - - for chunk in locators.chunks(limit) { - let query = "SELECT locator FROM appointments WHERE locator IN ".to_owned(); - let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); - - let mut stmt = self - .connection - .prepare(&format!("{query}{placeholders}")) - .unwrap(); - let known_locators = stmt - .query_map(params_from_iter(chunk), |row| { - let raw_locator: Vec = row.get(0).unwrap(); - let locator = Locator::from_slice(&raw_locator).unwrap(); - Ok(locator) - }) - .unwrap() - .map(|locator_res| locator_res.unwrap()); - registered_locators.extend(known_locators); + let locators: Vec<_> = locators.iter().map(|l| l.to_vec()).collect(); + + for chunk in locators.chunks(SQL_VARIABLE_LIMIT) { + let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); + let placeholders = format!("${}", str_indices.join(", $")); + let sql = format!("SELECT locator FROM appointments WHERE locator IN ({placeholders})"); + let mut query = sqlx::query(&sql); + for locator in chunk { + query = query.bind(locator); + } + registered_locators.extend( + query + .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) + .fetch_all(&self.pool) + .await + .unwrap(), + ) } registered_locators } /// Stores a [TransactionTracker] into the database. - pub(crate) fn store_tracker( + pub(crate) async fn store_tracker( &self, uuid: UUID, tracker: &TransactionTracker, ) -> Result<(), Error> { - let (height, confirmed) = tracker.status.to_db_data().ok_or(Error::MissingField)?; - - let query = - "INSERT INTO trackers (UUID, dispute_tx, penalty_tx, height, confirmed) VALUES (?1, ?2, ?3, ?4, ?5)"; - match self.store_data( - query, - params![ - uuid.to_vec(), - consensus::serialize(&tracker.dispute_tx), - consensus::serialize(&tracker.penalty_tx), - height, - confirmed, - ], - ) { - Ok(x) => { - log::debug!("Tracker successfully stored: {uuid}"); - Ok(x) - } - Err(e) => { - log::error!("Couldn't store tracker: {uuid}. Error: {e:?}"); - Err(e) - } - } + let (height, confirmed) = tracker + .status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; + let sql = "INSERT INTO trackers (UUID, dispute_tx, penalty_tx, height, confirmed) VALUES ($1, $2, $3, $4, $5)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .bind(consensus::serialize(&tracker.dispute_tx)) + .bind(consensus::serialize(&tracker.penalty_tx)) + .bind(height as i64) + .bind(confirmed as i64) + .execute(&self.pool) + .await + .map(|_| ()) } /// Updates the tracker status in the database. /// /// The only updatable fields are `height` and `confirmed`. - pub(crate) fn update_tracker_status( + pub(crate) async fn update_tracker_status( &self, uuid: UUID, status: &ConfirmationStatus, ) -> Result<(), Error> { - let (height, confirmed) = status.to_db_data().ok_or(Error::MissingField)?; + let (height, confirmed) = status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; + let updated_rows = + sqlx::query("UPDATE trackers SET height=($1), confirmed=($2) WHERE UUID=($3)") + .bind(height as i64) + .bind(confirmed as i64) + .bind(uuid.to_vec()) + .execute(&self.pool) + .await? + .rows_affected(); - let query = "UPDATE trackers SET height=(?1), confirmed=(?2) WHERE UUID=(?3)"; - match self.update_data(query, params![height, confirmed, uuid.to_vec(),]) { - Ok(x) => { - log::debug!("Tracker successfully updated: {uuid}"); - Ok(x) - } - Err(e) => { - log::error!("Couldn't update tracker: {uuid}. Error: {e:?}"); - Err(e) - } - } + (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) } /// Loads a [TransactionTracker] from the database. - pub(crate) fn load_tracker(&self, uuid: UUID) -> Option { - let key = uuid.to_vec(); - let mut stmt = self - .connection.prepare( - "SELECT t.dispute_tx, t.penalty_tx, t.height, t.confirmed, a.user_id - FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=(?)" - ) - .unwrap(); - - stmt.query_row([key], |row| { - let raw_dispute_tx: Vec = row.get(0).unwrap(); - let raw_penalty_tx: Vec = row.get(1).unwrap(); - let height: u32 = row.get(2).unwrap(); - let confirmed: bool = row.get(3).unwrap(); - let raw_userid: Vec = row.get(4).unwrap(); - - let dispute_tx = consensus::deserialize(&raw_dispute_tx).unwrap(); - let penalty_tx = consensus::deserialize(&raw_penalty_tx).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - - Ok(TransactionTracker { - dispute_tx, - penalty_tx, - status: ConfirmationStatus::from_db_data(height, confirmed), - user_id, + pub(crate) async fn load_tracker(&self, uuid: UUID) -> Option { + let sql = "SELECT t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=($1)"; + sqlx::query(sql) + .bind(uuid.to_vec()) + .map(|row: AnyRow| { + TransactionTracker::new( + Breach::new( + consensus::deserialize(row.get(0)).unwrap(), + consensus::deserialize(row.get(1)).unwrap(), + ), + UserId::from_slice(row.get(2)).unwrap(), + ConfirmationStatus::from_db_data( + row.get::(3) as u32, + row.get::(4) != 0, + ), + ) }) - }) - .ok() + .fetch_one(&self.pool) + .await + .ok() } /// Check if a tracker with `uuid` exists. - pub(crate) fn tracker_exists(&self, uuid: UUID) -> bool { - self.connection - .prepare("SELECT UUID FROM trackers WHERE UUID=(?)") - .unwrap() - .exists([uuid.to_vec()]) - .unwrap() + pub(crate) async fn tracker_exists(&self, uuid: UUID) -> bool { + sqlx::query("SELECT UUID FROM trackers WHERE UUID=($1)") + .bind(uuid.to_vec()) + .fetch_one(&self.pool) + .await + .is_ok() } /// Loads trackers from the database. If a locator is given, this method loads only the trackers /// matching this locator. If no locator is given, all the trackers in the database would be returned. - pub(crate) fn load_trackers( + pub(crate) async fn load_trackers( &self, locator: Option, ) -> HashMap { - let mut trackers = HashMap::new(); + let mut sql = "SELECT t.UUID, t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID".to_string(); - let mut sql = "SELECT t.UUID, t.dispute_tx, t.penalty_tx, t.height, t.confirmed, a.user_id - FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID" - .to_string(); // If a locator was passed, filter based on it. - if locator.is_some() { - sql.push_str(" WHERE a.locator=(?)"); - } - let mut stmt = self.connection.prepare(&sql).unwrap(); - - let mut rows = if let Some(locator) = locator { - stmt.query([locator.to_vec()]).unwrap() + let query = if let Some(locator) = locator { + sql.push_str(" WHERE a.locator=($1)"); + sqlx::query(&sql).bind(locator.to_vec()) } else { - stmt.query([]).unwrap() + sqlx::query(&sql) }; - while let Ok(Some(row)) = rows.next() { - let raw_uuid: Vec = row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid[0..20]).unwrap(); - let raw_dispute_tx: Vec = row.get(1).unwrap(); - let dispute_tx = consensus::deserialize(&raw_dispute_tx).unwrap(); - let raw_penalty_tx: Vec = row.get(2).unwrap(); - let penalty_tx = consensus::deserialize(&raw_penalty_tx).unwrap(); - let height: u32 = row.get(3).unwrap(); - let confirmed: bool = row.get(4).unwrap(); - let raw_userid: Vec = row.get(5).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - - trackers.insert( - uuid, - TransactionTracker { - dispute_tx, - penalty_tx, - status: ConfirmationStatus::from_db_data(height, confirmed), - user_id, - }, - ); - } - - trackers + query + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + TransactionTracker::new( + Breach::new( + consensus::deserialize(row.get(1)).unwrap(), + consensus::deserialize(row.get(2)).unwrap(), + ), + UserId::from_slice(row.get(3)).unwrap(), + ConfirmationStatus::from_db_data( + row.get::(4) as u32, + row.get::(5) != 0, + ), + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() } /// Loads trackers with the given confirmation status. /// /// Note that for [`ConfirmationStatus::InMempoolSince(height)`] variant, this pulls trackers /// with `h <= height` and not just `h = height`. - pub(crate) fn load_trackers_with_confirmation_status( + pub(crate) async fn load_trackers_with_confirmation_status( &self, status: ConfirmationStatus, ) -> Result, Error> { - let (height, confirmed) = status.to_db_data().ok_or(Error::MissingField)?; - let sql = format!( - "SELECT UUID FROM trackers WHERE confirmed=(?1) AND height{}(?2)", - if confirmed { "=" } else { "<=" } - ); - let mut stmt = self.connection.prepare(&sql).unwrap(); + let (height, confirmed) = status + .to_db_data() + .ok_or(Error::Decode("Tracker status isn't storable".into()))?; - Ok(stmt - .query_map(params![confirmed, height], |row| { - let raw_uuid: Vec = row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid).unwrap(); - Ok(uuid) - }) - .unwrap() - .map(|uuid_res| uuid_res.unwrap()) - .collect()) + sqlx::query(&format!( + "SELECT UUID FROM trackers WHERE confirmed=($1) AND height{}($2)", + if confirmed { "=" } else { "<=" } + )) + .bind(confirmed as i64) + .bind(height as i64) + .map(|row: AnyRow| UUID::from_slice(row.get(0)).unwrap()) + .fetch_all(&self.pool) + .await } /// Loads the transaction IDs of all the penalties and their status from the database. - pub(crate) fn load_penalties_summaries(&self) -> HashMap { - let mut summaries = HashMap::new(); - - let mut stmt = self - .connection - .prepare( - "SELECT t.UUID, t.penalty_tx, t.height, t.confirmed - FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID", - ) - .unwrap(); - let mut rows = stmt.query([]).unwrap(); - - while let Ok(Some(row)) = rows.next() { - let raw_uuid: Vec = row.get(0).unwrap(); - let raw_penalty_tx: Vec = row.get(1).unwrap(); - let height: u32 = row.get(2).unwrap(); - let confirmed: bool = row.get(3).unwrap(); - - // DISCUSS: Should we store the txids to avoid pulling raw txs and deserializing then hashing them. - let penalty_txid = consensus::deserialize::(&raw_penalty_tx) - .unwrap() - .txid(); - summaries.insert( - UUID::from_slice(&raw_uuid).unwrap(), - PenaltySummary::new( - penalty_txid, - ConfirmationStatus::from_db_data(height, confirmed), - ), - ); - } - summaries + pub(crate) async fn load_penalties_summaries(&self) -> HashMap { + let sql = "SELECT t.UUID, t.penalty_tx, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID"; + sqlx::query(sql) + .map(|row: AnyRow| { + ( + UUID::from_slice(row.get(0)).unwrap(), + PenaltySummary::new( + consensus::deserialize::(row.get(1)) + .unwrap() + .txid(), + ConfirmationStatus::from_db_data( + row.get::(2) as u32, + row.get::(3) != 0, + ), + ), + ) + }) + .fetch_all(&self.pool) + .await + .unwrap() + .into_iter() + .collect() } /// Stores the last known block into the database. - pub(crate) fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { - let query = "INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, ?)"; - self.store_data(query, params![block_hash.to_vec()]) + pub(crate) async fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { + sqlx::query("INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, $1)") + .bind(block_hash.to_vec()) + .execute(&self.pool) + .await + .map(|_| ()) } /// Loads the last known block from the database. - pub fn load_last_known_block(&self) -> Option { - let mut stmt = self - .connection - .prepare("SELECT block_hash FROM last_known_block WHERE id=0") - .unwrap(); - - stmt.query_row([], |row| { - let raw_hash: Vec = row.get(0).unwrap(); - Ok(BlockHash::from_slice(&raw_hash).unwrap()) - }) - .ok() + pub async fn load_last_known_block(&self) -> Option { + sqlx::query("SELECT block_hash FROM last_known_block WHERE id=0") + .map(|row: AnyRow| BlockHash::from_slice(row.get("block_hash")).unwrap()) + .fetch_one(&self.pool) + .await + .ok() } /// Stores the tower secret key into the database. /// /// When a new key is generated, old keys are not overwritten but are not retrievable from the API either. - pub fn store_tower_key(&self, sk: &SecretKey) -> Result<(), Error> { - let query = "INSERT INTO keys (key) VALUES (?)"; - self.store_data(query, params![sk.display_secret().to_string()]) + pub async fn store_tower_key(&self, sk: &SecretKey) -> Result<(), Error> { + sqlx::query("INSERT INTO keys (secret_key) VALUES ($1)") + .bind(sk.display_secret().to_string()) + .execute(&self.pool) + .await + .map(|_| ()) } /// Loads the last known tower secret key from the database. /// /// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed, /// but they are not accessible from the API either. - pub fn load_tower_key(&self) -> Option { - let mut stmt = self - .connection - .prepare( - "SELECT key FROM keys WHERE id = (SELECT seq FROM sqlite_sequence WHERE name=(?))", - ) - .unwrap(); - - stmt.query_row(["keys"], |row| { - let sk: String = row.get(0).unwrap(); - Ok(SecretKey::from_str(&sk).unwrap()) - }) - .ok() + pub async fn load_tower_key(&self) -> Option { + sqlx::query("SELECT secret_key FROM keys WHERE id = (SELECT MAX(id) from keys)") + .map(|row: AnyRow| SecretKey::from_str(row.get("secret_key")).unwrap()) + .fetch_one(&self.pool) + .await + .ok() } } #[cfg(test)] mod tests { use super::*; + use std::collections::HashSet; use std::iter::FromIterator; @@ -810,141 +635,151 @@ mod tests { }; impl DBM { - pub(crate) fn in_memory() -> Result { - let connection = Connection::open_in_memory()?; - connection.execute("PRAGMA foreign_keys=1;", [])?; - let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; + #[cfg(feature = "sqlite")] + async fn memory() -> Self { + use sqlx::any::AnyPoolOptions; + install_drivers(&[sqlx::sqlite::any::DRIVER]).ok(); + let pool = AnyPoolOptions::new() + // Each new connection actually creates a brand new DB: https://github.com/launchbadge/sqlx/issues/2510 + // So make sure the pool doesn't create more than one connection. + .max_connections(1) + .connect("sqlite::memory:") + .await + .unwrap(); + sqlx::migrate!("migrations/sqlite") + .run(&pool) + .await + .unwrap(); + Self { pool } + } - Ok(dbm) + #[cfg(feature = "postgres")] + async fn postgres() -> Self { + (|| async { + return_db_if_matching!( + "postgres://user:pass@localhost/teos", + postgres, + "migrations/postgres" + ); + Err(format!("Unreachable (the macro above will always match)")) + })() + .await + .unwrap() } - pub(crate) fn load_user(&self, user_id: UserId) -> Option { - let key = user_id.to_vec(); - let mut stmt = self - .connection - .prepare( - "SELECT available_slots, subscription_start, subscription_expiry - FROM users WHERE user_id=(?)", - ) - .unwrap(); - stmt.query_row([&key], |row| { - let slots = row.get(0).unwrap(); - let start = row.get(1).unwrap(); - let expiry = row.get(2).unwrap(); - Ok(UserInfo::new(slots, start, expiry)) - }) - .ok() + #[allow(unreachable_code)] + /// Returns a new [DBM], preferring sqlite over postgres if available. + pub(crate) async fn test_db() -> Self { + #[cfg(feature = "sqlite")] + return Self::memory().await; + + #[cfg(feature = "postgres")] + return Self::postgres().await; + + panic!("No database driver available. Make sure you compile with sqlite and/or postgres features enabled.") } - } - #[test] - fn test_create_tables() { - let connection = Connection::open_in_memory().unwrap(); - let mut dbm = DBM { connection }; - dbm.create_tables(Vec::from_iter(TABLES)).unwrap(); + pub(crate) async fn load_user(&self, user_id: UserId) -> Option { + let sql = "SELECT available_slots, subscription_start, subscription_expiry FROM users WHERE user_id=($1)"; + sqlx::query(sql) + .bind(user_id.to_vec()) + .map(|row: AnyRow| { + UserInfo::new( + row.get::("available_slots") as u32, + row.get::("subscription_start") as u32, + row.get::("subscription_expiry") as u32, + ) + }) + .fetch_one(&self.pool) + .await + .ok() + } } - #[test] - fn test_store_load_user() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_user() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); - let mut user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - - assert!(matches!(dbm.store_user(user_id, &user), Ok { .. })); - assert_eq!(dbm.load_user(user_id).unwrap(), user); + let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - // User info should be updatable but only via the update_user method - user = UserInfo::new(AVAILABLE_SLOTS * 2, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - assert!(matches!( - dbm.store_user(user_id, &user), - Err(Error::AlreadyExists) - )); - } + assert!(dbm.load_user(user_id).await.is_none()); - #[test] - fn test_load_nonexistent_user() { - let dbm = DBM::in_memory().unwrap(); + dbm.store_user(user_id, &user_info).await.unwrap(); + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); - let user_id = get_random_user_id(); - assert!(dbm.load_user(user_id).is_none()); + // Store an existing user should error (should use update_user). + dbm.store_user(user_id, &user_info).await.unwrap_err(); } - #[test] - fn test_update_user() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_update_user() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); - let mut user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let mut user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); - assert_eq!(dbm.load_user(user_id).unwrap(), user); + dbm.store_user(user_id, &user_info).await.unwrap(); - user.available_slots *= 2; - dbm.update_user(user_id, &user); - assert_eq!(dbm.load_user(user_id).unwrap(), user); + user_info.available_slots *= 2; + dbm.update_user(user_id, &user_info).await.unwrap(); + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); } - #[test] - fn test_load_user_locators() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_user_locators() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user_info).await.unwrap(); let mut locators = HashSet::new(); // Add some appointments to the user for _ in 0..10 { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); locators.insert(appointment.locator()); } - assert_eq!(dbm.load_user(user_id).unwrap(), user); + assert_eq!(dbm.load_user(user_id).await, Some(user_info)); assert_eq!( - HashSet::from_iter(dbm.load_user_locators(user_id)), + HashSet::from_iter(dbm.load_user_locators(user_id).await), locators ); } - #[test] - fn test_load_all_users() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_all_users() { + let dbm = DBM::test_db().await; let mut users = HashMap::new(); for i in 1..11 { let user_id = get_random_user_id(); - let user = UserInfo::new( + let user_info = UserInfo::new( AVAILABLE_SLOTS + i, SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - users.insert(user_id, user); - dbm.store_user(user_id, &user).unwrap(); + users.insert(user_id, user_info.clone()); + dbm.store_user(user_id, &user_info).await.unwrap(); } - assert_eq!(dbm.load_all_users(), users); + assert_eq!(dbm.load_all_users().await, users); } - #[test] - fn test_batch_remove_users() { - let mut dbm = DBM::in_memory().unwrap(); - - // Set a limit value for the maximum number of variables in SQLite so we can - // test splitting big queries into chunks. - let limit = 10; - dbm.connection - .set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, limit); + #[tokio::test] + async fn test_batch_remove_users() { + let dbm = DBM::test_db().await; let mut to_be_deleted = Vec::new(); let mut rest = HashSet::new(); - for i in 1..100 { + + for i in 0..SQL_VARIABLE_LIMIT * 3 { let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); if i % 2 == 0 { to_be_deleted.push(user_id); @@ -953,16 +788,18 @@ mod tests { } } - // Check that the db transaction had 5 (100/2*10) queries on it - assert_eq!(dbm.batch_remove_users(&to_be_deleted), 5); + // SQL_VARIABLE_LIMIT is 10 for tests, + // Check that deletion had `ceil(10 * 3 / 2) / 10` (2) queries on it + assert_eq!(dbm.batch_remove_users(&to_be_deleted).await, 2); + // Check user data was deleted - assert_eq!(rest, dbm.load_all_users().keys().cloned().collect()); + assert_eq!(rest, dbm.load_all_users().await.keys().cloned().collect()); } - #[test] - fn test_batch_remove_users_cascade() { + #[tokio::test] + async fn test_batch_remove_users_cascade() { // Test that removing users cascade deleted appointments and trackers - let mut dbm = DBM::in_memory().unwrap(); + let dbm = DBM::test_db().await; let uuid = generate_uuid(); let appointment = generate_dummy_appointment(None); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. @@ -971,44 +808,38 @@ mod tests { // Add the user and link an appointment (this is usually done once the appointment) // is added after the user creation, but for the test purpose it can be done all at once. let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(appointment.user_id, &info).unwrap(); + dbm.store_user(appointment.user_id, &info).await.unwrap(); // Appointment only - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); + dbm.store_appointment(uuid, &appointment).await.unwrap(); - dbm.batch_remove_users(&vec![appointment.user_id]); - assert!(dbm.load_user(appointment.user_id).is_none()); - assert!(dbm.load_appointment(uuid).is_none()); + dbm.batch_remove_users(&vec![appointment.user_id]).await; + assert!(dbm.load_user(appointment.user_id).await.is_none()); + assert!(dbm.load_appointment(uuid).await.is_none()); // Appointment + Tracker - dbm.store_user(appointment.user_id, &info).unwrap(); - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); - assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); + dbm.store_user(appointment.user_id, &info).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); - dbm.batch_remove_users(&vec![appointment.user_id]); - assert!(dbm.load_user(appointment.user_id).is_none()); - assert!(dbm.load_appointment(uuid).is_none()); - assert!(dbm.load_tracker(uuid).is_none()); + dbm.batch_remove_users(&vec![appointment.user_id]).await; + assert!(dbm.load_user(appointment.user_id).await.is_none()); + assert!(dbm.load_appointment(uuid).await.is_none()); + assert!(dbm.load_tracker(uuid).await.is_none()); } - #[test] - fn test_batch_remove_nonexistent_users() { - let mut dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_batch_remove_nonexistent_users() { + let dbm = DBM::test_db().await; let users = (0..10).map(|_| get_random_user_id()).collect(); - // Test it does not fail even if the user does not exist (it will log though) - dbm.batch_remove_users(&users); + // Test it does not fail even if the user does not exist + dbm.batch_remove_users(&users).await; } - #[test] - fn test_get_appointments_trackers_count() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_get_appointments_trackers_count() { + let dbm = DBM::test_db().await; let n_users = 100; let n_app_per_user = 4; let n_trk_per_user = 6; @@ -1016,124 +847,171 @@ mod tests { for _ in 0..n_users { let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); // These are un-triggered appointments. for _ in 0..n_app_per_user { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); } // And these are triggered ones (trackers). for _ in 0..n_trk_per_user { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); } } - assert_eq!(dbm.get_appointments_count(), n_users * n_app_per_user); - assert_eq!(dbm.get_trackers_count(), n_users * n_trk_per_user); + assert_eq!(dbm.get_appointments_count().await, n_users * n_app_per_user); + assert_eq!(dbm.get_trackers_count().await, n_users * n_trk_per_user); } - #[test] - fn test_store_load_appointment() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_load_appointment() { + let dbm = DBM::test_db().await; // In order to add an appointment we need the associated user to be present let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); - assert_eq!(dbm.load_appointment(uuid).unwrap(), appointment); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!(dbm.load_appointment(uuid).await.unwrap(), appointment); // Appointment info should be updatable but only via the update_appointment method - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Err(Error::AlreadyExists) - )); + assert!(dbm + .store_appointment(uuid, &appointment) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_unique_violation()) } - #[test] - fn test_store_appointment_missing_user() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_appointment_missing_user() { + let dbm = DBM::test_db().await; let uuid = generate_uuid(); let appointment = generate_dummy_appointment(None); - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Err(Error::MissingForeignKey) - )); - assert!((dbm.load_tracker(uuid).is_none())); + assert!(dbm + .store_appointment(uuid, &appointment) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_foreign_key_violation()); + assert!((dbm.load_tracker(uuid).await.is_none())); } - #[test] - fn test_load_nonexistent_appointment() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_update_appointment() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + // Modify the appointment and update it + let mut modified_appointment = appointment; + modified_appointment.inner.encrypted_blob.reverse(); + + // Not all fields are updatable, create another appointment modifying fields that cannot be + let mut another_modified_appointment = modified_appointment.clone(); + another_modified_appointment.user_id = get_random_user_id(); + + // Check how only the modifiable fields have been updated + dbm.update_appointment(uuid, &another_modified_appointment) + .await + .unwrap(); + assert_eq!( + dbm.load_appointment(uuid).await.unwrap(), + modified_appointment + ); + assert_ne!( + dbm.load_appointment(uuid).await.unwrap(), + another_modified_appointment + ); + } + + #[tokio::test] + async fn test_load_nonexistent_appointment() { + let dbm = DBM::test_db().await; let uuid = generate_uuid(); - assert!(dbm.load_appointment(uuid).is_none()); + assert!(dbm.load_appointment(uuid).await.is_none()); } - #[test] - fn test_appointment_exists() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_appointment_exists() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - assert!(!dbm.appointment_exists(uuid)); + assert!(!dbm.appointment_exists(uuid).await); - dbm.store_user(user_id, &user).unwrap(); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); - assert!(dbm.appointment_exists(uuid)); + assert!(dbm.appointment_exists(uuid).await); } - #[test] - fn test_update_appointment() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_get_appointment_length() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!( + dbm.get_appointment_length(uuid).await.unwrap(), + appointment.inner.encrypted_blob.len() + ); assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } + dbm.get_appointment_length(generate_uuid()).await, + Err(Error::RowNotFound) )); + } - // Modify the appointment and update it - let mut modified_appointment = appointment; - modified_appointment.inner.encrypted_blob.reverse(); + #[tokio::test] + async fn test_get_appointment_user_and_length() { + let dbm = DBM::test_db().await; - // Not all fields are updatable, create another appointment modifying fields that cannot be - let mut another_modified_appointment = modified_appointment.clone(); - another_modified_appointment.user_id = get_random_user_id(); + let user_id = get_random_user_id(); + let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - // Check how only the modifiable fields have been updated - dbm.update_appointment(uuid, &another_modified_appointment) - .unwrap(); - assert_eq!(dbm.load_appointment(uuid).unwrap(), modified_appointment); - assert_ne!( - dbm.load_appointment(uuid).unwrap(), - another_modified_appointment + dbm.store_user(user_id, &user).await.unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + assert_eq!( + dbm.get_appointment_user_and_length(uuid).await.unwrap(), + (user_id, appointment.encrypted_blob().len()) ); + assert!(matches!( + dbm.get_appointment_user_and_length(generate_uuid()).await, + Err(Error::RowNotFound) + )); } - #[test] - fn test_load_all_appointments() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_all_appointments() { + let dbm = DBM::test_db().await; let mut appointments = HashMap::new(); for i in 1..11 { @@ -1143,35 +1021,35 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); appointments.insert(uuid, appointment); } - assert_eq!(dbm.load_appointments(None), appointments); + assert_eq!(dbm.load_appointments(None).await, appointments); // If an appointment has an associated tracker, it should not be loaded since it is seen // as a triggered appointment let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); // We should get all the appointments back except from the triggered one - assert_eq!(dbm.load_appointments(None), appointments); + assert_eq!(dbm.load_appointments(None).await, appointments); } - #[test] - fn test_load_appointments_with_locator() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_appointments_with_locator() { + let dbm = DBM::test_db().await; let mut appointments = HashMap::new(); let dispute_tx = get_random_tx(); let dispute_txid = dispute_tx.txid(); @@ -1184,91 +1062,47 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); // Let some appointments belong to a specific dispute tx and some with random ones. // We will use the locator for that dispute tx to query these appointments. if i % 2 == 0 { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // Store the appointments made using our dispute tx. appointments.insert(uuid, appointment); } else { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); } } // Validate that no other appointments than the ones with our locator are returned. - assert_eq!(dbm.load_appointments(Some(locator)), appointments); + assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); // If an appointment has an associated tracker, it should not be loaded since it is seen // as a triggered appointment let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); // Generate an appointment for our dispute tx, thus it gets the same locator as the ones generated above. let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); // We should get all the appointments matching our locator back except from the triggered one - assert_eq!(dbm.load_appointments(Some(locator)), appointments); - } - - #[test] - fn test_get_appointment_length() { - let dbm = DBM::in_memory().unwrap(); - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - dbm.store_user(user_id, &user).unwrap(); - dbm.store_appointment(uuid, &appointment).unwrap(); - - assert_eq!( - dbm.get_appointment_length(uuid).unwrap(), - appointment.inner.encrypted_blob.len() - ); - assert!(dbm.get_appointment_length(generate_uuid()).is_none()); - } - - #[test] - fn test_get_appointment_user_and_length() { - let dbm = DBM::in_memory().unwrap(); - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - dbm.store_user(user_id, &user).unwrap(); - dbm.store_appointment(uuid, &appointment).unwrap(); - - assert_eq!( - dbm.get_appointment_user_and_length(uuid).unwrap(), - (user_id, appointment.encrypted_blob().len()) - ); - assert!(dbm - .get_appointment_user_and_length(generate_uuid()) - .is_none()); + assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); } - #[test] - fn test_batch_remove_appointments() { - let mut dbm = DBM::in_memory().unwrap(); - - // Set a limit value for the maximum number of variables in SQLite so we can - // test splitting big queries into chunks. - let limit = 10; - dbm.connection - .set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, limit); + #[tokio::test] + async fn test_batch_remove_appointments() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); let mut user = UserInfo::new( @@ -1276,14 +1110,14 @@ mod tests { SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let mut rest = HashSet::new(); for i in 1..6 { let mut to_be_deleted = Vec::new(); - for j in 0..limit * 2 * i { + for j in 0..SQL_VARIABLE_LIMIT * 2 * i { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); if j % 2 == 0 { to_be_deleted.push(uuid); @@ -1299,21 +1133,25 @@ mod tests { // Check that the db transaction had i queries on it assert_eq!( - dbm.batch_remove_appointments(&to_be_deleted, &updated_users), + dbm.batch_remove_appointments(&to_be_deleted, &updated_users) + .await, i as usize ); // Check appointment data was deleted and users properly updated - assert_eq!(rest, dbm.load_appointments(None).keys().cloned().collect()); assert_eq!( - dbm.load_user(user_id).unwrap().available_slots, + rest, + dbm.load_appointments(None).await.keys().cloned().collect() + ); + assert_eq!( + dbm.load_user(user_id).await.unwrap().available_slots, user.available_slots ); } } - #[test] - fn test_batch_remove_appointments_cascade() { - let mut dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_batch_remove_appointments_cascade() { + let dbm = DBM::test_db().await; let uuid = generate_uuid(); let appointment = generate_dummy_appointment(None); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. @@ -1322,47 +1160,45 @@ mod tests { let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); // Add the user b/c of FK restrictions - dbm.store_user(appointment.user_id, &info).unwrap(); + dbm.store_user(appointment.user_id, &info).await.unwrap(); + println!("{}", appointment.inner.to_self_delay); // Appointment only - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); + dbm.store_appointment(uuid, &appointment).await.unwrap(); dbm.batch_remove_appointments( &vec![uuid], - &HashMap::from_iter([(appointment.user_id, info)]), - ); - assert!(dbm.load_appointment(uuid).is_none()); + &HashMap::from_iter([(appointment.user_id, info.clone())]), + ) + .await; + assert!(dbm.load_appointment(uuid).await.is_none()); // Appointment + Tracker - assert!(matches!( - dbm.store_appointment(uuid, &appointment), - Ok { .. } - )); - assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); dbm.batch_remove_appointments( &vec![uuid], &HashMap::from_iter([(appointment.user_id, info)]), - ); - assert!(dbm.load_appointment(uuid).is_none()); - assert!(dbm.load_tracker(uuid).is_none()); + ) + .await; + assert!(dbm.load_appointment(uuid).await.is_none()); + assert!(dbm.load_tracker(uuid).await.is_none()); } - #[test] - fn test_batch_remove_nonexistent_appointments() { - let mut dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_batch_remove_nonexistent_appointments() { + let dbm = DBM::test_db().await; let appointments = (0..10).map(|_| generate_uuid()).collect(); - // Test it does not fail even if the user does not exist (it will log though) - dbm.batch_remove_appointments(&appointments, &HashMap::new()); + // Test it does not fail even if the user does not exist + dbm.batch_remove_appointments(&appointments, &HashMap::new()) + .await; } - #[test] - fn test_load_uuids() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_uuids() { + let dbm = DBM::test_db().await; let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); let dispute_tx = get_random_tx(); @@ -1372,11 +1208,11 @@ mod tests { // Add ten appointments triggered by the same locator. for _ in 0..10 { let user_id = get_random_user_id(); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); uuids.insert(uuid); } @@ -1384,23 +1220,23 @@ mod tests { // Add ten more appointments triggered by different locators. for _ in 0..10 { let user_id = get_random_user_id(); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let dispute_txid = get_random_tx().txid(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); } assert_eq!( - HashSet::from_iter(dbm.load_uuids(Locator::new(dispute_txid))), + HashSet::from_iter(dbm.load_uuids(Locator::new(dispute_txid)).await), uuids ); } - #[test] - fn test_batch_check_locators_exist() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_batch_check_locators_exist() { + let dbm = DBM::test_db().await; // Generate `n_app` appointments which we will store in the DB. let n_app = 100; let appointments: Vec<_> = (0..n_app) @@ -1410,12 +1246,13 @@ mod tests { // Register all the users beforehand. for user_id in appointments.iter().map(|a| a.user_id) { let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); } // Store all the `n_app` appointments. for appointment in appointments.iter() { dbm.store_appointment(appointment.uuid(), appointment) + .await .unwrap(); } @@ -1433,55 +1270,58 @@ mod tests { .collect(); assert_eq!( - HashSet::from_iter(dbm.batch_check_locators_exist(all_locators)), + HashSet::from_iter(dbm.batch_check_locators_exist(all_locators).await), known_locators ); } - #[test] - fn test_store_load_tracker() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_load_tracker() { + let dbm = DBM::test_db().await; // In order to add a tracker we need the associated appointment to be present (which // at the same time requires an associated user to be present) let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(21)); - assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); - assert_eq!(dbm.load_tracker(uuid).unwrap(), tracker); + dbm.store_tracker(uuid, &tracker).await.unwrap(); + assert_eq!(dbm.load_tracker(uuid).await.unwrap(), tracker); } - #[test] - fn test_store_duplicate_tracker() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_duplicate_tracker() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); + dbm.store_tracker(uuid, &tracker).await.unwrap(); // Try to store it again, but it shouldn't go through - assert!(matches!( - dbm.store_tracker(uuid, &tracker), - Err(Error::AlreadyExists) - )); + assert!(dbm + .store_tracker(uuid, &tracker) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_unique_violation()); } - #[test] - fn test_store_tracker_missing_appointment() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_tracker_missing_appointment() { + let dbm = DBM::test_db().await; let uuid = generate_uuid(); let user_id = get_random_user_id(); @@ -1489,31 +1329,36 @@ mod tests { // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - assert!(matches!( - dbm.store_tracker(uuid, &tracker), - Err(Error::MissingForeignKey) - )); + // Try to store the tracker with no appointment for it + assert!(dbm + .store_tracker(uuid, &tracker) + .await + .unwrap_err() + .into_database_error() + .unwrap() + .is_foreign_key_violation()) } - #[test] - fn test_update_tracker_status() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_update_tracker_status() { + let dbm = DBM::test_db().await; let user_id = get_random_user_id(); let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); // Update the status and check if it's actually updated. dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(100)) + .await .unwrap(); assert_eq!( - dbm.load_tracker(uuid).unwrap().status, + dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::ConfirmedIn(100) ); @@ -1522,22 +1367,23 @@ mod tests { dbm.update_tracker_status( uuid, &ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_REJECTED) - ), - Err(Error::MissingField) + ) + .await, + Err(Error::Decode(..)) )); } - #[test] - fn test_load_nonexistent_tracker() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_nonexistent_tracker() { + let dbm = DBM::test_db().await; let uuid = generate_uuid(); - assert!(dbm.load_tracker(uuid).is_none()); + assert!(dbm.load_tracker(uuid).await.is_none()); } - #[test] - fn test_load_all_trackers() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_all_trackers() { + let dbm = DBM::test_db().await; let mut trackers = HashMap::new(); for i in 1..11 { @@ -1547,23 +1393,23 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); trackers.insert(uuid, tracker); } - assert_eq!(dbm.load_trackers(None), trackers); + assert_eq!(dbm.load_trackers(None).await, trackers); } - #[test] - fn test_load_trackers_with_locator() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_trackers_with_locator() { + let dbm = DBM::test_db().await; let mut trackers = HashMap::new(); let dispute_tx = get_random_tx(); let dispute_txid = dispute_tx.txid(); @@ -1577,7 +1423,7 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let tracker = get_random_tracker(user_id, status); // Let some trackers belong to our dispute tx and some belong to random ones. @@ -1590,16 +1436,16 @@ mod tests { } else { generate_dummy_appointment_with_user(user_id, None) }; - dbm.store_appointment(uuid, &appointment).unwrap(); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); } - assert_eq!(dbm.load_trackers(Some(locator)), trackers); + assert_eq!(dbm.load_trackers(Some(locator)).await, trackers); } - #[test] - fn test_load_trackers_with_confirmation_status_in_mempool() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_trackers_with_confirmation_status_in_mempool() { + let dbm = DBM::test_db().await; let n_trackers = 100; let mut tracker_statuses = HashMap::new(); @@ -1611,10 +1457,10 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // Some trackers confirmed and some aren't. let status = if i % 2 == 0 { @@ -1624,7 +1470,7 @@ mod tests { }; let tracker = get_random_tracker(user_id, status); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); tracker_statuses.insert(uuid, status); } @@ -1644,6 +1490,7 @@ mod tests { dbm.load_trackers_with_confirmation_status(ConfirmationStatus::InMempoolSince( i )) + .await .unwrap() ), in_mempool_since_i, @@ -1651,9 +1498,9 @@ mod tests { } } - #[test] + #[tokio::test] fn test_load_trackers_with_confirmation_status_confirmed() { - let dbm = DBM::in_memory().unwrap(); + let dbm = DBM::test_db().await; let n_blocks = 100; let n_trackers = 30; let mut tracker_statuses = HashMap::new(); @@ -1668,10 +1515,10 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); // Some trackers confirmed and some aren't. let status = if j % 2 == 0 { @@ -1681,7 +1528,7 @@ mod tests { }; let tracker = get_random_tracker(user_id, status); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); tracker_statuses.insert(uuid, status); } } @@ -1699,6 +1546,7 @@ mod tests { assert_eq!( HashSet::from_iter( dbm.load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(i)) + .await .unwrap() ), confirmed_in_i, @@ -1706,26 +1554,27 @@ mod tests { } } - #[test] - fn test_load_trackers_with_confirmation_status_bad_status() { + #[tokio::test] + async fn test_load_trackers_with_confirmation_status_bad_status() { let dbm = DBM::in_memory().unwrap(); assert!(matches!( dbm.load_trackers_with_confirmation_status(ConfirmationStatus::Rejected( rpc_errors::RPC_VERIFY_REJECTED - )), - Err(Error::MissingField) + )) + .await, + Err(Error::Decode(..)) )); - assert!(matches!( - dbm.load_trackers_with_confirmation_status(ConfirmationStatus::IrrevocablyResolved), - Err(Error::MissingField) + dbm.load_trackers_with_confirmation_status(ConfirmationStatus::IrrevocablyResolved) + .await, + Err(Error::Decode(..)) )); } - #[test] - fn test_load_penalties_summaries() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_load_penalties_summaries() { + let dbm = DBM::test_db().await; let n_trackers = 100; let mut penalties_summaries = HashMap::new(); @@ -1736,10 +1585,10 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - dbm.store_user(user_id, &user).unwrap(); + dbm.store_user(user_id, &user).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); + dbm.store_appointment(uuid, &appointment).await.unwrap(); let status = if i % 2 == 0 { ConfirmationStatus::InMempoolSince(i) @@ -1748,45 +1597,45 @@ mod tests { }; let tracker = get_random_tracker(user_id, status); - dbm.store_tracker(uuid, &tracker).unwrap(); + dbm.store_tracker(uuid, &tracker).await.unwrap(); penalties_summaries .insert(uuid, PenaltySummary::new(tracker.penalty_tx.txid(), status)); } - assert_eq!(dbm.load_penalties_summaries(), penalties_summaries); + assert_eq!(dbm.load_penalties_summaries().await, penalties_summaries); } - #[test] - fn test_store_load_last_known_block() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_load_last_known_block() { + let dbm = DBM::test_db().await; let mut block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); - dbm.store_last_known_block(&block_hash).unwrap(); - assert_eq!(dbm.load_last_known_block().unwrap(), block_hash); + dbm.store_last_known_block(&block_hash).await.unwrap(); + assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); // Update with a new hash to check it can be done block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); - dbm.store_last_known_block(&block_hash).unwrap(); - assert_eq!(dbm.load_last_known_block().unwrap(), block_hash); + dbm.store_last_known_block(&block_hash).await.unwrap(); + assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); } - #[test] - fn test_store_load_nonexistent_last_known_block() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_load_nonexistent_last_known_block() { + let dbm = DBM::test_db().await; - assert!(dbm.load_last_known_block().is_none()); + assert!(dbm.load_last_known_block().await.is_none()); } - #[test] - fn test_store_load_tower_key() { - let dbm = DBM::in_memory().unwrap(); + #[tokio::test] + async fn test_store_load_tower_key() { + let dbm = DBM::test_db().await; - assert!(dbm.load_tower_key().is_none()); + assert!(dbm.load_tower_key().await.is_none()); for _ in 0..7 { let sk = get_random_keypair().0; - dbm.store_tower_key(&sk).unwrap(); - assert_eq!(dbm.load_tower_key().unwrap(), sk); + dbm.store_tower_key(&sk).await.unwrap(); + assert_eq!(dbm.load_tower_key().await.unwrap(), sk); } } } diff --git a/teos/src/dbm_new.rs b/teos/src/dbm_new.rs deleted file mode 100644 index d7300b45..00000000 --- a/teos/src/dbm_new.rs +++ /dev/null @@ -1,1590 +0,0 @@ -//! Logic related to the tower database manager (DBM), component in charge of persisting data on disk. -#![allow(dead_code)] - -use std::collections::HashMap; -use std::str::FromStr; - -use bitcoin::hashes::Hash; -use bitcoin::secp256k1::SecretKey; -use bitcoin::{consensus, BlockHash, Txid}; -use sqlx::any::{install_drivers, AnyRow}; -use sqlx::{AnyPool, Row}; - -use teos_common::appointment::{Appointment, Locator}; -use teos_common::UserId; - -use crate::extended_appointment::{ExtendedAppointment, UUID}; -use crate::gatekeeper::UserInfo; -use crate::responder::{ConfirmationStatus, TransactionTracker}; -use crate::watcher::Breach; - -type Error = sqlx::Error; - -#[cfg(not(test))] -/// The maximum number of bind variables per SQL query. -/// `32766` for SQLite & `65535` for PostgreSQL. Picking `32766` for both since it's big enough already. -const SQL_VARIABLE_LIMIT: usize = 32766; -#[cfg(test)] -/// The maximum number of bind variables per SQL query. Set to `10` for testing. -const SQL_VARIABLE_LIMIT: usize = 10; - -/// Checks if the database type (`$db_type`) matches with the connection string (`$connection_string`). -/// If the connection string matches the database type, this macro does: -/// - Try to install the driver for the passed database type. -/// - Try to connect to the database. -/// - Try to run the migrations specified in `$migrations_path`. -/// - Return [Ok(DBM)] if everything succeeds, [Err(String)] otherwise explaining the error. -macro_rules! return_db_if_matching { - ($connection_string:expr, $db_type:tt, $migrations_path:literal) => { - if $connection_string.starts_with(concat!(stringify!($db_type), ":")) { - install_drivers(&[sqlx::$db_type::any::DRIVER]) - // Just log the warning but try to connect anyways. `install_drivers` might fail if the driver is already installed. - .map_err(|e| log::error!("Failed to install database drivers for {}: {e}", stringify!($db_type))) - .ok(); - let pool = AnyPool::connect($connection_string) - .await - .map_err(|e| format!("Couldn't connect to {}: {e}", $connection_string))?; - sqlx::migrate!($migrations_path) - .run(&pool) - .await - .map_err(|e| format!("Failed to run database migrations: {e}"))?; - return Ok(Self { pool }); - } - } -} - -/// Component in charge of interacting with the underlying database. -#[derive(Debug)] -pub struct DBM { - pool: AnyPool, -} - -impl DBM { - /// Creates a new [DBM] instance. - pub async fn new(connection_string: &str) -> Result { - #[cfg(not(any(feature = "sqlite", feature = "postgres")))] - compile_error!("Can't compile with no database drivers."); - - // Note that sqlx initiates `PRAGMA foreign_keys = ON` connections by default. - #[cfg(feature = "sqlite")] - return_db_if_matching!(connection_string, sqlite, "migrations/sqlite"); - - #[cfg(feature = "postgres")] - return_db_if_matching!(connection_string, postgres, "migrations/postgres"); - - let supported_dbs = vec![ - #[cfg(feature = "sqlite")] - "SQLite", - #[cfg(feature = "postgres")] - "PostgreSQL", - ]; - - Err(format!( - "The database connection string ({connection_string}) is invalid or isn't supported. Supported databases are: {supported_dbs:?}." - )) - } - - /// Stores a user ([UserInfo]) into the database. - pub(crate) async fn store_user( - &self, - user_id: UserId, - user_info: &UserInfo, - ) -> Result<(), Error> { - let sql = "INSERT INTO users (user_id, available_slots, subscription_start, subscription_expiry) VALUES ($1, $2, $3, $4)"; - sqlx::query(sql) - .bind(user_id.to_vec()) - .bind(user_info.available_slots as i64) - .bind(user_info.subscription_start as i64) - .bind(user_info.subscription_expiry as i64) - .execute(&self.pool) - .await - .map(|_| ()) - } - - /// Updates an existing user ([UserInfo]) in the database. - pub(crate) async fn update_user( - &self, - user_id: UserId, - user_info: &UserInfo, - ) -> Result<(), Error> { - let sql = "UPDATE users SET available_slots=($1), subscription_start=($2), subscription_expiry=($3) WHERE user_id=($4)"; - let updated_rows = sqlx::query(sql) - .bind(user_info.available_slots as i64) - .bind(user_info.subscription_start as i64) - .bind(user_info.subscription_expiry as i64) - .bind(user_id.to_vec()) - .execute(&self.pool) - .await? - .rows_affected(); - - (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) - } - - /// Loads the associated locators ([Locator]) of a given user ([UserId]). - pub(crate) async fn load_user_locators(&self, user_id: UserId) -> Vec { - sqlx::query("SELECT locator FROM appointments WHERE user_id=($1)") - .bind(user_id.to_vec()) - .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) - .fetch_all(&self.pool) - .await - .unwrap() - } - - /// Loads all users from the database. - pub(crate) async fn load_all_users(&self) -> HashMap { - sqlx::query( - "SELECT user_id, available_slots, subscription_start, subscription_expiry FROM users", - ) - .map(|row: AnyRow| { - ( - UserId::from_slice(row.get("user_id")).unwrap(), - UserInfo::new( - row.get::("available_slots") as u32, - row.get::("subscription_start") as u32, - row.get::("subscription_expiry") as u32, - ), - ) - }) - .fetch_all(&self.pool) - .await - .unwrap() - .into_iter() - .collect() - } - - /// Removes some users from the database in batch. - pub(crate) async fn batch_remove_users(&self, users: &Vec) -> usize { - let users: Vec<_> = users.iter().map(|uuid| uuid.to_vec()).collect(); - - for chunk in users.chunks(SQL_VARIABLE_LIMIT) { - let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); - let placeholders = format!("${}", str_indices.join(", $")); - let sql = format!("DELETE FROM users WHERE user_id IN ({placeholders})"); - let mut query = sqlx::query(&sql); - for user_id in chunk { - query = query.bind(user_id); - } - query.execute(&self.pool).await.unwrap(); - } - - (users.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize - } - - /// Get the number of stored appointments. - pub(crate) async fn get_appointments_count(&self) -> usize { - let sql = "SELECT COUNT(*) FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL"; - sqlx::query(sql) - .map(|row: AnyRow| row.get::(0) as usize) - .fetch_one(&self.pool) - .await - .unwrap() - } - - /// Get the number of stored trackers. - pub(crate) async fn get_trackers_count(&self) -> usize { - sqlx::query("SELECT COUNT(*) FROM trackers") - .map(|row: AnyRow| row.get::(0) as usize) - .fetch_one(&self.pool) - .await - .unwrap() - } - - /// Stores an [Appointment] into the database. - pub(crate) async fn store_appointment( - &self, - uuid: UUID, - appointment: &ExtendedAppointment, - ) -> Result<(), Error> { - let sql = "INSERT INTO appointments (UUID, locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id) VALUES ($1, $2, $3, $4, $5, $6, $7)"; - sqlx::query(sql) - .bind(uuid.to_vec()) - .bind(appointment.locator().to_vec()) - .bind(appointment.encrypted_blob()) - .bind(appointment.to_self_delay() as i64) - .bind(&appointment.user_signature) - .bind(appointment.start_block as i64) - .bind(appointment.user_id.to_vec()) - .execute(&self.pool) - .await - .map(|_| ()) - } - - /// Updates an existing [Appointment] in the database. - pub(crate) async fn update_appointment( - &self, - uuid: UUID, - appointment: &ExtendedAppointment, - ) -> Result<(), Error> { - // DISCUSS: Check what fields we'd like to make updatable. e_blob and signature are the obvious, to_self_delay and start_block may not be necessary (or even risky) - let sql = "UPDATE appointments SET encrypted_blob=($1), to_self_delay=($2), user_signature=($3), start_block=($4) WHERE UUID=($5)"; - let updated_rows = sqlx::query(sql) - .bind(appointment.encrypted_blob()) - .bind(appointment.to_self_delay() as i64) - .bind(&appointment.user_signature) - .bind(appointment.start_block as i64) - .bind(uuid.to_vec()) - .execute(&self.pool) - .await? - .rows_affected(); - - (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) - } - - /// Loads an [Appointment] from the database. - pub(crate) async fn load_appointment(&self, uuid: UUID) -> Option { - let sql = "SELECT locator, encrypted_blob, to_self_delay, user_id, user_signature, start_block FROM appointments WHERE UUID=($1)"; - sqlx::query(sql) - .bind(uuid.to_vec()) - .map(|row: AnyRow| { - ExtendedAppointment::new( - Appointment::new( - Locator::from_slice(row.get("locator")).unwrap(), - row.get("encrypted_blob"), - row.get::("to_self_delay") as u32, - ), - UserId::from_slice(row.get("user_id")).unwrap(), - row.get("user_signature"), - row.get::("start_block") as u32, - ) - }) - .fetch_one(&self.pool) - .await - .ok() - } - - /// Check if an appointment with `uuid` exists. - pub(crate) async fn appointment_exists(&self, uuid: UUID) -> bool { - sqlx::query("SELECT UUID FROM appointments WHERE UUID=($1)") - .bind(uuid.to_vec()) - .fetch_one(&self.pool) - .await - .is_ok() - } - - /// Gets the length of an appointment (the length of `appointment.encrypted_blob`). - pub(crate) async fn get_appointment_length(&self, uuid: UUID) -> Result { - sqlx::query("SELECT length(encrypted_blob) FROM appointments WHERE UUID=($1)") - .bind(uuid.to_vec()) - .map(|row: AnyRow| row.get::(0) as usize) - .fetch_one(&self.pool) - .await - } - - /// Gets the [`UserId`] of the owner of the appointment along with the appointment - /// length (same as [DBM::get_appointment_length]) for `uuid`. - pub(crate) async fn get_appointment_user_and_length( - &self, - uuid: UUID, - ) -> Result<(UserId, usize), Error> { - sqlx::query("SELECT user_id, length(encrypted_blob) FROM appointments WHERE UUID=($1)") - .bind(uuid.to_vec()) - .map(|row: AnyRow| { - ( - UserId::from_slice(row.get("user_id")).unwrap(), - row.get::(1) as usize, - ) - }) - .fetch_one(&self.pool) - .await - } - - /// Loads appointments from the database. If a locator is given, this method loads only the appointments - /// matching this locator. If no locator is given, all the appointments in the database would be returned. - pub(crate) async fn load_appointments( - &self, - locator: Option, - ) -> HashMap { - let mut sql = "SELECT a.UUID, a.locator, a.encrypted_blob, a.to_self_delay, a.user_id, a.user_signature, a.start_block FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL".to_string(); - - // If a locator was passed, filter based on it. - let query = if let Some(locator) = locator { - sql.push_str(" AND a.locator=($1)"); - sqlx::query(&sql).bind(locator.to_vec()) - } else { - sqlx::query(&sql) - }; - - query - .map(|row: AnyRow| { - ( - UUID::from_slice(row.get(0)).unwrap(), - ExtendedAppointment::new( - Appointment::new( - Locator::from_slice(row.get(1)).unwrap(), - row.get(2), - row.get::(3) as u32, - ), - UserId::from_slice(row.get(4)).unwrap(), - row.get(5), - row.get::(6) as u32, - ), - ) - }) - .fetch_all(&self.pool) - .await - .unwrap() - .into_iter() - .collect() - } - - /// Removes an [Appointment] from the database. - pub(crate) async fn remove_appointment(&self, uuid: UUID) { - if let Err(e) = sqlx::query("DELETE FROM appointments WHERE UUID=($1)") - .execute(&self.pool) - .await - { - log::error!("Failed to remove appointment ({uuid}) due to: {e}") - } - } - - /// Removes some appointments from the database in batch and updates the associated users - /// (giving back freed appointment slots) in one transaction so that the deletion and the - /// update is atomic. - pub(crate) async fn batch_remove_appointments( - &self, - appointments: &Vec, - updated_users: &HashMap, - ) -> usize { - let uuids: Vec<_> = appointments.iter().map(|uuid| uuid.to_vec()).collect(); - let mut tx = self.pool.begin().await.unwrap(); - - for chunk in uuids.chunks(SQL_VARIABLE_LIMIT) { - let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); - let placeholders = format!("${}", str_indices.join(", $")); - let sql = format!("DELETE FROM appointments WHERE UUID IN ({placeholders})"); - let mut query = sqlx::query(&sql); - for uuid in chunk { - query = query.bind(uuid); - } - query.execute(&mut *tx).await.unwrap(); - } - - for (user_id, info) in updated_users.iter() { - sqlx::query("UPDATE users SET available_slots=($1) WHERE user_id=($2)") - .bind(info.available_slots as i64) - .bind(user_id.to_vec()) - .execute(&mut *tx) - .await - .unwrap(); - } - - if let Err(e) = tx.commit().await { - log::error!("Failed to remove appointments in batch: {e}") - } - - (uuids.len() as f64 / SQL_VARIABLE_LIMIT as f64).ceil() as usize - } - - /// Loads the [`UUID`]s of appointments triggered by `locator`. - pub(crate) async fn load_uuids(&self, locator: Locator) -> Vec { - sqlx::query("SELECT UUID from appointments WHERE locator=($1)") - .bind(locator.to_vec()) - .map(|row: AnyRow| UUID::from_slice(row.get("UUID")).unwrap()) - .fetch_all(&self.pool) - .await - .unwrap() - } - - /// Filters the given set of [`Locator`]s by including only the ones which trigger any of our stored appointments. - pub(crate) async fn batch_check_locators_exist(&self, locators: Vec<&Locator>) -> Vec { - let mut registered_locators = Vec::new(); - let locators: Vec<_> = locators.iter().map(|l| l.to_vec()).collect(); - - for chunk in locators.chunks(SQL_VARIABLE_LIMIT) { - let str_indices: Vec<_> = (1..=chunk.len()).map(|i| i.to_string()).collect(); - let placeholders = format!("${}", str_indices.join(", $")); - let sql = format!("SELECT locator FROM appointments WHERE locator IN ({placeholders})"); - let mut query = sqlx::query(&sql); - for locator in chunk { - query = query.bind(locator); - } - registered_locators.extend( - query - .map(|row: AnyRow| Locator::from_slice(row.get("locator")).unwrap()) - .fetch_all(&self.pool) - .await - .unwrap(), - ) - } - - registered_locators - } - - /// Stores a [TransactionTracker] into the database. - pub(crate) async fn store_tracker( - &self, - uuid: UUID, - tracker: &TransactionTracker, - ) -> Result<(), Error> { - let (height, confirmed) = tracker - .status - .to_db_data() - .ok_or(Error::Decode("Tracker status isn't storable".into()))?; - let sql = "INSERT INTO trackers (UUID, dispute_tx, penalty_tx, height, confirmed) VALUES ($1, $2, $3, $4, $5)"; - sqlx::query(sql) - .bind(uuid.to_vec()) - .bind(consensus::serialize(&tracker.dispute_tx)) - .bind(consensus::serialize(&tracker.penalty_tx)) - .bind(height as i64) - .bind(confirmed as i64) - .execute(&self.pool) - .await - .map(|_| ()) - } - - /// Updates the tracker information in the database. - /// - /// The only updatable fields are `height` and `confirmed`. - pub(crate) async fn update_tracker_status( - &self, - uuid: UUID, - status: &ConfirmationStatus, - ) -> Result<(), Error> { - let (height, confirmed) = status - .to_db_data() - .ok_or(Error::Decode("Tracker status isn't storable".into()))?; - let updated_rows = - sqlx::query("UPDATE trackers SET height=($1), confirmed=($2) WHERE UUID=($3)") - .bind(height as i64) - .bind(confirmed as i64) - .bind(uuid.to_vec()) - .execute(&self.pool) - .await? - .rows_affected(); - - (updated_rows != 0).then_some(()).ok_or(Error::RowNotFound) - } - - /// Loads a [TransactionTracker] from the database. - pub(crate) async fn load_tracker(&self, uuid: UUID) -> Option { - let sql = "SELECT t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=($1)"; - sqlx::query(sql) - .bind(uuid.to_vec()) - .map(|row: AnyRow| { - TransactionTracker::new( - Breach::new( - consensus::deserialize(row.get(0)).unwrap(), - consensus::deserialize(row.get(1)).unwrap(), - ), - UserId::from_slice(row.get(2)).unwrap(), - ConfirmationStatus::from_db_data( - row.get::(3) as u32, - row.get::(4) != 0, - ), - ) - }) - .fetch_one(&self.pool) - .await - .ok() - } - - /// Check if a tracker with `uuid` exists. - pub(crate) async fn tracker_exists(&self, uuid: UUID) -> bool { - sqlx::query("SELECT UUID FROM trackers WHERE UUID=($1)") - .bind(uuid.to_vec()) - .fetch_one(&self.pool) - .await - .is_ok() - } - - /// Loads trackers from the database. If a locator is given, this method loads only the trackers - /// matching this locator. If no locator is given, all the trackers in the database would be returned. - pub(crate) async fn load_trackers( - &self, - locator: Option, - ) -> HashMap { - let mut sql = "SELECT t.UUID, t.dispute_tx, t.penalty_tx, a.user_id, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID".to_string(); - - // If a locator was passed, filter based on it. - let query = if let Some(locator) = locator { - sql.push_str(" WHERE a.locator=($1)"); - sqlx::query(&sql).bind(locator.to_vec()) - } else { - sqlx::query(&sql) - }; - - query - .map(|row: AnyRow| { - ( - UUID::from_slice(row.get(0)).unwrap(), - TransactionTracker::new( - Breach::new( - consensus::deserialize(row.get(1)).unwrap(), - consensus::deserialize(row.get(2)).unwrap(), - ), - UserId::from_slice(row.get(3)).unwrap(), - ConfirmationStatus::from_db_data( - row.get::(4) as u32, - row.get::(5) != 0, - ), - ), - ) - }) - .fetch_all(&self.pool) - .await - .unwrap() - .into_iter() - .collect() - } - - /// Loads trackers with the given confirmation status. - /// - /// Note that for [`ConfirmationStatus::InMempoolSince(height)`] variant, this pulls trackers - /// with `h <= height` and not just `h = height`. - pub(crate) async fn load_trackers_with_confirmation_status( - &self, - status: ConfirmationStatus, - ) -> Result, Error> { - let (height, confirmed) = status - .to_db_data() - .ok_or(Error::Decode("Tracker status isn't storable".into()))?; - - sqlx::query(&format!( - "SELECT UUID FROM trackers WHERE confirmed=($1) AND height{}($2)", - if confirmed { "=" } else { "<=" } - )) - .bind(confirmed as i64) - .bind(height as i64) - .map(|row: AnyRow| UUID::from_slice(row.get(0)).unwrap()) - .fetch_all(&self.pool) - .await - } - - /// Loads the transaction IDs of all the penalties and their status from the database. - pub(crate) async fn load_penalties_summaries( - &self, - ) -> HashMap { - let sql = "SELECT t.UUID, t.penalty_tx, t.height, t.confirmed FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID"; - sqlx::query(sql) - .map(|row: AnyRow| { - ( - UUID::from_slice(row.get(0)).unwrap(), - ( - consensus::deserialize::(row.get(1)) - .unwrap() - .txid(), - ConfirmationStatus::from_db_data( - row.get::(2) as u32, - row.get::(3) != 0, - ), - ), - ) - }) - .fetch_all(&self.pool) - .await - .unwrap() - .into_iter() - .collect() - } - - /// Stores the last known block into the database. - pub(crate) async fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { - sqlx::query("INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, $1)") - .bind(block_hash.to_vec()) - .execute(&self.pool) - .await - .map(|_| ()) - } - - /// Loads the last known block from the database. - pub async fn load_last_known_block(&self) -> Option { - sqlx::query("SELECT block_hash FROM last_known_block WHERE id=0") - .map(|row: AnyRow| BlockHash::from_slice(row.get("block_hash")).unwrap()) - .fetch_one(&self.pool) - .await - .ok() - } - - /// Stores the tower secret key into the database. - /// - /// When a new key is generated, old keys are not overwritten but are not retrievable from the API either. - pub async fn store_tower_key(&self, sk: &SecretKey) -> Result<(), Error> { - sqlx::query("INSERT INTO keys (secret_key) VALUES ($1)") - .bind(sk.display_secret().to_string()) - .execute(&self.pool) - .await - .map(|_| ()) - } - - /// Loads the last known tower secret key from the database. - /// - /// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed, - /// but they are not accessible from the API either. - pub async fn load_tower_key(&self) -> Option { - sqlx::query("SELECT secret_key FROM keys WHERE id = (SELECT MAX(id) from keys)") - .map(|row: AnyRow| SecretKey::from_str(row.get("secret_key")).unwrap()) - .fetch_one(&self.pool) - .await - .ok() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use std::collections::HashSet; - use std::iter::FromIterator; - - use teos_common::cryptography::{get_random_bytes, get_random_keypair}; - use teos_common::test_utils::{get_random_locator, get_random_user_id}; - - use crate::test_utils::{ - generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, - get_random_tracker, get_random_tx, AVAILABLE_SLOTS, SUBSCRIPTION_EXPIRY, - SUBSCRIPTION_START, - }; - - impl DBM { - #[cfg(feature = "sqlite")] - async fn memory() -> Self { - use sqlx::any::AnyPoolOptions; - install_drivers(&[sqlx::sqlite::any::DRIVER]).ok(); - let pool = AnyPoolOptions::new() - // Each new connection actually creates a brand new DB: https://github.com/launchbadge/sqlx/issues/2510 - // So make sure the pool doesn't create more than one connection. - .max_connections(1) - .connect("sqlite::memory:") - .await - .unwrap(); - sqlx::migrate!("migrations/sqlite") - .run(&pool) - .await - .unwrap(); - Self { pool } - } - - #[cfg(feature = "postgres")] - async fn postgres() -> Self { - (|| async { - return_db_if_matching!( - "postgres://user:pass@localhost/teos", - postgres, - "migrations/postgres" - ); - Err(format!("Unreachable (the macro above will always match)")) - })() - .await - .unwrap() - } - - #[allow(unreachable_code)] - /// Returns a new [DBM], preferring sqlite over postgres if available. - async fn test_db() -> Self { - #[cfg(feature = "sqlite")] - return Self::memory().await; - - #[cfg(feature = "postgres")] - return Self::postgres().await; - - panic!("No database driver available. Make sure you compile with sqlite and/or postgres features enabled.") - } - - async fn load_user(&self, user_id: UserId) -> Option { - let sql = "SELECT available_slots, subscription_start, subscription_expiry FROM users WHERE user_id=($1)"; - sqlx::query(sql) - .bind(user_id.to_vec()) - .map(|row: AnyRow| { - UserInfo::new( - row.get::("available_slots") as u32, - row.get::("subscription_start") as u32, - row.get::("subscription_expiry") as u32, - ) - }) - .fetch_one(&self.pool) - .await - .ok() - } - } - - #[tokio::test] - async fn test_store_user() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - - assert!(dbm.load_user(user_id).await.is_none()); - - dbm.store_user(user_id, &user_info).await.unwrap(); - assert_eq!(dbm.load_user(user_id).await, Some(user_info)); - - // Store an existing user should error (should use update_user). - dbm.store_user(user_id, &user_info).await.unwrap_err(); - } - - #[tokio::test] - async fn test_update_user() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let mut user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - - dbm.store_user(user_id, &user_info).await.unwrap(); - - user_info.available_slots *= 2; - dbm.update_user(user_id, &user_info).await.unwrap(); - assert_eq!(dbm.load_user(user_id).await, Some(user_info)); - } - - #[tokio::test] - async fn test_load_user_locators() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user_info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user_info).await.unwrap(); - - let mut locators = HashSet::new(); - - // Add some appointments to the user - for _ in 0..10 { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - locators.insert(appointment.locator()); - } - - assert_eq!(dbm.load_user(user_id).await, Some(user_info)); - assert_eq!( - HashSet::from_iter(dbm.load_user_locators(user_id).await), - locators - ); - } - - #[tokio::test] - async fn test_load_all_users() { - let dbm = DBM::test_db().await; - let mut users = HashMap::new(); - - for i in 1..11 { - let user_id = get_random_user_id(); - let user_info = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - users.insert(user_id, user_info.clone()); - dbm.store_user(user_id, &user_info).await.unwrap(); - } - - assert_eq!(dbm.load_all_users().await, users); - } - - #[tokio::test] - async fn test_batch_remove_users() { - let dbm = DBM::test_db().await; - - let mut to_be_deleted = Vec::new(); - let mut rest = HashSet::new(); - - for i in 0..SQL_VARIABLE_LIMIT * 3 { - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - if i % 2 == 0 { - to_be_deleted.push(user_id); - } else { - rest.insert(user_id); - } - } - - // SQL_VARIABLE_LIMIT is 10 for tests, - // Check that deletion had `ceil(10 * 3 / 2) / 10` (2) queries on it - assert_eq!(dbm.batch_remove_users(&to_be_deleted).await, 2); - - // Check user data was deleted - assert_eq!(rest, dbm.load_all_users().await.keys().cloned().collect()); - } - - #[tokio::test] - async fn test_batch_remove_users_cascade() { - // Test that removing users cascade deleted appointments and trackers - let dbm = DBM::test_db().await; - let uuid = generate_uuid(); - let appointment = generate_dummy_appointment(None); - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(appointment.user_id, ConfirmationStatus::ConfirmedIn(100)); - - // Add the user and link an appointment (this is usually done once the appointment) - // is added after the user creation, but for the test purpose it can be done all at once. - let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(appointment.user_id, &info).await.unwrap(); - - // Appointment only - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - dbm.batch_remove_users(&vec![appointment.user_id]).await; - assert!(dbm.load_user(appointment.user_id).await.is_none()); - assert!(dbm.load_appointment(uuid).await.is_none()); - - // Appointment + Tracker - dbm.store_user(appointment.user_id, &info).await.unwrap(); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - dbm.batch_remove_users(&vec![appointment.user_id]).await; - assert!(dbm.load_user(appointment.user_id).await.is_none()); - assert!(dbm.load_appointment(uuid).await.is_none()); - assert!(dbm.load_tracker(uuid).await.is_none()); - } - - #[tokio::test] - async fn test_batch_remove_nonexistent_users() { - let dbm = DBM::test_db().await; - let users = (0..10).map(|_| get_random_user_id()).collect(); - - // Test it does not fail even if the user does not exist - dbm.batch_remove_users(&users).await; - } - - #[tokio::test] - async fn test_get_appointments_trackers_count() { - let dbm = DBM::test_db().await; - let n_users = 100; - let n_app_per_user = 4; - let n_trk_per_user = 6; - - for _ in 0..n_users { - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - // These are un-triggered appointments. - for _ in 0..n_app_per_user { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - } - - // And these are triggered ones (trackers). - for _ in 0..n_trk_per_user { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - } - } - - assert_eq!(dbm.get_appointments_count().await, n_users * n_app_per_user); - assert_eq!(dbm.get_trackers_count().await, n_users * n_trk_per_user); - } - - #[tokio::test] - async fn test_store_load_appointment() { - let dbm = DBM::test_db().await; - - // In order to add an appointment we need the associated user to be present - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - assert_eq!(dbm.load_appointment(uuid).await.unwrap(), appointment); - - // Appointment info should be updatable but only via the update_appointment method - assert!(dbm - .store_appointment(uuid, &appointment) - .await - .unwrap_err() - .into_database_error() - .unwrap() - .is_unique_violation()) - } - - #[tokio::test] - async fn test_store_appointment_missing_user() { - let dbm = DBM::test_db().await; - - let uuid = generate_uuid(); - let appointment = generate_dummy_appointment(None); - - assert!(dbm - .store_appointment(uuid, &appointment) - .await - .unwrap_err() - .into_database_error() - .unwrap() - .is_foreign_key_violation()); - assert!((dbm.load_tracker(uuid).await.is_none())); - } - - #[tokio::test] - async fn test_update_appointment() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // Modify the appointment and update it - let mut modified_appointment = appointment; - modified_appointment.inner.encrypted_blob.reverse(); - - // Not all fields are updatable, create another appointment modifying fields that cannot be - let mut another_modified_appointment = modified_appointment.clone(); - another_modified_appointment.user_id = get_random_user_id(); - - // Check how only the modifiable fields have been updated - dbm.update_appointment(uuid, &another_modified_appointment) - .await - .unwrap(); - assert_eq!( - dbm.load_appointment(uuid).await.unwrap(), - modified_appointment - ); - assert_ne!( - dbm.load_appointment(uuid).await.unwrap(), - another_modified_appointment - ); - } - - #[tokio::test] - async fn test_load_nonexistent_appointment() { - let dbm = DBM::test_db().await; - - let uuid = generate_uuid(); - assert!(dbm.load_appointment(uuid).await.is_none()); - } - - #[tokio::test] - async fn test_appointment_exists() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - assert!(!dbm.appointment_exists(uuid).await); - - dbm.store_user(user_id, &user).await.unwrap(); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - assert!(dbm.appointment_exists(uuid).await); - } - - #[tokio::test] - async fn test_get_appointment_length() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - dbm.store_user(user_id, &user).await.unwrap(); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - assert_eq!( - dbm.get_appointment_length(uuid).await.unwrap(), - appointment.inner.encrypted_blob.len() - ); - assert!(matches!( - dbm.get_appointment_length(generate_uuid()).await, - Err(Error::RowNotFound) - )); - } - - #[tokio::test] - async fn test_get_appointment_user_and_length() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - - dbm.store_user(user_id, &user).await.unwrap(); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - assert_eq!( - dbm.get_appointment_user_and_length(uuid).await.unwrap(), - (user_id, appointment.encrypted_blob().len()) - ); - assert!(matches!( - dbm.get_appointment_user_and_length(generate_uuid()).await, - Err(Error::RowNotFound) - )); - } - - #[tokio::test] - async fn test_load_all_appointments() { - let dbm = DBM::test_db().await; - let mut appointments = HashMap::new(); - - for i in 1..11 { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - appointments.insert(uuid, appointment); - } - - assert_eq!(dbm.load_appointments(None).await, appointments); - - // If an appointment has an associated tracker, it should not be loaded since it is seen - // as a triggered appointment - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - // We should get all the appointments back except from the triggered one - assert_eq!(dbm.load_appointments(None).await, appointments); - } - - #[tokio::test] - async fn test_load_appointments_with_locator() { - let dbm = DBM::test_db().await; - let mut appointments = HashMap::new(); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.txid(); - let locator = Locator::new(dispute_txid); - - for i in 1..11 { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - // Let some appointments belong to a specific dispute tx and some with random ones. - // We will use the locator for that dispute tx to query these appointments. - if i % 2 == 0 { - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - // Store the appointments made using our dispute tx. - appointments.insert(uuid, appointment); - } else { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - } - } - - // Validate that no other appointments than the ones with our locator are returned. - assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); - - // If an appointment has an associated tracker, it should not be loaded since it is seen - // as a triggered appointment - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - // Generate an appointment for our dispute tx, thus it gets the same locator as the ones generated above. - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - // We should get all the appointments matching our locator back except from the triggered one - assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); - } - - #[tokio::test] - async fn test_batch_remove_appointments() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let mut user = UserInfo::new( - AVAILABLE_SLOTS + 123, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - let mut rest = HashSet::new(); - for i in 1..6 { - let mut to_be_deleted = Vec::new(); - for j in 0..SQL_VARIABLE_LIMIT * 2 * i { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - if j % 2 == 0 { - to_be_deleted.push(uuid); - } else { - rest.insert(uuid); - } - } - - // When the appointment are deleted, the user will get back slots based on the deleted data. - // Here we can just make a number up to make sure it matches. - user.available_slots = i as u32; - let updated_users = HashMap::from_iter([(user_id, user.clone())]); - - // Check that the db transaction had i queries on it - assert_eq!( - dbm.batch_remove_appointments(&to_be_deleted, &updated_users) - .await, - i as usize - ); - // Check appointment data was deleted and users properly updated - assert_eq!( - rest, - dbm.load_appointments(None).await.keys().cloned().collect() - ); - assert_eq!( - dbm.load_user(user_id).await.unwrap().available_slots, - user.available_slots - ); - } - } - - #[tokio::test] - async fn test_batch_remove_appointments_cascade() { - let dbm = DBM::test_db().await; - let uuid = generate_uuid(); - let appointment = generate_dummy_appointment(None); - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(appointment.user_id, ConfirmationStatus::ConfirmedIn(21)); - - let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - - // Add the user b/c of FK restrictions - dbm.store_user(appointment.user_id, &info).await.unwrap(); - - println!("{}", appointment.inner.to_self_delay); - // Appointment only - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - dbm.batch_remove_appointments( - &vec![uuid], - &HashMap::from_iter([(appointment.user_id, info.clone())]), - ) - .await; - assert!(dbm.load_appointment(uuid).await.is_none()); - - // Appointment + Tracker - dbm.store_appointment(uuid, &appointment).await.unwrap(); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - dbm.batch_remove_appointments( - &vec![uuid], - &HashMap::from_iter([(appointment.user_id, info)]), - ) - .await; - assert!(dbm.load_appointment(uuid).await.is_none()); - assert!(dbm.load_tracker(uuid).await.is_none()); - } - - #[tokio::test] - async fn test_batch_remove_nonexistent_appointments() { - let dbm = DBM::test_db().await; - let appointments = (0..10).map(|_| generate_uuid()).collect(); - - // Test it does not fail even if the user does not exist - dbm.batch_remove_appointments(&appointments, &HashMap::new()) - .await; - } - - #[tokio::test] - async fn test_load_uuids() { - let dbm = DBM::test_db().await; - - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.txid(); - let mut uuids = HashSet::new(); - - // Add ten appointments triggered by the same locator. - for _ in 0..10 { - let user_id = get_random_user_id(); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - uuids.insert(uuid); - } - - // Add ten more appointments triggered by different locators. - for _ in 0..10 { - let user_id = get_random_user_id(); - dbm.store_user(user_id, &user).await.unwrap(); - - let dispute_txid = get_random_tx().txid(); - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - } - - assert_eq!( - HashSet::from_iter(dbm.load_uuids(Locator::new(dispute_txid)).await), - uuids - ); - } - - #[tokio::test] - async fn test_batch_check_locators_exist() { - let dbm = DBM::test_db().await; - // Generate `n_app` appointments which we will store in the DB. - let n_app = 100; - let appointments: Vec<_> = (0..n_app) - .map(|_| generate_dummy_appointment(None)) - .collect(); - - // Register all the users beforehand. - for user_id in appointments.iter().map(|a| a.user_id) { - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - } - - // Store all the `n_app` appointments. - for appointment in appointments.iter() { - dbm.store_appointment( - UUID::new(appointment.locator(), appointment.user_id), - appointment, - ) - .await - .unwrap(); - } - - // Select `n_app / 5` locators as if they appeared in a new block. - let known_locators: HashSet<_> = appointments - .iter() - .take(n_app / 5) - .map(|a| a.locator()) - .collect(); - // And extra `n_app / 5` unknown locators. - let unknown_locators: HashSet<_> = (0..n_app / 5).map(|_| get_random_locator()).collect(); - let all_locators = known_locators - .iter() - .chain(unknown_locators.iter()) - .collect(); - - assert_eq!( - HashSet::from_iter(dbm.batch_check_locators_exist(all_locators).await), - known_locators - ); - } - - #[tokio::test] - async fn test_store_load_tracker() { - let dbm = DBM::test_db().await; - - // In order to add a tracker we need the associated appointment to be present (which - // at the same time requires an associated user to be present) - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(21)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - assert_eq!(dbm.load_tracker(uuid).await.unwrap(), tracker); - } - - #[tokio::test] - async fn test_store_duplicate_tracker() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - // Try to store it again, but it shouldn't go through - assert!(dbm - .store_tracker(uuid, &tracker) - .await - .unwrap_err() - .into_database_error() - .unwrap() - .is_unique_violation()); - } - - #[tokio::test] - async fn test_store_tracker_missing_appointment() { - let dbm = DBM::test_db().await; - - let uuid = generate_uuid(); - let user_id = get_random_user_id(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - - // Try to store the tracker with no appointment for it - assert!(dbm - .store_tracker(uuid, &tracker) - .await - .unwrap_err() - .into_database_error() - .unwrap() - .is_foreign_key_violation()) - } - - #[tokio::test] - async fn test_update_tracker_status() { - let dbm = DBM::test_db().await; - - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - // Update the status and check if it's actually updated. - dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(100)) - .await - .unwrap(); - assert_eq!( - dbm.load_tracker(uuid).await.unwrap().status, - ConfirmationStatus::ConfirmedIn(100) - ); - - // Rejected status doesn't have a persistent DB representation. - assert!(matches!( - dbm.update_tracker_status(uuid, &ConfirmationStatus::Rejected(100)) - .await, - Err(Error::Decode(..)) - )); - } - - #[tokio::test] - async fn test_load_nonexistent_tracker() { - let dbm = DBM::test_db().await; - - let uuid = generate_uuid(); - assert!(dbm.load_tracker(uuid).await.is_none()); - } - - #[tokio::test] - async fn test_load_all_trackers() { - let dbm = DBM::test_db().await; - let mut trackers = HashMap::new(); - - for i in 1..11 { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(42)); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - trackers.insert(uuid, tracker); - } - - assert_eq!(dbm.load_trackers(None).await, trackers); - } - - #[tokio::test] - async fn test_load_trackers_with_locator() { - let dbm = DBM::test_db().await; - let mut trackers = HashMap::new(); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.txid(); - let locator = Locator::new(dispute_txid); - let status = ConfirmationStatus::InMempoolSince(42); - - for i in 1..11 { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - let tracker = get_random_tracker(user_id, status); - - // Let some trackers belong to our dispute tx and some belong to random ones. - let (uuid, appointment) = if i % 2 == 0 { - let (uuid, app) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - // Store the trackers of appointments made with our dispute tx. - trackers.insert(uuid, tracker.clone()); - (uuid, app) - } else { - generate_dummy_appointment_with_user(user_id, None) - }; - dbm.store_appointment(uuid, &appointment).await.unwrap(); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - } - - assert_eq!(dbm.load_trackers(Some(locator)).await, trackers); - } - - #[tokio::test] - async fn test_load_trackers_with_confirmation_status() { - let dbm = DBM::test_db().await; - let n_trackers = 100; - let mut tracker_statuses = HashMap::new(); - - // Store a bunch of trackers. - for i in 0..n_trackers { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - // Some trackers confirmed and some aren't. - let status = if i % 2 == 0 { - ConfirmationStatus::InMempoolSince(i) - } else { - ConfirmationStatus::ConfirmedIn(i) - }; - - let tracker = get_random_tracker(user_id, status); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - tracker_statuses.insert(uuid, status); - } - - for i in 0..n_trackers + 10 { - let in_mempool_since_i: HashSet = tracker_statuses - .iter() - .filter_map(|(&uuid, &status)| { - if let ConfirmationStatus::InMempoolSince(x) = status { - // If a tracker was in mempool since x, then it's also in mempool since x + 1, x + 2, etc... - return (x <= i).then(|| uuid); - } - None - }) - .collect(); - assert_eq!( - HashSet::from_iter( - dbm.load_trackers_with_confirmation_status(ConfirmationStatus::InMempoolSince( - i - )) - .await - .unwrap() - ), - in_mempool_since_i, - ); - let confirmed_in_i: HashSet = tracker_statuses - .iter() - .filter_map(|(&uuid, &status)| { - if let ConfirmationStatus::ConfirmedIn(x) = status { - return (x == i).then(|| uuid); - } - None - }) - .collect(); - assert_eq!( - HashSet::from_iter( - dbm.load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(i)) - .await - .unwrap() - ), - confirmed_in_i, - ); - } - - assert!(matches!( - dbm.load_trackers_with_confirmation_status(ConfirmationStatus::Rejected(100)) - .await, - Err(Error::Decode(..)) - )); - } - - #[tokio::test] - async fn test_load_penalties_summaries() { - let dbm = DBM::test_db().await; - let n_trackers = 100; - let mut penalties_summaries = HashMap::new(); - - for i in 0..n_trackers { - let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - dbm.store_user(user_id, &user).await.unwrap(); - - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).await.unwrap(); - - let status = if i % 2 == 0 { - ConfirmationStatus::InMempoolSince(i) - } else { - ConfirmationStatus::ConfirmedIn(i) - }; - - let tracker = get_random_tracker(user_id, status); - dbm.store_tracker(uuid, &tracker).await.unwrap(); - - penalties_summaries.insert(uuid, (tracker.penalty_tx.txid(), status)); - } - - assert_eq!(dbm.load_penalties_summaries().await, penalties_summaries); - } - - #[tokio::test] - async fn test_store_load_last_known_block() { - let dbm = DBM::test_db().await; - - let mut block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); - dbm.store_last_known_block(&block_hash).await.unwrap(); - assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); - - // Update with a new hash to check it can be done - block_hash = BlockHash::from_slice(&get_random_bytes(32)).unwrap(); - dbm.store_last_known_block(&block_hash).await.unwrap(); - assert_eq!(dbm.load_last_known_block().await.unwrap(), block_hash); - } - - #[tokio::test] - async fn test_store_load_nonexistent_last_known_block() { - let dbm = DBM::test_db().await; - - assert!(dbm.load_last_known_block().await.is_none()); - } - - #[tokio::test] - async fn test_store_load_tower_key() { - let dbm = DBM::test_db().await; - - assert!(dbm.load_tower_key().await.is_none()); - for _ in 0..7 { - let sk = get_random_keypair().0; - dbm.store_tower_key(&sk).await.unwrap(); - assert_eq!(dbm.load_tower_key().await.unwrap(), sk); - } - } -} diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index 4f581870..3d3d6bf5 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; + +use tokio::sync::Mutex; use teos_common::appointment::{compute_appointment_slots, Locator}; use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; @@ -69,20 +71,20 @@ pub struct Gatekeeper { expiry_delta: u32, /// Map of users registered within the tower. registered_users: Mutex>, - /// A [DBM] (database manager) instance. Used to persist appointment data into disk. - dbm: Arc>, + /// A [DBM] (database manager) instance. Used to persist user data into disk. + dbm: Arc, } impl Gatekeeper { /// Creates a new [Gatekeeper] instance. - pub fn new( + pub async fn new( last_known_block_height: u32, subscription_slots: u32, subscription_duration: u32, expiry_delta: u32, - dbm: Arc>, + dbm: Arc, ) -> Self { - let registered_users = dbm.lock().unwrap().load_all_users(); + let registered_users = dbm.load_all_users().await; Gatekeeper { last_known_block_height: AtomicU32::new(last_known_block_height), subscription_slots, @@ -94,36 +96,34 @@ impl Gatekeeper { } /// Returns whether the [Gatekeeper] has been created from scratch (fresh) or from backed-up data. - pub fn is_fresh(&self) -> bool { - self.registered_users.lock().unwrap().is_empty() + pub async fn is_fresh(&self) -> bool { + self.registered_users.lock().await.is_empty() } /// Ges the number of users currently registered to the tower. - pub(crate) fn get_registered_users_count(&self) -> usize { - self.registered_users.lock().unwrap().len() + pub(crate) async fn get_registered_users_count(&self) -> usize { + self.registered_users.lock().await.len() } /// Gets the list of all registered user ids. - pub(crate) fn get_user_ids(&self) -> Vec { - self.registered_users - .lock() - .unwrap() - .keys() - .cloned() - .collect() + pub(crate) async fn get_user_ids(&self) -> Vec { + self.registered_users.lock().await.keys().cloned().collect() } /// Gets the data held by the tower about a given user. - pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { - let info = self.registered_users.lock().unwrap().get(&user_id).cloned(); - info.map(|info| (info, self.dbm.lock().unwrap().load_user_locators(user_id))) + pub(crate) async fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { + let info = self.registered_users.lock().await.get(&user_id).cloned(); + if let Some(info) = info { + return Some((info, self.dbm.load_user_locators(user_id).await)); + } + None } /// Authenticates a user. /// /// User authentication is performed using ECRecover against fixed messages (one for each command). /// Notice all interaction with the tower should be guarded by this. - pub(crate) fn authenticate_user( + pub(crate) async fn authenticate_user( &self, message: &[u8], signature: &str, @@ -133,7 +133,7 @@ impl Gatekeeper { .map_err(|_| AuthenticationFailure("Wrong message or signature."))?, ); - if self.registered_users.lock().unwrap().contains_key(&user_id) { + if self.registered_users.lock().await.contains_key(&user_id) { Ok(user_id) } else { Err(AuthenticationFailure("User not found.")) @@ -141,14 +141,14 @@ impl Gatekeeper { } /// Adds a new user to the tower (or updates its subscription if already registered). - pub(crate) fn add_update_user( + pub(crate) async fn add_update_user( &self, user_id: UserId, ) -> Result { let block_count = self.last_known_block_height.load(Ordering::Acquire); // TODO: For now, new calls to `add_update_user` add subscription_slots to the current count and reset the expiry time - let mut registered_users = self.registered_users.lock().unwrap(); + let mut registered_users = self.registered_users.lock().await; let user_info = match registered_users.get_mut(&user_id) { // User already exists, updating the info Some(user_info) => { @@ -160,7 +160,7 @@ impl Gatekeeper { .subscription_expiry .checked_add(self.subscription_duration) .unwrap_or(u32::MAX); - self.dbm.lock().unwrap().update_user(user_id, user_info); + self.dbm.update_user(user_id, user_info).await.ok(); user_info } @@ -171,11 +171,7 @@ impl Gatekeeper { block_count, block_count + self.subscription_duration, ); - self.dbm - .lock() - .unwrap() - .store_user(user_id, &user_info) - .unwrap(); + self.dbm.store_user(user_id, &user_info).await.unwrap(); registered_users.insert(user_id, user_info); registered_users.get_mut(&user_id).unwrap() @@ -191,21 +187,16 @@ impl Gatekeeper { } /// Adds an appointment to a given user, or updates it if already present in the system (and belonging to the requester). - pub(crate) fn add_update_appointment( + pub(crate) async fn add_update_appointment( &self, user_id: UserId, uuid: UUID, appointment: &ExtendedAppointment, ) -> Result { // For updates, the difference between the existing appointment size and the update is computed. - let mut registered_users = self.registered_users.lock().unwrap(); + let mut registered_users = self.registered_users.lock().await; let user_info = registered_users.get_mut(&user_id).unwrap(); - let used_blob_size = self - .dbm - .lock() - .unwrap() - .get_appointment_length(uuid) - .unwrap_or(0); + let used_blob_size = self.dbm.get_appointment_length(uuid).await.unwrap_or(0); let used_slots = compute_appointment_slots(used_blob_size, ENCRYPTED_BLOB_MAX_SIZE); let required_slots = @@ -217,7 +208,7 @@ impl Gatekeeper { // than the old appointment user_info.available_slots = (user_info.available_slots as i64 - diff) as u32; - self.dbm.lock().unwrap().update_user(user_id, user_info); + self.dbm.update_user(user_id, user_info).await.ok(); Ok(user_info.available_slots) } else { @@ -226,11 +217,11 @@ impl Gatekeeper { } /// Checks whether a subscription has expired. - pub(crate) fn has_subscription_expired( + pub(crate) async fn has_subscription_expired( &self, user_id: UserId, ) -> Result<(bool, u32), AuthenticationFailure<'_>> { - self.registered_users.lock().unwrap().get(&user_id).map_or( + self.registered_users.lock().await.get(&user_id).map_or( Err(AuthenticationFailure("User not found.")), |user_info| { Ok(( @@ -244,15 +235,14 @@ impl Gatekeeper { /// Gets a map of outdated users. Outdated users are those whose subscription has expired and the renewal grace period /// has already passed ([expiry_delta](Self::expiry_delta)). - pub(crate) fn get_outdated_users(&self, block_height: u32) -> Vec { - self.registered_users - .lock() - .unwrap() - .iter() + pub(crate) async fn get_outdated_users(&self, block_height: u32) -> Vec { + let registered_users = self.registered_users.lock().await.clone(); + registered_users + .into_iter() // NOTE: Ideally there won't be a user with `block_height > subscription_expiry + expiry_delta`, but // this might happen if we skip a couple of block connections due to a force update. .filter(|(_, info)| block_height >= info.subscription_expiry + self.expiry_delta) - .map(|(user_id, _)| *user_id) + .map(|(user_id, _)| user_id) .collect() } @@ -262,15 +252,17 @@ impl Gatekeeper { /// /// DISCUSS: When `refund` is `false` we don't give back the slots to the user for the deleted appointments. /// This is to discourage misbehavior (sending bad appointments, either non-decryptable or rejected by the network). - pub(crate) fn delete_appointments(&self, appointments: Vec, refund: bool) { - let mut dbm = self.dbm.lock().unwrap(); - + pub(crate) async fn delete_appointments(&self, appointments: Vec, refund: bool) { let updated_users = if refund { let mut updated_users = HashMap::new(); - let mut registered_users = self.registered_users.lock().unwrap(); + let mut registered_users = self.registered_users.lock().await; // Give back the consumed slots to each user. for uuid in appointments.iter() { - let (user_id, blob_size) = dbm.get_appointment_user_and_length(*uuid).unwrap(); + let (user_id, blob_size) = self + .dbm + .get_appointment_user_and_length(*uuid) + .await + .unwrap(); registered_users.get_mut(&user_id).unwrap().available_slots += compute_appointment_slots(blob_size, ENCRYPTED_BLOB_MAX_SIZE); updated_users.insert(user_id, registered_users[&user_id]); @@ -284,9 +276,11 @@ impl Gatekeeper { // An optimization for the case when only one appointment is being deleted without refunding. // This avoids creating a DB transaction for a single query. if appointments.len() == 1 && updated_users.is_empty() { - dbm.remove_appointment(appointments[0]) + self.dbm.remove_appointment(appointments[0]).await; } else { - dbm.batch_remove_appointments(&appointments, &updated_users); + self.dbm + .batch_remove_appointments(&appointments, &updated_users) + .await; } } } @@ -300,18 +294,18 @@ impl AsyncListen for Gatekeeper { log::info!("New block received: {}", block.header.block_hash()); // Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired. - let outdated_users = self.get_outdated_users(height); + let outdated_users = self.get_outdated_users(height).await; if !outdated_users.is_empty() { // Remove the outdated users from memory first. { - let mut registered_users = self.registered_users.lock().unwrap(); + let mut registered_users = self.registered_users.lock().await; // Removing each outdated user in a loop is more efficient than retaining non-outdated users // because retaining would loop over all the available users which is always more than the outdated ones. for outdated_user in outdated_users.iter() { registered_users.remove(outdated_user); } } - self.dbm.lock().unwrap().batch_remove_users(&outdated_users); + self.dbm.batch_remove_users(&outdated_users).await; } // Update last known block height @@ -401,9 +395,8 @@ mod tests { // Add the appointment to the database. This is normally done by the Watcher. gatekeeper .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); } @@ -458,7 +451,7 @@ mod tests { let receipt = gatekeeper.add_update_user(user_id).unwrap(); // The data should have been also added to the database assert_eq!( - gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), + gatekeeper.dbm.load_user(user_id).await.unwrap(), UserInfo::new( receipt.available_slots(), receipt.subscription_start(), @@ -481,7 +474,7 @@ mod tests { // Data in the database should have been updated too assert_eq!( - gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), + gatekeeper.dbm.load_user(user_id).await.unwrap(), UserInfo::new( updated_receipt.available_slots(), updated_receipt.subscription_start(), @@ -505,7 +498,7 @@ mod tests { // Data in the database remains untouched assert_eq!( - gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), + gatekeeper.dbm.load_user(user_id).await.unwrap(), UserInfo::new( updated_receipt.available_slots(), updated_receipt.subscription_start(), @@ -540,9 +533,8 @@ mod tests { // Simulate the watcher adding the appointment in the database. gatekeeper .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); @@ -550,7 +542,7 @@ mod tests { assert_eq!(slots_before, available_slots + 1); // Slots should have been updated in the database too. - let mut loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + let mut loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, available_slots); // Adding the exact same appointment should leave the slots count unchanged. @@ -563,7 +555,7 @@ mod tests { assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); - loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // If we add an update to an existing appointment with a bigger data blob (modulo ENCRYPTED_BLOB_MAX_SIZE), additional slots should be taken @@ -575,16 +567,15 @@ mod tests { // Simulate the watcher updating the appointment in the database. gatekeeper .dbm - .lock() - .unwrap() .update_appointment(uuid, &bigger_appointment) + .await .unwrap(); let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); - loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // Adding back a smaller update (modulo ENCRYPTED_BLOB_MAX_SIZE) should reduce the count @@ -594,16 +585,15 @@ mod tests { // Simulate the watcher updating the appointment in the database. gatekeeper .dbm - .lock() - .unwrap() .update_appointment(uuid, &appointment) + .await .unwrap(); let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); - loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // Adding an appointment with a different uuid should not count as an update @@ -614,16 +604,15 @@ mod tests { // Simulate the watcher adding the appointment in the database. gatekeeper .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); - loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); // Finally, trying to add an appointment when the user has no enough slots should fail @@ -641,7 +630,7 @@ mod tests { )); // The entry in the database should remain unchanged in this case - loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(); + loaded_user = gatekeeper.dbm.load_user(user_id).await.unwrap(); assert_eq!(loaded_user.available_slots, updated_slot_count); } @@ -721,9 +710,8 @@ mod tests { // Add the appointment to the database. This is normally done by the Watcher. gatekeeper .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); if i % 2 == 0 { uuids_to_delete.push(uuid); @@ -734,12 +722,11 @@ mod tests { if i % 5 == 0 { gatekeeper .dbm - .lock() - .unwrap() .store_tracker( uuid, &get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)), ) + .await .unwrap(); trackers.push(uuid); } @@ -751,17 +738,17 @@ mod tests { gatekeeper.delete_appointments(uuids_to_delete.clone(), false); for uuid in uuids_to_delete.clone() { - assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!gatekeeper.dbm.appointment_exists(uuid).await); } for uuid in rest { - assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(gatekeeper.dbm.appointment_exists(uuid).await); } for uuid in trackers { if uuids_to_delete.contains(&uuid) { // The tracker should be deleted as well. - assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + assert!(!gatekeeper.dbm.tracker_exists(uuid).await); } else { - assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + assert!(gatekeeper.dbm.tracker_exists(uuid).await); } } @@ -795,9 +782,8 @@ mod tests { // Add the appointment to the database. This is normally done by the Watcher. gatekeeper .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); if i % 2 == 0 { // We don't reduce the remaining slots for the appointments which are @@ -814,12 +800,11 @@ mod tests { if i % 5 == 0 { gatekeeper .dbm - .lock() - .unwrap() .store_tracker( uuid, &get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)), ) + .await .unwrap(); trackers.push(uuid); } @@ -831,17 +816,17 @@ mod tests { gatekeeper.delete_appointments(uuids_to_delete.clone(), true); for uuid in uuids_to_delete.clone() { - assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!gatekeeper.dbm.appointment_exists(uuid).await); } for uuid in rest { - assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(gatekeeper.dbm.appointment_exists(uuid).await); } for uuid in trackers { if uuids_to_delete.contains(&uuid) { // The tracker should be deleted as well. - assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + assert!(!gatekeeper.dbm.tracker_exists(uuid).await); } else { - assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid)); + assert!(gatekeeper.dbm.tracker_exists(uuid).await); } } @@ -884,7 +869,7 @@ mod tests { .lock() .unwrap() .contains_key(user_id)); - assert!(gatekeeper.dbm.lock().unwrap().load_user(*user_id).is_none()); + assert!(gatekeeper.dbm.load_user(*user_id).await.is_none()); } // Check that the last_known_block_header has been properly updated diff --git a/teos/src/lib.rs b/teos/src/lib.rs index dbd81b22..4999f542 100644 --- a/teos/src/lib.rs +++ b/teos/src/lib.rs @@ -8,6 +8,7 @@ pub mod protos { tonic::include_proto!("teos.v2"); } pub mod api; +pub mod async_listener; pub mod bitcoin_cli; pub mod carrier; pub mod chain_monitor; @@ -18,7 +19,6 @@ pub mod dbm; mod errors; mod extended_appointment; pub mod gatekeeper; -pub mod async_listener; pub mod responder; #[doc(hidden)] mod rpc_errors; @@ -26,6 +26,5 @@ pub mod tls; mod tx_index; pub mod watcher; -pub mod dbm_new; #[cfg(test)] mod test_utils; diff --git a/teos/src/main.rs b/teos/src/main.rs index 909d77b1..0ac14c66 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -26,7 +26,6 @@ use teos::carrier::Carrier; use teos::chain_monitor::ChainMonitor; use teos::config::{self, Config, Opt}; use teos::dbm::DBM; -use teos::dbm_new::DBM as NewDBM; use teos::gatekeeper::Gatekeeper; use teos::protos as msgs; use teos::protos::private_tower_services_server::PrivateTowerServicesServer; @@ -59,7 +58,7 @@ where Ok(last_n_blocks) } -async fn create_new_tower_keypair(dbm: &NewDBM) -> (SecretKey, PublicKey) { +async fn create_new_tower_keypair(dbm: &DBM) -> (SecretKey, PublicKey) { let (sk, pk) = get_random_keypair(); dbm.store_tower_key(&sk).await.unwrap(); (sk, pk) @@ -124,8 +123,8 @@ async fn main() { conf.log_non_default_options(); } - let new_dbm = if conf.database_url == "managed" { - teos::dbm_new::DBM::new(&format!( + let dbm = if conf.database_url == "managed" { + DBM::new(&format!( "sqlite://{}", path_network // rwc = Read + Write + Create (creates the database file if not found) @@ -136,25 +135,21 @@ async fn main() { .await .unwrap() } else { - teos::dbm_new::DBM::new(&conf.database_url).await.unwrap() + DBM::new(&conf.database_url).await.unwrap() }; - let new_dbm = Arc::new(new_dbm); - - let dbm = Arc::new(Mutex::new( - DBM::new(path_network.join("teos_db.sql3")).unwrap(), - )); + let dbm = Arc::new(dbm); // Load tower secret key or create a fresh one if none is found. If overwrite key is set, create a new // key straightaway let (tower_sk, tower_pk) = { if conf.overwrite_key { log::info!("Overwriting tower keys"); - create_new_tower_keypair(&new_dbm).await - } else if let Some(sk) = new_dbm.load_tower_key().await { + create_new_tower_keypair(&dbm).await + } else if let Some(sk) = dbm.load_tower_key().await { (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)) } else { log::info!("Tower keys not found. Creating a fresh set"); - create_new_tower_keypair(&new_dbm).await + create_new_tower_keypair(&dbm).await } }; log::info!("tower_id: {tower_pk}"); @@ -199,7 +194,7 @@ async fn main() { ); let mut derefed = bitcoin_cli.deref(); // Load last known block from DB if found. Poll it from Bitcoind otherwise. - let last_known_block = new_dbm.load_last_known_block().await; + let last_known_block = dbm.load_last_known_block().await; let tip = if let Some(block_hash) = last_known_block { let mut last_known_header = derefed .get_header(&block_hash, None) @@ -276,13 +271,16 @@ async fn main() { }; // Build components - let gatekeeper = Arc::new(Gatekeeper::new( - tip.height, - conf.subscription_slots, - conf.subscription_duration, - conf.expiry_delta, - dbm.clone(), - )); + let gatekeeper = Arc::new( + Gatekeeper::new( + tip.height, + conf.subscription_slots, + conf.subscription_duration, + conf.expiry_delta, + dbm.clone(), + ) + .await, + ); let mut poller = ChainPoller::new(&mut derefed, Network::from_str(btc_network).unwrap()); let (responder, watcher) = { @@ -314,7 +312,7 @@ async fn main() { (responder, watcher) }; - if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() { + if watcher.is_fresh().await & responder.is_fresh().await & gatekeeper.is_fresh().await { log::info!("Fresh bootstrap"); } else { log::info!("Bootstrapping from backed up data"); diff --git a/teos/src/responder.rs b/teos/src/responder.rs index 82d77ac0..dd3db4b6 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -130,7 +130,7 @@ pub struct Responder { /// A [Gatekeeper] instance. Data regarding users is requested to it. gatekeeper: Arc, /// A [DBM] (database manager) instance. Used to persist tracker data into disk. - dbm: Arc>, + dbm: Arc, /// A list of all the reorged trackers that might need to be republished after reorg resolution. reorged_trackers: Mutex>, } @@ -142,7 +142,7 @@ impl Responder { last_known_block_height: u32, carrier: Carrier, gatekeeper: Arc, - dbm: Arc>, + dbm: Arc, ) -> Self { Responder { carrier: Mutex::new(carrier), @@ -154,13 +154,14 @@ impl Responder { } /// Returns whether the [Responder] has been created from scratch (fresh) or from backed-up data. - pub fn is_fresh(&self) -> bool { - self.get_trackers_count() == 0 + pub async fn is_fresh(&self) -> bool { + self.get_trackers_count().await == 0 } /// Gets the total number of trackers in the [Responder]. - pub(crate) fn get_trackers_count(&self) -> usize { - self.dbm.lock().unwrap().get_trackers_count() + pub(crate) async fn get_trackers_count(&self) -> usize { + // replace with a pull from the database. + self.dbm.get_trackers_count().await } /// Checks whether the [Responder] has gone through a reorg and some transactions should to be resent. @@ -172,27 +173,29 @@ impl Responder { /// /// Breaches can either be added to the [Responder] in the form of a [TransactionTracker] if the [penalty transaction](Breach::penalty_tx) /// is accepted by the `bitcoind` or rejected otherwise. - pub(crate) fn handle_breach( + pub(crate) async fn handle_breach( &self, uuid: UUID, breach: Breach, user_id: UserId, ) -> ConfirmationStatus { - let mut carrier = self.carrier.lock().unwrap(); - let tx_index = self.tx_index.lock().unwrap(); - - // Check whether the transaction is in mempool or part of our internal txindex. Send it to our node otherwise. - let status = if let Some(block_hash) = tx_index.get(&breach.penalty_tx.txid()) { - ConfirmationStatus::ConfirmedIn(tx_index.get_height(block_hash).unwrap() as u32) - } else if carrier.in_mempool(&breach.penalty_tx.txid()) { - // If it's in mempool we assume it was just included - ConfirmationStatus::InMempoolSince(carrier.block_height()) - } else { - carrier.send_transaction(&breach.penalty_tx) + let status = { + let mut carrier = self.carrier.lock().unwrap(); + let tx_index = self.tx_index.lock().unwrap(); + + // Check whether the transaction is in mempool or part of our internal txindex. Send it to our node otherwise. + if let Some(block_hash) = tx_index.get(&breach.penalty_tx.txid()) { + ConfirmationStatus::ConfirmedIn(tx_index.get_height(block_hash).unwrap() as u32) + } else if carrier.in_mempool(&breach.penalty_tx.txid()) { + // If it's in mempool we assume it was just included + ConfirmationStatus::InMempoolSince(carrier.block_height()) + } else { + carrier.send_transaction(&breach.penalty_tx) + } }; if status.accepted() { - self.add_tracker(uuid, breach, user_id, status); + self.add_tracker(uuid, breach, user_id, status).await; } status @@ -206,7 +209,7 @@ impl Responder { /// /// Some transaction may already be confirmed by the time the tower tries to send them to the network. If that's the case, /// the [Responder] will simply continue tracking the job until its completion. - pub(crate) fn add_tracker( + pub(crate) async fn add_tracker( &self, uuid: UUID, breach: Breach, @@ -215,9 +218,8 @@ impl Responder { ) { if self .dbm - .lock() - .unwrap() .store_tracker(uuid, &TransactionTracker::new(breach, user_id, status)) + .await .is_ok() { log::info!("New tracker added (uuid={uuid})"); @@ -229,8 +231,8 @@ impl Responder { } /// Checks whether a given tracker can be found in the [Responder]. - pub(crate) fn has_tracker(&self, uuid: UUID) -> bool { - self.dbm.lock().unwrap().tracker_exists(uuid) + pub(crate) async fn has_tracker(&self, uuid: UUID) -> bool { + self.dbm.tracker_exists(uuid).await } /// Checks the confirmation count for the [TransactionTracker]s. @@ -238,21 +240,25 @@ impl Responder { /// For unconfirmed transactions, it checks whether they have been confirmed or keep missing confirmations. /// For confirmed transactions, nothing is done until they are completed (confirmation count reaches [IRREVOCABLY_RESOLVED](constants::IRREVOCABLY_RESOLVED)) /// Returns the set of completed trackers or [None] if none were completed. - fn check_confirmations(&self, txids: HashSet, current_height: u32) -> Option> { + async fn check_confirmations( + &self, + txids: HashSet, + current_height: u32, + ) -> Option> { let mut completed_trackers = Vec::new(); - let mut reorged_trackers = self.reorged_trackers.lock().unwrap(); - let dbm = self.dbm.lock().unwrap(); - for (uuid, penalty_summary) in dbm.load_penalties_summaries() { + for (uuid, penalty_summary) in self.dbm.load_penalties_summaries().await { if txids.contains(&penalty_summary.penalty_txid) { // First confirmation was received - dbm.update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(current_height)) + self.dbm + .update_tracker_status(uuid, &ConfirmationStatus::ConfirmedIn(current_height)) + .await .unwrap(); // Remove that uuid from reorged trackers if it was confirmed. - reorged_trackers.remove(&uuid); + self.reorged_trackers.lock().unwrap().remove(&uuid); // TODO: We won't need this check when we persist the correct tracker status // in the DB after migrations are supported. - } else if reorged_trackers.contains(&uuid) { + } else if self.reorged_trackers.lock().unwrap().contains(&uuid) { // Don't consider reorged trackers since they have wrong DB status. continue; } else if let ConfirmationStatus::ConfirmedIn(h) = penalty_summary.status { @@ -282,19 +288,22 @@ impl Responder { /// It tries to publish the dispute and penalty transactions of reorged trackers to the blockchain. /// /// Returns a vector of rejected trackers during rebroadcast if any were rejected, [None] otherwise. - fn handle_reorged_txs(&self, height: u32) -> Option> { + async fn handle_reorged_txs(&self, height: u32) -> Option> { // NOTE: We are draining the reorged trackers set, meaning that we won't try sending these disputes again. let reorged_trackers: Vec = self.reorged_trackers.lock().unwrap().drain().collect(); - let mut carrier = self.carrier.lock().unwrap(); - let dbm = self.dbm.lock().unwrap(); let mut rejected = Vec::new(); // Republish all the dispute transactions of the reorged trackers. for uuid in reorged_trackers { - let tracker = dbm.load_tracker(uuid).unwrap(); + let tracker = self.dbm.load_tracker(uuid).await.unwrap(); let dispute_txid = tracker.dispute_tx.txid(); // Try to publish the dispute transaction. - let should_publish_penalty = match carrier.send_transaction(&tracker.dispute_tx) { + let should_publish_penalty = match self + .carrier + .lock() + .unwrap() + .send_transaction(&tracker.dispute_tx) + { ConfirmationStatus::InMempoolSince(_) => { log::info!( "Reorged dispute tx (txid={}) is in the mempool now", @@ -325,15 +334,20 @@ impl Responder { if should_publish_penalty { // Try to rebroadcast the penalty tx. - if let ConfirmationStatus::Rejected(_) = - carrier.send_transaction(&tracker.penalty_tx) - { + let status = self + .carrier + .lock() + .unwrap() + .send_transaction(&tracker.penalty_tx); + if let ConfirmationStatus::Rejected(_) = status { rejected.push(uuid) } else { // The penalty might actually be confirmed (ConfirmationStatus::IrrevocablyResolved) since bitcoind // is fully synced with the stronger chain already, but we won't know which block was it confirmed in. // We should see the tracker appear in the blockchain in the next couple of connected blocks. - dbm.update_tracker_status(uuid, &ConfirmationStatus::InMempoolSince(height)) + self.dbm + .update_tracker_status(uuid, &ConfirmationStatus::InMempoolSince(height)) + .await .unwrap() } } else { @@ -350,9 +364,7 @@ impl Responder { /// fess and needs to be bumped, but there is not much we can do until anchors). /// /// Returns a vector of rejected trackers during rebroadcast if any were rejected, [None] otherwise. - fn rebroadcast_stale_txs(&self, height: u32) -> Option> { - let dbm = self.dbm.lock().unwrap(); - let mut carrier = self.carrier.lock().unwrap(); + async fn rebroadcast_stale_txs(&self, height: u32) -> Option> { let mut rejected = Vec::new(); // Retry sending trackers which have been in the mempool since more than `CONFIRMATIONS_BEFORE_RETRY` blocks. @@ -361,17 +373,23 @@ impl Responder { // NOTE: Ideally this will only pull UUIDs which have been in mempool since `CONFIRMATIONS_BEFORE_RETRY`, but // might also return ones which have been there for a longer period. This can only happen if the tower missed // a couple of block connections due to a force update. - for uuid in dbm + for uuid in self + .dbm .load_trackers_with_confirmation_status(stale_confirmation_status) + .await .unwrap() { - let tracker = dbm.load_tracker(uuid).unwrap(); + let tracker = self.dbm.load_tracker(uuid).await.unwrap(); log::warn!( "Penalty transaction has missed many confirmations: {}", tracker.penalty_tx.txid() ); // Rebroadcast the penalty transaction. - let status = carrier.send_transaction(&tracker.penalty_tx); + let status = self + .carrier + .lock() + .unwrap() + .send_transaction(&tracker.penalty_tx); if let ConfirmationStatus::Rejected(_) = status { rejected.push(uuid); } else { @@ -379,7 +397,7 @@ impl Responder { // Sending it will yield `ConfirmationStatus::IrrevocablyResolved` which would panic here. // We might want to replace `ConfirmationStatus::IrrevocablyResolved` variant with // `ConfirmationStatus::ConfirmedIn(height - IRREVOCABLY_RESOLVED) - dbm.update_tracker_status(uuid, &status).unwrap(); + self.dbm.update_tracker_status(uuid, &status).await.unwrap(); } } @@ -412,8 +430,11 @@ impl AsyncListen for Responder { self.tx_index.lock().unwrap().update(block.header, &txs); // Delete trackers completed at this height - if let Some(trackers) = self.check_confirmations(txs.keys().cloned().collect(), height) { - self.gatekeeper.delete_appointments(trackers, true); + if let Some(trackers) = self + .check_confirmations(txs.keys().cloned().collect(), height) + .await + { + self.gatekeeper.delete_appointments(trackers, true).await; } let mut trackers_to_delete = Vec::new(); @@ -421,19 +442,20 @@ impl AsyncListen for Responder { // We will need to update those trackers that have been reorged. if self.coming_from_reorg() { // Handle reorged transactions. This clears `self.reorged_trackers`. - if let Some(trackers) = self.handle_reorged_txs(height) { + if let Some(trackers) = self.handle_reorged_txs(height).await { trackers_to_delete.extend(trackers); } } // Rebroadcast those transactions that need to - if let Some(trackers) = self.rebroadcast_stale_txs(height) { + if let Some(trackers) = self.rebroadcast_stale_txs(height).await { trackers_to_delete.extend(trackers); } if !trackers_to_delete.is_empty() { self.gatekeeper - .delete_appointments(trackers_to_delete, false); + .delete_appointments(trackers_to_delete, false) + .await; } // Remove all receipts created in this block @@ -453,13 +475,15 @@ impl AsyncListen for Responder { // TODO: Not only confirmed trackers need to be marked as reorged, but trackers that hasn't confirmed but their // dispute did confirm in the reorged block. We can pull dispute txids of non confirmed penalties and get their // confirmation block from our tx_index. - self.reorged_trackers.lock().unwrap().extend( - self.dbm - .lock() - .unwrap() - .load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(height)) - .unwrap(), - ); + let reorged_trackers = self + .dbm + .load_trackers_with_confirmation_status(ConfirmationStatus::ConfirmedIn(height)) + .await + .unwrap(); + self.reorged_trackers + .lock() + .unwrap() + .extend(reorged_trackers); } } @@ -506,8 +530,8 @@ mod tests { impl Eq for Responder {} impl Responder { - pub(crate) fn get_trackers(&self) -> HashMap { - self.dbm.lock().unwrap().load_trackers(None) + pub(crate) async fn get_trackers(&self) -> HashMap { + self.dbm.load_trackers(None).await } pub(crate) fn get_carrier(&self) -> &Mutex { @@ -522,16 +546,15 @@ mod tests { tracker } - pub(crate) fn add_dummy_tracker(&self, tracker: &TransactionTracker) { + pub(crate) async fn add_dummy_tracker(&self, tracker: &TransactionTracker) { let (_, appointment) = generate_dummy_appointment_with_user( tracker.user_id, Some(&tracker.dispute_tx.txid()), ); - store_appointment_and_its_user(&self.dbm.lock().unwrap(), &appointment); + store_appointment_and_its_user(&self.dbm, &appointment).await; self.dbm - .lock() - .unwrap() .store_tracker(appointment.uuid(), tracker) + .await .unwrap(); } @@ -539,7 +562,7 @@ mod tests { let appointment = generate_dummy_appointment(None); let (uuid, user_id) = (appointment.uuid(), appointment.user_id); // Store the appointment and the user to the DB. - store_appointment_and_its_user(&self.dbm.lock().unwrap(), &appointment); + store_appointment_and_its_user(&self.dbm, &appointment).await; (user_id, uuid) } } @@ -547,7 +570,7 @@ mod tests { async fn create_responder( chain: &mut Blockchain, gatekeeper: Arc, - dbm: Arc>, + dbm: Arc, query: MockedServerQuery, ) -> (Responder, BitcoindStopper) { let height = if chain.tip().height < IRREVOCABLY_RESOLVED { @@ -568,7 +591,7 @@ mod tests { async fn init_responder_with_chain_and_dbm( mocked_query: MockedServerQuery, chain: &mut Blockchain, - dbm: Arc>, + dbm: Arc, ) -> (Responder, BitcoindStopper) { let gk = Gatekeeper::new( chain.get_block_count(), @@ -581,7 +604,7 @@ mod tests { } async fn init_responder(mocked_query: MockedServerQuery) -> (Responder, BitcoindStopper) { - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); init_responder_with_chain_and_dbm(mocked_query, &mut chain, dbm).await } @@ -624,7 +647,7 @@ mod tests { async fn test_responder_new() { // A fresh responder has no associated data let mut chain = Blockchain::default().with_height(START_HEIGHT); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); let (responder, _s) = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm.clone()) .await; @@ -662,7 +685,7 @@ mod tests { responder.handle_breach(uuid, breach, user_id), ConfirmationStatus::InMempoolSince(start_height) ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); + let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); assert_eq!( tracker.status, ConfirmationStatus::InMempoolSince(start_height) @@ -676,10 +699,7 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height) ); // Getting the tracker should return the old one. - assert_eq!( - tracker, - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap() - ); + assert_eq!(tracker, responder.dbm.load_tracker(uuid).await.unwrap()); } #[tokio::test] @@ -694,7 +714,7 @@ mod tests { responder.handle_breach(uuid, breach, user_id), ConfirmationStatus::InMempoolSince(start_height) ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); + let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); assert_eq!( tracker.status, ConfirmationStatus::InMempoolSince(start_height) @@ -729,7 +749,7 @@ mod tests { responder.handle_breach(uuid, breach, user_id), ConfirmationStatus::ConfirmedIn(target_height) ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); + let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); assert_eq!( tracker.status, ConfirmationStatus::ConfirmedIn(target_height) @@ -770,7 +790,7 @@ mod tests { // Check that the data has been added to the responder. assert_eq!( - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), + responder.dbm.load_tracker(uuid).await.unwrap(), TransactionTracker::new( breach, user_id, @@ -790,7 +810,7 @@ mod tests { ); assert_eq!( - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), + responder.dbm.load_tracker(uuid).await.unwrap(), TransactionTracker::new( breach.clone(), user_id, @@ -808,7 +828,7 @@ mod tests { ); assert_eq!( - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), + responder.dbm.load_tracker(uuid).await.unwrap(), TransactionTracker::new( breach, user_id, @@ -851,7 +871,7 @@ mod tests { let (user_id, uuid) = responder.store_dummy_appointment_to_db(); // Data should not be there before adding it - assert!(responder.dbm.lock().unwrap().load_tracker(uuid).is_none()); + assert!(responder.dbm.load_tracker(uuid).await.is_none()); // Data should be there now let breach = get_random_breach(); @@ -862,7 +882,7 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height), ); assert_eq!( - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), + responder.dbm.load_tracker(uuid).await.unwrap(), TransactionTracker::new( breach, user_id, @@ -872,7 +892,7 @@ mod tests { // After deleting the data it should be gone responder.gatekeeper.delete_appointments(vec![uuid], false); - assert!(responder.dbm.lock().unwrap().load_tracker(uuid).is_none()); + assert!(responder.dbm.load_tracker(uuid).await.is_none()); } #[tokio::test] @@ -943,13 +963,7 @@ mod tests { // The ones in mempool should still be there (at the same height) for uuid in in_mempool { assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status, + responder.dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::InMempoolSince(21) ); } @@ -957,13 +971,7 @@ mod tests { // The ones that just got confirmed should have been flagged so (at this height) for uuid in just_confirmed { assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status, + responder.dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::ConfirmedIn(target_height) ); } @@ -971,13 +979,7 @@ mod tests { // The ones that were already confirmed but have not reached the end should remain the same for uuid in confirmed { assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status, + responder.dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::ConfirmedIn(42) ); } @@ -1004,13 +1006,7 @@ mod tests { // And all the reorged trackers should have in mempool since `height` status. for uuid in trackers { assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status, + responder.dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::InMempoolSince(height) ); } @@ -1043,13 +1039,7 @@ mod tests { // And all the reorged trackers statuses should be untouched. for uuid in trackers { assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status, + responder.dbm.load_tracker(uuid).await.unwrap().status, ConfirmationStatus::ConfirmedIn(42) ); } @@ -1076,13 +1066,7 @@ mod tests { assert!(responder.rebroadcast_stale_txs(height).is_none()); for (uuid, former_status) in statues { - let status = responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status; + let status = responder.dbm.load_tracker(uuid).await.unwrap().status; if let ConfirmationStatus::InMempoolSince(h) = former_status { if height - h >= CONFIRMATIONS_BEFORE_RETRY as u32 { // Transactions which stayed for more than `CONFIRMATIONS_BEFORE_RETRY` should have been rebroadcasted. @@ -1134,13 +1118,7 @@ mod tests { assert_eq!(should_reject, rejected); for (uuid, former_status) in statues { - let status = responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .status; + let status = responder.dbm.load_tracker(uuid).await.unwrap().status; // All tracker statues shouldn't change since the submitted ones were all rejected. assert_eq!(status, former_status); } @@ -1148,11 +1126,14 @@ mod tests { #[tokio::test] async fn test_block_connected() { - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let start_height = START_HEIGHT * 2; let mut chain = Blockchain::default().with_height(start_height); - let (responder, _s) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; + let (responder, _s) = init_responder_with_chain_and_dbm( + MockedServerQuery::Regular, + &mut chain, + Arc::new(DBM::test_db().await), + ) + .await; // block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder // is keeping track of. @@ -1190,9 +1171,8 @@ mod tests { .unwrap(); responder .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); // Trackers complete in the next block. @@ -1217,9 +1197,8 @@ mod tests { .unwrap(); responder .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let breach = Breach::new(dispute_tx, get_random_tx()); @@ -1253,9 +1232,8 @@ mod tests { .unwrap(); responder .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let breach = Breach::new(dispute_tx, get_random_tx()); @@ -1289,9 +1267,8 @@ mod tests { .unwrap(); responder .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let breach = Breach::new(dispute_tx, get_random_tx()); @@ -1344,12 +1321,7 @@ mod tests { // COMPLETED TRACKERS CHECKS // Data should have been removed for tracker in completed_trackers { - assert!(responder - .dbm - .lock() - .unwrap() - .load_tracker(tracker.uuid()) - .is_none()); + assert!(responder.dbm.load_tracker(tracker.uuid()).await.is_none()); let (_, user_locators) = responder.gatekeeper.get_user_info(tracker.user_id).unwrap(); assert!(!user_locators.contains(&tracker.locator())); } @@ -1357,12 +1329,7 @@ mod tests { // OUTDATED TRACKERS CHECKS // Data should have been removed (tracker not found nor the user) for tracker in outdated_trackers { - assert!(responder - .dbm - .lock() - .unwrap() - .load_tracker(tracker.uuid()) - .is_none()); + assert!(responder.dbm.load_tracker(tracker.uuid()).await.is_none()); assert!(responder .gatekeeper .get_user_info(tracker.user_id) @@ -1375,9 +1342,8 @@ mod tests { assert_eq!( responder .dbm - .lock() - .unwrap() .load_tracker(tracker.uuid()) + .await .unwrap() .status, ConfirmationStatus::ConfirmedIn(target_block_height) @@ -1387,9 +1353,8 @@ mod tests { assert_eq!( responder .dbm - .lock() - .unwrap() .load_tracker(tracker.uuid()) + .await .unwrap() .status, ConfirmationStatus::InMempoolSince(target_block_height - 1) @@ -1401,9 +1366,8 @@ mod tests { assert_eq!( responder .dbm - .lock() - .unwrap() .load_tracker(tracker.uuid()) + .await .unwrap() .status, ConfirmationStatus::InMempoolSince(target_block_height), @@ -1413,10 +1377,13 @@ mod tests { #[tokio::test] async fn test_block_disconnected() { - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - let (responder, _s) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; + let (responder, _s) = init_responder_with_chain_and_dbm( + MockedServerQuery::Regular, + &mut chain, + Arc::new(DBM::test_db().await), + ) + .await; // Add user to the database let user_id = get_random_user_id(); @@ -1432,9 +1399,8 @@ mod tests { generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); responder .dbm - .lock() - .unwrap() .store_appointment(uuid, &appointment) + .await .unwrap(); let breach = Breach::new(dispute_tx, get_random_tx()); @@ -1450,7 +1416,9 @@ mod tests { // Check that trackers are flagged as reorged if the height they were included at gets disconnected for (i, uuid) in block_range.clone().zip(reorged.iter()).rev() { // The header doesn't really matter, just the height - responder.block_disconnected(&chain.tip().header, i as u32).await; + responder + .block_disconnected(&chain.tip().header, i as u32) + .await; // Check that the proper tracker gets reorged at the proper height assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); // Check that the carrier block_height has been updated diff --git a/teos/src/test_utils.rs b/teos/src/test_utils.rs index 4dba0a93..dfea978c 100644 --- a/teos/src/test_utils.rs +++ b/teos/src/test_utils.rs @@ -341,14 +341,16 @@ pub(crate) fn get_random_tracker( TransactionTracker::new(breach, user_id, status) } -pub(crate) fn store_appointment_and_its_user(dbm: &DBM, appointment: &ExtendedAppointment) { +pub(crate) async fn store_appointment_and_its_user(dbm: &DBM, appointment: &ExtendedAppointment) { dbm.store_user( appointment.user_id, &UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY), ) + .await // It's ok if the user is already stored. .ok(); dbm.store_appointment(appointment.uuid(), appointment) + .await .unwrap(); } @@ -392,7 +394,7 @@ pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> (Carrier, pub(crate) async fn create_responder( chain: &mut Blockchain, gatekeeper: Arc, - dbm: Arc>, + dbm: Arc, server_url: &str, ) -> Responder { let height = chain.tip().height; @@ -413,7 +415,7 @@ pub(crate) async fn create_watcher( responder: Arc, gatekeeper: Arc, bitcoind_mock: BitcoindMock, - dbm: Arc>, + dbm: Arc, ) -> (Watcher, BitcoindStopper) { let last_n_blocks = get_last_n_blocks(chain, 6).await; @@ -471,7 +473,7 @@ pub(crate) async fn create_api_with_config( let bitcoind_mock = BitcoindMock::new(MockOptions::default()); let mut chain = Blockchain::default().with_height(START_HEIGHT); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); let gk = Arc::new(Gatekeeper::new( chain.get_block_count(), api_config.slots, diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index 7df86b7d..3bc73d49 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -109,7 +109,7 @@ pub struct Watcher { /// The tower identifier. pub tower_id: TowerId, /// A [DBM] (database manager) instance. Used to persist appointment data into disk. - dbm: Arc>, + dbm: Arc, } impl Watcher { @@ -121,7 +121,7 @@ impl Watcher { last_known_block_height: u32, signing_key: SecretKey, tower_id: TowerId, - dbm: Arc>, + dbm: Arc, ) -> Self { Watcher { locator_cache: Mutex::new(TxIndex::new(last_n_blocks, last_known_block_height)), @@ -135,14 +135,17 @@ impl Watcher { } /// Returns whether the [Watcher] has been created from scratch (fresh) or from backed-up data. - pub fn is_fresh(&self) -> bool { - self.get_appointments_count() == 0 + pub async fn is_fresh(&self) -> bool { + self.get_appointments_count().await == 0 } /// Registers a new user within the [Watcher]. This request is passed to the [Gatekeeper], who is in /// charge of managing users. - pub(crate) fn register(&self, user_id: UserId) -> Result { - let mut receipt = self.gatekeeper.add_update_user(user_id)?; + pub(crate) async fn register( + &self, + user_id: UserId, + ) -> Result { + let mut receipt = self.gatekeeper.add_update_user(user_id).await?; receipt.sign(&self.signing_key); Ok(receipt) @@ -159,7 +162,7 @@ impl Watcher { /// If an appointment is accepted, an [ExtendedAppointment] (constructed from the [Appointment]) will be persisted on disk. /// In case the locator for the given appointment can be found in the cache (meaning the appointment has been /// triggered recently) the data will be passed to the [Responder] straightaway (modulo it being valid). - pub(crate) fn add_appointment( + pub(crate) async fn add_appointment( &self, appointment: Appointment, user_signature: String, @@ -167,10 +170,14 @@ impl Watcher { let user_id = self .gatekeeper .authenticate_user(&appointment.to_vec(), &user_signature) + .await .map_err(|_| AddAppointmentFailure::AuthenticationFailure)?; - let (has_subscription_expired, expiry) = - self.gatekeeper.has_subscription_expired(user_id).unwrap(); + let (has_subscription_expired, expiry) = self + .gatekeeper + .has_subscription_expired(user_id) + .await + .unwrap(); if has_subscription_expired { return Err(AddAppointmentFailure::SubscriptionExpired(expiry)); @@ -185,7 +192,7 @@ impl Watcher { let uuid = extended_appointment.uuid(); - if self.responder.has_tracker(uuid) { + if self.responder.has_tracker(uuid).await { log::info!("Tracker for {uuid} already found in Responder"); return Err(AddAppointmentFailure::AlreadyTriggered); } @@ -195,25 +202,28 @@ impl Watcher { let available_slots = self .gatekeeper .add_update_appointment(user_id, uuid, &extended_appointment) + .await .map_err(|_| AddAppointmentFailure::NotEnoughSlots)?; // FIXME: There's an edge case here if store_triggered_appointment is called and bitcoind is unreachable. // This will hang, the request will timeout but be accepted. However, the user will not be handed the receipt. // This could be fixed adding a thread to take care of storing while the main thread returns the receipt. // Not fixing this atm since working with threads that call self.method is surprisingly non-trivial. - match self + let dispute_tx = self .locator_cache .lock() .unwrap() .get(&extended_appointment.locator()) - { + .cloned(); + match dispute_tx { // Appointments that were triggered in blocks held in the cache Some(dispute_tx) => { - self.store_triggered_appointment(uuid, &extended_appointment, user_id, dispute_tx); + self.store_triggered_appointment(uuid, &extended_appointment, user_id, &dispute_tx) + .await; } // Regular appointments that have not been triggered (or, at least, not recently) None => { - self.store_appointment(uuid, &extended_appointment); + self.store_appointment(uuid, &extended_appointment).await; } }; @@ -227,21 +237,23 @@ impl Watcher { } /// Stores an appointment in the database (or updates it if it already exists). - fn store_appointment( + async fn store_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, ) -> StoredAppointment { - let dbm = self.dbm.lock().unwrap(); - if dbm.appointment_exists(uuid) { + if self.dbm.appointment_exists(uuid).await { log::debug!( "User {} is updating appointment {uuid}", appointment.user_id ); - dbm.update_appointment(uuid, appointment).unwrap(); + self.dbm + .update_appointment(uuid, appointment) + .await + .unwrap(); StoredAppointment::Update } else { - dbm.store_appointment(uuid, appointment).unwrap(); + self.dbm.store_appointment(uuid, appointment).await.unwrap(); StoredAppointment::New } } @@ -250,7 +262,7 @@ impl Watcher { /// /// If the appointment is rejected by the [Responder] (i.e. for being invalid), the data is wiped /// from the database but the slot is not freed. - fn store_triggered_appointment( + async fn store_triggered_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, @@ -266,21 +278,22 @@ impl Watcher { // Data needs to be added the database straightaway since appointments are // FKs to trackers. If handle breach fails, data will be deleted later. self.dbm - .lock() - .unwrap() .store_appointment(uuid, appointment) + .await // TODO: Don't unwrap, or better, make this insertion atomic with the // `responder.has_tracker` that might cause the unwrap in the first place. // ref: https://github.com/talaia-labs/rust-teos/pull/190#discussion_r1218235632 .unwrap(); - if let ConfirmationStatus::Rejected(reason) = self.responder.handle_breach( - uuid, - Breach::new(dispute_tx.clone(), penalty_tx), - user_id, - ) { + if let ConfirmationStatus::Rejected(reason) = self + .responder + .handle_breach(uuid, Breach::new(dispute_tx.clone(), penalty_tx), user_id) + .await + { + // DISCUSS: We could either free the slots or keep it occupied as if this was misbehavior. + // Keeping it for now. log::warn!("Appointment bounced in the Responder. Reason: {reason:?}"); - self.gatekeeper.delete_appointments(vec![uuid], false); + self.gatekeeper.delete_appointments(vec![uuid], false).await; TriggeredAppointment::Rejected } else { log::info!("Appointment went straight to the Responder"); @@ -309,7 +322,7 @@ impl Watcher { /// - The user subscription has not expired /// - The appointment belongs to the user /// - The appointment exists within the system (either in the [Watcher] or the [Responder]) - pub(crate) fn get_appointment( + pub(crate) async fn get_appointment( &self, locator: Locator, user_signature: &str, @@ -319,27 +332,27 @@ impl Watcher { let user_id = self .gatekeeper .authenticate_user(message.as_bytes(), user_signature) + .await .map_err(|_| GetAppointmentFailure::AuthenticationFailure)?; - let (has_subscription_expired, expiry) = - self.gatekeeper.has_subscription_expired(user_id).unwrap(); + let (has_subscription_expired, expiry) = self + .gatekeeper + .has_subscription_expired(user_id) + .await + .unwrap(); if has_subscription_expired { return Err(GetAppointmentFailure::SubscriptionExpired(expiry)); } let uuid = UUID::new(locator, user_id); - let dbm = self.dbm.lock().unwrap(); - dbm.load_tracker(uuid) - .map(AppointmentInfo::Tracker) - .or_else(|| { - dbm.load_appointment(uuid) - .map(|ext_app| AppointmentInfo::Appointment(ext_app.inner)) - }) - .ok_or_else(|| { - log::info!("Cannot find {locator}"); - GetAppointmentFailure::NotFound - }) + if let Some(tracker) = self.dbm.load_tracker(uuid).await { + Ok(AppointmentInfo::Tracker(tracker)) + } else if let Some(appointment) = self.dbm.load_appointment(uuid).await { + Ok(AppointmentInfo::Appointment(appointment.inner)) + } else { + Err(GetAppointmentFailure::NotFound) + } } /// Gets a map of breaches provided a map between locators and transactions. @@ -347,15 +360,14 @@ impl Watcher { /// The provided map if intersected with the map of all locators monitored by [Watcher] and the result /// is considered the list of all breaches. This is queried on a per-block basis with all the /// `(locator, transaction)` pairs computed from the transaction data. - fn get_breaches( + async fn get_breaches( &self, locator_tx_map: HashMap, ) -> HashMap { let breaches: HashMap = self .dbm - .lock() - .unwrap() .batch_check_locators_exist(locator_tx_map.keys().collect()) + .await .iter() .map(|locator| (*locator, locator_tx_map[locator].clone())) .collect(); @@ -375,21 +387,24 @@ impl Watcher { /// If the decryption fails for some appointments or if it succeeds but they get rejected when sent to the network, /// they are marked as an invalid breaches and returned. /// [None] is returned if none of these breaches are invalid. - fn handle_breaches(&self, breaches: HashMap) -> Option> { + async fn handle_breaches(&self, breaches: HashMap) -> Option> { let mut invalid_breaches = Vec::new(); for (locator, dispute_tx) in breaches.into_iter() { - // WARNING(deadlock): Don't lock `self.dbm` over the loop since `Responder::handle_breach` uses it as well. - let uuids = self.dbm.lock().unwrap().load_uuids(locator); + let uuids = self.dbm.load_uuids(locator).await; for uuid in uuids { - let appointment = self.dbm.lock().unwrap().load_appointment(uuid).unwrap(); + let appointment = self.dbm.load_appointment(uuid).await.unwrap(); match cryptography::decrypt(appointment.encrypted_blob(), &dispute_tx.txid()) { Ok(penalty_tx) => { - if let ConfirmationStatus::Rejected(_) = self.responder.handle_breach( - uuid, - Breach::new(dispute_tx.clone(), penalty_tx), - appointment.user_id, - ) { + if let ConfirmationStatus::Rejected(_) = self + .responder + .handle_breach( + uuid, + Breach::new(dispute_tx.clone(), penalty_tx), + appointment.user_id, + ) + .await + { invalid_breaches.push(uuid); } } @@ -404,58 +419,58 @@ impl Watcher { } /// Ges the number of users currently registered with the tower. - pub(crate) fn get_registered_users_count(&self) -> usize { - self.gatekeeper.get_registered_users_count() + pub(crate) async fn get_registered_users_count(&self) -> usize { + self.gatekeeper.get_registered_users_count().await } /// Gets the total number of appointments excluding trackers. - pub(crate) fn get_appointments_count(&self) -> usize { - self.dbm.lock().unwrap().get_appointments_count() + pub(crate) async fn get_appointments_count(&self) -> usize { + self.dbm.get_appointments_count().await } /// Gets the total number of trackers in the [Responder]. - pub(crate) fn get_trackers_count(&self) -> usize { - self.responder.get_trackers_count() + pub(crate) async fn get_trackers_count(&self) -> usize { + self.responder.get_trackers_count().await } /// Gets all the appointments stored in the [Watcher] (from the database). - pub(crate) fn get_all_watcher_appointments(&self) -> HashMap { - self.dbm.lock().unwrap().load_appointments(None) + pub(crate) async fn get_all_watcher_appointments(&self) -> HashMap { + self.dbm.load_appointments(None).await } /// Gets all the appointments matching a specific locator from the [Watcher] (from the database). - pub(crate) fn get_watcher_appointments_with_locator( + pub(crate) async fn get_watcher_appointments_with_locator( &self, locator: Locator, ) -> HashMap { - self.dbm.lock().unwrap().load_appointments(Some(locator)) + self.dbm.load_appointments(Some(locator)).await } /// Gets all the trackers stored in the [Responder] (from the database). - pub(crate) fn get_all_responder_trackers(&self) -> HashMap { - self.dbm.lock().unwrap().load_trackers(None) + pub(crate) async fn get_all_responder_trackers(&self) -> HashMap { + self.dbm.load_trackers(None).await } /// Gets all the trackers matching s specific locator from the [Responder] (from the database). - pub(crate) fn get_responder_trackers_with_locator( + pub(crate) async fn get_responder_trackers_with_locator( &self, locator: Locator, ) -> HashMap { - self.dbm.lock().unwrap().load_trackers(Some(locator)) + self.dbm.load_trackers(Some(locator)).await } /// Gets the list of all registered user ids. - pub(crate) fn get_user_ids(&self) -> Vec { - self.gatekeeper.get_user_ids() + pub(crate) async fn get_user_ids(&self) -> Vec { + self.gatekeeper.get_user_ids().await } /// Gets the data held by the tower about a given user. - pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { - self.gatekeeper.get_user_info(user_id) + pub(crate) async fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec)> { + self.gatekeeper.get_user_info(user_id).await } /// Gets information about a user's subscription. - pub(crate) fn get_subscription_info( + pub(crate) async fn get_subscription_info( &self, signature: &str, ) -> Result<(UserInfo, Vec), GetSubscriptionInfoFailure> { @@ -464,16 +479,20 @@ impl Watcher { let user_id = self .gatekeeper .authenticate_user(message.as_bytes(), signature) + .await .map_err(|_| GetSubscriptionInfoFailure::AuthenticationFailure)?; - let (has_subscription_expired, expiry) = - self.gatekeeper.has_subscription_expired(user_id).unwrap(); + let (has_subscription_expired, expiry) = self + .gatekeeper + .has_subscription_expired(user_id) + .await + .unwrap(); if has_subscription_expired { return Err(GetSubscriptionInfoFailure::SubscriptionExpired(expiry)); } - let (subscription_info, locators) = self.gatekeeper.get_user_info(user_id).unwrap(); + let (subscription_info, locators) = self.gatekeeper.get_user_info(user_id).await.unwrap(); Ok((subscription_info, locators)) } } @@ -507,8 +526,13 @@ impl AsyncListen for Watcher { .update(block.header, &locator_tx_map); // Get the breaches found in this block, handle them, and delete invalid ones. - if let Some(invalid_breaches) = self.handle_breaches(self.get_breaches(locator_tx_map)) { - self.gatekeeper.delete_appointments(invalid_breaches, false); + if let Some(invalid_breaches) = self + .handle_breaches(self.get_breaches(locator_tx_map).await) + .await + { + self.gatekeeper + .delete_appointments(invalid_breaches, false) + .await; } // Update last known block @@ -574,13 +598,13 @@ mod tests { } async fn init_watcher(chain: &mut Blockchain) -> (Watcher, BitcoindStopper) { - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); init_watcher_with_db(chain, dbm).await } async fn init_watcher_with_db( chain: &mut Blockchain, - dbm: Arc>, + dbm: Arc, ) -> (Watcher, BitcoindStopper) { let bitcoind_mock = BitcoindMock::new(MockOptions::default()); @@ -623,7 +647,7 @@ mod tests { async fn test_new() { // A fresh watcher has no associated data let mut chain = Blockchain::default().with_height(START_HEIGHT); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); let (watcher, _s) = init_watcher_with_db(&mut chain, dbm.clone()).await; assert!(watcher.is_fresh()); @@ -641,7 +665,7 @@ mod tests { .unwrap(); } - // Create a new Responder reusing the same DB and check that the data is loaded + // Create a new watcher reusing the same DB and check that the data is loaded let (another_w, _as) = init_watcher_with_db(&mut chain, dbm).await; assert!(!another_w.is_fresh()); assert_eq!(watcher, another_w); @@ -786,7 +810,7 @@ mod tests { assert_eq!(watcher.responder.get_trackers_count(), 2); // Data should not be in the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // Transaction rejected // Update the Responder with a new Carrier @@ -809,7 +833,7 @@ mod tests { assert_eq!(watcher.responder.get_trackers_count(), 2); // Data should not be in the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // FAIL cases (non-registered, subscription expired and not enough slots) @@ -822,7 +846,7 @@ mod tests { Err(AddAppointmentFailure::AuthenticationFailure) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // If the user has no enough slots, the appointment is rejected. We do not test all possible cases since updates are // already tested int he Gatekeeper. Testing that it is rejected if the condition is met should suffice. @@ -843,7 +867,7 @@ mod tests { Err(AddAppointmentFailure::NotEnoughSlots) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // If the user subscription has expired, the appointment should be rejected. watcher @@ -858,7 +882,7 @@ mod tests { Err(AddAppointmentFailure::SubscriptionExpired { .. }) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); } #[tokio::test] @@ -921,7 +945,7 @@ mod tests { ); // In this case the appointment is kept in the Responder and, therefore, in the database assert!(watcher.responder.has_tracker(uuid)); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(watcher.dbm.appointment_exists(uuid).await); // A properly formatted but invalid transaction should be rejected by the Responder // Update the Responder with a new Carrier that will reject the transaction @@ -939,7 +963,7 @@ mod tests { ); // In this case the appointment is not kept in the Responder nor in the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // Invalid triggered appointments should not be passed to the Responder // Use a dispute_tx that does not match the appointment to replicate a decryption error @@ -951,7 +975,7 @@ mod tests { ); // The appointment is not kept anywhere assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); } #[tokio::test] @@ -1266,7 +1290,7 @@ mod tests { // Both appointments can be found before mining a block, only the user's 2 can be found afterwards for &uuid in &[uuid1, uuid2] { - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(watcher.dbm.appointment_exists(uuid).await); } // We always need to connect the gatekeeper first so it cleans up outdated users and their data. @@ -1280,14 +1304,14 @@ mod tests { .await; // uuid1 and user1 should have been deleted while uuid2 and user2 still exists. - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid1)); + assert!(!watcher.dbm.appointment_exists(uuid1).await); assert!(!watcher .gatekeeper .get_registered_users() .lock() .unwrap() .contains_key(&user_id)); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid2)); + assert!(watcher.dbm.appointment_exists(uuid2).await); assert!(watcher .gatekeeper .get_registered_users() @@ -1302,7 +1326,7 @@ mod tests { let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); watcher.add_appointment(appointment.inner, sig).unwrap(); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(watcher.dbm.appointment_exists(uuid).await); let block = chain.generate(Some(vec![dispute_tx])); watcher @@ -1336,7 +1360,7 @@ mod tests { // Data should have been wiped from the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); // Check triggering with a valid formatted transaction but that is rejected by the Responder. let dispute_tx = get_random_tx(); @@ -1364,7 +1388,7 @@ mod tests { // Data should have been wiped from the database assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(!watcher.dbm.appointment_exists(uuid).await); } #[tokio::test] From 38abec0e06341b5a9c8cffa212466136bb2c8234 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 5 Aug 2023 20:28:20 +0300 Subject: [PATCH 06/15] Adjusted the tests (basically adding .awaits) Postgres tests are still not working because with each test we need to spawn a brand new database (best done inside a docker container) from within the tests --- teos/src/api/http.rs | 3 +- teos/src/api/internal.rs | 89 ++++++++++--- teos/src/dbm.rs | 50 ++++--- teos/src/gatekeeper.rs | 158 +++++++++++++--------- teos/src/responder.rs | 276 +++++++++++++++++++++++---------------- teos/src/test_utils.rs | 17 ++- teos/src/watcher.rs | 250 ++++++++++++++++++++++------------- 7 files changed, 531 insertions(+), 312 deletions(-) diff --git a/teos/src/api/http.rs b/teos/src/api/http.rs index a041ee34..221228a8 100644 --- a/teos/src/api/http.rs +++ b/teos/src/api/http.rs @@ -842,7 +842,8 @@ mod tests_methods { ); internal_api .get_watcher() - .add_dummy_tracker_to_responder(&tracker); + .add_dummy_tracker_to_responder(&tracker) + .await; // Try to add it via the http API let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())).inner; diff --git a/teos/src/api/internal.rs b/teos/src/api/internal.rs index e8695740..a7fe3b73 100644 --- a/teos/src/api/internal.rs +++ b/teos/src/api/internal.rs @@ -477,13 +477,18 @@ mod tests_private_api { // Add data to the Watcher so we can retrieve it later on let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); let appointment = generate_dummy_appointment(None).inner; let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); internal_api .watcher .add_appointment(appointment.clone(), user_signature) + .await .unwrap(); let response = internal_api @@ -504,7 +509,7 @@ mod tests_private_api { let (internal_api, _s) = create_api().await; // Add data to the Responser so we can retrieve it later on - internal_api.watcher.add_random_tracker_to_responder(); + internal_api.watcher.add_random_tracker_to_responder().await; let response = internal_api .get_all_appointments(Request::new(())) @@ -547,12 +552,17 @@ mod tests_private_api { // Add that many appointments to the watcher. for _ in 0..appointments_to_create { let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); let appointment = generate_dummy_appointment(Some(&dispute_txid)).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); internal_api .watcher .add_appointment(appointment, signature) + .await .unwrap(); } @@ -604,7 +614,8 @@ mod tests_private_api { ); internal_api .watcher - .add_dummy_tracker_to_responder(&tracker); + .add_dummy_tracker_to_responder(&tracker) + .await; } let locator = Locator::new(dispute_tx.txid()); @@ -657,7 +668,7 @@ mod tests_private_api { // Register a user let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - internal_api.watcher.register(user_id).unwrap(); + internal_api.watcher.register(user_id).await.unwrap(); // Add data to the Watcher for _ in 0..2 { @@ -666,12 +677,13 @@ mod tests_private_api { internal_api .watcher .add_appointment(appointment.clone(), user_signature) + .await .unwrap(); } // And the Responder for _ in 0..3 { - internal_api.watcher.add_random_tracker_to_responder(); + internal_api.watcher.add_random_tracker_to_responder().await; } let response = internal_api @@ -696,7 +708,7 @@ mod tests_private_api { for _ in 0..2 { let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - internal_api.watcher.register(user_id).unwrap(); + internal_api.watcher.register(user_id).await.unwrap(); users.insert(user_id.to_vec()); } @@ -729,7 +741,7 @@ mod tests_private_api { // Register a user and get it back let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - internal_api.watcher.register(user_id).unwrap(); + internal_api.watcher.register(user_id).await.unwrap(); let response = internal_api .get_user(Request::new(msgs::GetUserRequest { @@ -749,6 +761,7 @@ mod tests_private_api { internal_api .watcher .add_appointment(appointment.inner, user_signature) + .await .unwrap(); let response = internal_api @@ -912,7 +925,11 @@ mod tests_public_api { // User must be registered let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); let appointment = generate_dummy_appointment(None).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); @@ -966,7 +983,11 @@ mod tests_public_api { // User is registered but has no slots let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); let appointment = generate_dummy_appointment(None).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); @@ -995,7 +1016,11 @@ mod tests_public_api { // User is registered but subscription is expired let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); let appointment = generate_dummy_appointment(None).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); @@ -1021,7 +1046,7 @@ mod tests_public_api { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - internal_api.watcher.register(user_id).unwrap(); + internal_api.watcher.register(user_id).await.unwrap(); // Add a tracker to the responder to simulate it being triggered. let dispute_tx = get_random_tx(); @@ -1032,7 +1057,8 @@ mod tests_public_api { ); internal_api .get_watcher() - .add_dummy_tracker_to_responder(&tracker); + .add_dummy_tracker_to_responder(&tracker) + .await; // Try to add it again using the API. let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())).inner; @@ -1084,7 +1110,11 @@ mod tests_public_api { // The user must be registered let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // Add the appointment let appointment = generate_dummy_appointment(None).inner; @@ -1092,6 +1122,7 @@ mod tests_public_api { internal_api .watcher .add_appointment(appointment.clone(), user_signature) + .await .unwrap(); // Get the appointment through the API @@ -1117,7 +1148,11 @@ mod tests_public_api { // Add a first user to link the appointment to him let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // There's no need to add the appointment given the subscription status is checked first let appointment = generate_dummy_appointment(None).inner; @@ -1145,7 +1180,11 @@ mod tests_public_api { // The user is registered but the appointment does not exist let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // Try to get the appointment through the API let appointment = generate_dummy_appointment(None).inner; @@ -1172,7 +1211,11 @@ mod tests_public_api { // Register the user let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // There s no need to add the appointment given the subscription status is checked first. let appointment = generate_dummy_appointment(None).inner; @@ -1223,7 +1266,11 @@ mod tests_public_api { // The user must be registered let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // Get the subscription info though the API let message = "get subscription info".to_string(); @@ -1270,7 +1317,11 @@ mod tests_public_api { // The user is registered but the subscription has expired let (user_sk, user_pk) = get_random_keypair(); - internal_api.watcher.register(UserId(user_pk)).unwrap(); + internal_api + .watcher + .register(UserId(user_pk)) + .await + .unwrap(); // Try to get the subscription info though the API let message = "get subscription info".to_string(); diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 9cf4787c..57a6a05e 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -153,7 +153,7 @@ impl DBM { } /// Removes some users from the database in batch. - pub(crate) async fn batch_remove_users(&self, users: &Vec) -> usize { + pub(crate) async fn batch_remove_users(&self, users: Vec) -> usize { let users: Vec<_> = users.iter().map(|uuid| uuid.to_vec()).collect(); for chunk in users.chunks(SQL_VARIABLE_LIMIT) { @@ -330,6 +330,7 @@ impl DBM { /// Removes an [Appointment] from the database. pub(crate) async fn remove_appointment(&self, uuid: UUID) { if let Err(e) = sqlx::query("DELETE FROM appointments WHERE UUID=($1)") + .bind(uuid.to_vec()) .execute(&self.pool) .await { @@ -342,8 +343,8 @@ impl DBM { /// update is atomic. pub(crate) async fn batch_remove_appointments( &self, - appointments: &Vec, - updated_users: &HashMap, + appointments: Vec, + updated_users: HashMap, ) -> usize { let uuids: Vec<_> = appointments.iter().map(|uuid| uuid.to_vec()).collect(); let mut tx = self.pool.begin().await.unwrap(); @@ -790,7 +791,7 @@ mod tests { // SQL_VARIABLE_LIMIT is 10 for tests, // Check that deletion had `ceil(10 * 3 / 2) / 10` (2) queries on it - assert_eq!(dbm.batch_remove_users(&to_be_deleted).await, 2); + assert_eq!(dbm.batch_remove_users(to_be_deleted).await, 2); // Check user data was deleted assert_eq!(rest, dbm.load_all_users().await.keys().cloned().collect()); @@ -813,7 +814,7 @@ mod tests { // Appointment only dbm.store_appointment(uuid, &appointment).await.unwrap(); - dbm.batch_remove_users(&vec![appointment.user_id]).await; + dbm.batch_remove_users(vec![appointment.user_id]).await; assert!(dbm.load_user(appointment.user_id).await.is_none()); assert!(dbm.load_appointment(uuid).await.is_none()); @@ -822,7 +823,7 @@ mod tests { dbm.store_appointment(uuid, &appointment).await.unwrap(); dbm.store_tracker(uuid, &tracker).await.unwrap(); - dbm.batch_remove_users(&vec![appointment.user_id]).await; + dbm.batch_remove_users(vec![appointment.user_id]).await; assert!(dbm.load_user(appointment.user_id).await.is_none()); assert!(dbm.load_appointment(uuid).await.is_none()); assert!(dbm.load_tracker(uuid).await.is_none()); @@ -834,7 +835,7 @@ mod tests { let users = (0..10).map(|_| get_random_user_id()).collect(); // Test it does not fail even if the user does not exist - dbm.batch_remove_users(&users).await; + dbm.batch_remove_users(users).await; } #[tokio::test] @@ -1100,6 +1101,25 @@ mod tests { assert_eq!(dbm.load_appointments(Some(locator)).await, appointments); } + #[tokio::test] + async fn test_remove_appointment() { + let dbm = DBM::test_db().await; + + let user_id = get_random_user_id(); + let user = UserInfo::new( + AVAILABLE_SLOTS + 123, + SUBSCRIPTION_START, + SUBSCRIPTION_EXPIRY, + ); + dbm.store_user(user_id, &user).await.unwrap(); + + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + dbm.store_appointment(uuid, &appointment).await.unwrap(); + + dbm.remove_appointment(uuid).await; + assert!(!dbm.appointment_exists(uuid).await) + } + #[tokio::test] async fn test_batch_remove_appointments() { let dbm = DBM::test_db().await; @@ -1133,7 +1153,7 @@ mod tests { // Check that the db transaction had i queries on it assert_eq!( - dbm.batch_remove_appointments(&to_be_deleted, &updated_users) + dbm.batch_remove_appointments(to_be_deleted, updated_users) .await, i as usize ); @@ -1167,8 +1187,8 @@ mod tests { dbm.store_appointment(uuid, &appointment).await.unwrap(); dbm.batch_remove_appointments( - &vec![uuid], - &HashMap::from_iter([(appointment.user_id, info.clone())]), + vec![uuid], + HashMap::from_iter([(appointment.user_id, info.clone())]), ) .await; assert!(dbm.load_appointment(uuid).await.is_none()); @@ -1178,8 +1198,8 @@ mod tests { dbm.store_tracker(uuid, &tracker).await.unwrap(); dbm.batch_remove_appointments( - &vec![uuid], - &HashMap::from_iter([(appointment.user_id, info)]), + vec![uuid], + HashMap::from_iter([(appointment.user_id, info)]), ) .await; assert!(dbm.load_appointment(uuid).await.is_none()); @@ -1192,7 +1212,7 @@ mod tests { let appointments = (0..10).map(|_| generate_uuid()).collect(); // Test it does not fail even if the user does not exist - dbm.batch_remove_appointments(&appointments, &HashMap::new()) + dbm.batch_remove_appointments(appointments, HashMap::new()) .await; } @@ -1499,7 +1519,7 @@ mod tests { } #[tokio::test] - fn test_load_trackers_with_confirmation_status_confirmed() { + async fn test_load_trackers_with_confirmation_status_confirmed() { let dbm = DBM::test_db().await; let n_blocks = 100; let n_trackers = 30; @@ -1556,7 +1576,7 @@ mod tests { #[tokio::test] async fn test_load_trackers_with_confirmation_status_bad_status() { - let dbm = DBM::in_memory().unwrap(); + let dbm = DBM::test_db().await; assert!(matches!( dbm.load_trackers_with_confirmation_status(ConfirmationStatus::Rejected( diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index 3d3d6bf5..5b874fc2 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -279,7 +279,7 @@ impl Gatekeeper { self.dbm.remove_appointment(appointments[0]).await; } else { self.dbm - .batch_remove_appointments(&appointments, &updated_users) + .batch_remove_appointments(appointments, updated_users) .await; } } @@ -305,7 +305,7 @@ impl AsyncListen for Gatekeeper { registered_users.remove(outdated_user); } } - self.dbm.batch_remove_users(&outdated_users).await; + self.dbm.batch_remove_users(outdated_users).await; } // Update last known block height @@ -342,9 +342,10 @@ mod tests { self.subscription_slots == other.subscription_slots && self.subscription_duration == other.subscription_duration && self.expiry_delta == other.expiry_delta - && *self.registered_users.lock().unwrap() == *other.registered_users.lock().unwrap() && self.last_known_block_height.load(Ordering::Relaxed) == other.last_known_block_height.load(Ordering::Relaxed) + && *self.registered_users.try_lock().unwrap() + == *other.registered_users.try_lock().unwrap() } } impl Eq for Gatekeeper {} @@ -354,24 +355,24 @@ mod tests { &self.registered_users } - pub(crate) fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) { - self.add_update_user(user_id).unwrap(); - let mut registered_users = self.registered_users.lock().unwrap(); + pub(crate) async fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) { + self.add_update_user(user_id).await.unwrap(); + let mut registered_users = self.registered_users.lock().await; let user = registered_users.get_mut(&user_id).unwrap(); user.subscription_expiry = outdates_at - self.expiry_delta; } } - fn init_gatekeeper(chain: &Blockchain) -> Gatekeeper { - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - Gatekeeper::new(chain.get_block_count(), SLOTS, DURATION, EXPIRY_DELTA, dbm) + async fn init_gatekeeper(chain: &Blockchain) -> Gatekeeper { + let dbm = Arc::new(DBM::test_db().await); + Gatekeeper::new(chain.get_block_count(), SLOTS, DURATION, EXPIRY_DELTA, dbm).await } #[tokio::test] async fn test_new() { // A fresh gatekeeper has no associated data let chain = Blockchain::default().with_height(START_HEIGHT); - let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); + let dbm = Arc::new(DBM::test_db().await); let gatekeeper = Gatekeeper::new( chain.get_block_count(), @@ -379,18 +380,20 @@ mod tests { DURATION, EXPIRY_DELTA, dbm.clone(), - ); - assert!(gatekeeper.is_fresh()); + ) + .await; + assert!(gatekeeper.is_fresh().await); // If we add some users and appointments to the system and create a new Gatekeeper reusing the same db // (as if simulating a bootstrap from existing data), the data should be properly loaded. for _ in 0..10 { let user_id = get_random_user_id(); - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Add the appointment to the database. This is normally done by the Watcher. gatekeeper @@ -402,14 +405,14 @@ mod tests { // Create a new GK reusing the same DB and check that the data is loaded let another_gk = - Gatekeeper::new(chain.get_block_count(), SLOTS, DURATION, EXPIRY_DELTA, dbm); - assert!(!another_gk.is_fresh()); + Gatekeeper::new(chain.get_block_count(), SLOTS, DURATION, EXPIRY_DELTA, dbm).await; + assert!(!another_gk.is_fresh().await); assert_eq!(gatekeeper, another_gk); } #[tokio::test] async fn test_authenticate_user() { - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)).await; // Authenticate user returns the UserId if the user is found in the system, or an AuthenticationError otherwise. @@ -417,7 +420,7 @@ mod tests { let message = "message".as_bytes(); let wrong_signature = "signature"; assert_eq!( - gatekeeper.authenticate_user(message, wrong_signature), + gatekeeper.authenticate_user(message, wrong_signature).await, Err(AuthenticationFailure("Wrong message or signature.")) ); @@ -425,15 +428,15 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let signature = cryptography::sign(message, &user_sk).unwrap(); assert_eq!( - gatekeeper.authenticate_user(message, &signature), + gatekeeper.authenticate_user(message, &signature).await, Err(AuthenticationFailure("User not found.")) ); // Last, let's add the user to the Gatekeeper and try again. let user_id = UserId(user_pk); - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); assert_eq!( - gatekeeper.authenticate_user(message, &signature), + gatekeeper.authenticate_user(message, &signature).await, Ok(user_id) ); } @@ -441,14 +444,14 @@ mod tests { #[tokio::test] async fn test_add_update_user() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let gatekeeper = init_gatekeeper(&chain); + let gatekeeper = init_gatekeeper(&chain).await; // add_update_user adds a user to the system if it is not still registered, otherwise it add slots to the user subscription // and refreshes the subscription expiry. Slots are added up to u32:MAX, further call will return an MaxSlotsReached error. // Let's start by adding new user let user_id = get_random_user_id(); - let receipt = gatekeeper.add_update_user(user_id).unwrap(); + let receipt = gatekeeper.add_update_user(user_id).await.unwrap(); // The data should have been also added to the database assert_eq!( gatekeeper.dbm.load_user(user_id).await.unwrap(), @@ -464,7 +467,7 @@ mod tests { gatekeeper .last_known_block_height .store(chain.get_block_count(), Ordering::Relaxed); - let updated_receipt = gatekeeper.add_update_user(user_id).unwrap(); + let updated_receipt = gatekeeper.add_update_user(user_id).await.unwrap(); assert_eq!(updated_receipt.available_slots(), SLOTS * 2); assert_eq!( @@ -486,13 +489,13 @@ mod tests { gatekeeper .registered_users .lock() - .unwrap() + .await .get_mut(&user_id) .unwrap() .available_slots = u32::MAX; assert!(matches!( - gatekeeper.add_update_user(user_id), + gatekeeper.add_update_user(user_id).await, Err(MaxSlotsReached) )); @@ -509,26 +512,27 @@ mod tests { #[tokio::test] async fn test_add_update_appointment() { - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)).await; // if a given appointment is not associated with a given user, add_update_appointment adds the appointment user appointments alongside the number os slots it consumes. If the appointment // is already associated with the user, it will update it (both data and slot count). // Let's first add the a user to the Gatekeeper (inputs are always sanitized here, so we don't need tests for non-registered users) let user_id = get_random_user_id(); - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); // Now let's add a new appointment let slots_before = gatekeeper .registered_users .lock() - .unwrap() + .await .get(&user_id) .unwrap() .available_slots; let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); let available_slots = gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Simulate the watcher adding the appointment in the database. gatekeeper @@ -537,7 +541,7 @@ mod tests { .await .unwrap(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + let (_, user_locators) = gatekeeper.get_user_info(user_id).await.unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(slots_before, available_slots + 1); @@ -549,9 +553,10 @@ mod tests { // We don't really need to update the appointment in the DB since it's the very same appointment. let mut updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + let (_, user_locators) = gatekeeper.get_user_info(user_id).await.unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); @@ -563,6 +568,7 @@ mod tests { bigger_appointment.inner.encrypted_blob = get_random_bytes(ENCRYPTED_BLOB_MAX_SIZE + 1); updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &bigger_appointment) + .await .unwrap(); // Simulate the watcher updating the appointment in the database. gatekeeper @@ -571,7 +577,7 @@ mod tests { .await .unwrap(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + let (_, user_locators) = gatekeeper.get_user_info(user_id).await.unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); @@ -581,6 +587,7 @@ mod tests { // Adding back a smaller update (modulo ENCRYPTED_BLOB_MAX_SIZE) should reduce the count updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Simulate the watcher updating the appointment in the database. gatekeeper @@ -589,7 +596,7 @@ mod tests { .await .unwrap(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + let (_, user_locators) = gatekeeper.get_user_info(user_id).await.unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots); @@ -600,6 +607,7 @@ mod tests { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Simulate the watcher adding the appointment in the database. gatekeeper @@ -608,7 +616,7 @@ mod tests { .await .unwrap(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); + let (_, user_locators) = gatekeeper.get_user_info(user_id).await.unwrap(); assert!(user_locators.contains(&appointment.locator())); assert_eq!(updated_slot_count, available_slots - 1); @@ -620,12 +628,14 @@ mod tests { gatekeeper .registered_users .lock() - .unwrap() + .await .get_mut(&user_id) .unwrap() .available_slots = 0; assert!(matches!( - gatekeeper.add_update_appointment(user_id, uuid, &appointment), + gatekeeper + .add_update_appointment(user_id, uuid, &appointment) + .await, Err(NotEnoughSlots) )); @@ -636,19 +646,19 @@ mod tests { #[tokio::test] async fn test_has_subscription_expired() { - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)).await; // If the user is not registered, querying for a subscription expiry check should return an error let user_id = get_random_user_id(); assert!(matches!( - gatekeeper.has_subscription_expired(user_id), + gatekeeper.has_subscription_expired(user_id).await, Err(AuthenticationFailure { .. }) )); // If the user is registered and the subscription is active we should get (false, expiry) - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); assert_eq!( - gatekeeper.has_subscription_expired(user_id), + gatekeeper.has_subscription_expired(user_id).await, Ok((false, DURATION + START_HEIGHT as u32)) ); @@ -657,12 +667,12 @@ mod tests { gatekeeper .registered_users .lock() - .unwrap() + .await .get_mut(&user_id) .unwrap() .subscription_expiry = expiry; assert_eq!( - gatekeeper.has_subscription_expired(user_id), + gatekeeper.has_subscription_expired(user_id).await, Ok((true, expiry)) ); } @@ -670,28 +680,32 @@ mod tests { #[tokio::test] async fn test_get_outdated_users() { let start_height = START_HEIGHT as u32 + EXPIRY_DELTA; - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(start_height as usize)); + let gatekeeper = + init_gatekeeper(&Blockchain::default().with_height(start_height as usize)).await; // Initially, there are not outdated users, so querying any block height should return an empty map for i in 0..start_height { - assert_eq!(gatekeeper.get_outdated_users(i), vec![]); + assert_eq!(gatekeeper.get_outdated_users(i).await, vec![]); } // Adding a user whose subscription is outdated should return an entry let user_id = get_random_user_id(); - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); // Check that data is not yet outdated - assert_eq!(gatekeeper.get_outdated_users(start_height), vec![]); + assert_eq!(gatekeeper.get_outdated_users(start_height).await, vec![]); // Add an outdated user and check again - gatekeeper.add_outdated_user(user_id, start_height); - assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]); + gatekeeper.add_outdated_user(user_id, start_height).await; + assert_eq!( + gatekeeper.get_outdated_users(start_height).await, + vec![user_id] + ); } #[tokio::test] async fn test_delete_appointments_without_refund() { - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)).await; let n_users = 100; let n_apps = 10; let mut uuids_to_delete = Vec::new(); @@ -701,11 +715,12 @@ mod tests { for _ in 0..n_users { let user_id = get_random_user_id(); - gatekeeper.add_update_user(user_id).unwrap(); + gatekeeper.add_update_user(user_id).await.unwrap(); for i in 0..n_apps { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Add the appointment to the database. This is normally done by the Watcher. gatekeeper @@ -731,11 +746,13 @@ mod tests { trackers.push(uuid); } } - users_info.insert(user_id, gatekeeper.get_user_info(user_id).unwrap().0); + users_info.insert(user_id, gatekeeper.get_user_info(user_id).await.unwrap().0); } // Delete these appointments without refunding their owners. - gatekeeper.delete_appointments(uuids_to_delete.clone(), false); + gatekeeper + .delete_appointments(uuids_to_delete.clone(), false) + .await; for uuid in uuids_to_delete.clone() { assert!(!gatekeeper.dbm.appointment_exists(uuid).await); @@ -754,14 +771,14 @@ mod tests { for (user_id, user_info_before_deletion) in users_info { // Since `refund` was false, the users' slots should not have changed after deleting appointments. - let (user_info_after_deletion, _) = gatekeeper.get_user_info(user_id).unwrap(); + let (user_info_after_deletion, _) = gatekeeper.get_user_info(user_id).await.unwrap(); assert_eq!(user_info_after_deletion, user_info_before_deletion); } } #[tokio::test] async fn test_delete_appointments_with_refund() { - let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)).await; let n_users = 100; let n_apps = 10; let mut uuids_to_delete = Vec::new(); @@ -771,13 +788,18 @@ mod tests { for _ in 0..n_users { let user_id = get_random_user_id(); - gatekeeper.add_update_user(user_id).unwrap(); - let mut user_remaining_slots = - gatekeeper.get_user_info(user_id).unwrap().0.available_slots; + gatekeeper.add_update_user(user_id).await.unwrap(); + let mut user_remaining_slots = gatekeeper + .get_user_info(user_id) + .await + .unwrap() + .0 + .available_slots; for i in 0..n_apps { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); // Add the appointment to the database. This is normally done by the Watcher. gatekeeper @@ -813,7 +835,9 @@ mod tests { } // Delete these appointments and refund their owners their slots back. - gatekeeper.delete_appointments(uuids_to_delete.clone(), true); + gatekeeper + .delete_appointments(uuids_to_delete.clone(), true) + .await; for uuid in uuids_to_delete.clone() { assert!(!gatekeeper.dbm.appointment_exists(uuid).await); @@ -831,11 +855,15 @@ mod tests { } for (user_id, correct_remaining_slots) in users_remaining_slots { - let remaining_slots_from_db = - gatekeeper.get_user_info(user_id).unwrap().0.available_slots; + let remaining_slots_from_db = gatekeeper + .get_user_info(user_id) + .await + .unwrap() + .0 + .available_slots; assert_eq!(remaining_slots_from_db, correct_remaining_slots); assert_eq!( - gatekeeper.registered_users.lock().unwrap()[&user_id].available_slots, + gatekeeper.registered_users.lock().await[&user_id].available_slots, correct_remaining_slots ); } @@ -846,7 +874,7 @@ mod tests { // block_connected in the Gatekeeper is used to keep track of time in order to manage the users' subscription expiry. // Remove users that get outdated at the new block's height from registered_users and the database. let mut chain = Blockchain::default().with_height(START_HEIGHT); - let gatekeeper = init_gatekeeper(&chain); + let gatekeeper = init_gatekeeper(&chain).await; // Check that users are outdated when the expected height if hit let user1_id = get_random_user_id(); @@ -854,7 +882,9 @@ mod tests { let user3_id = get_random_user_id(); for user_id in &[user1_id, user2_id, user3_id] { - gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1) + gatekeeper + .add_outdated_user(*user_id, chain.tip().height + 1) + .await } // Connect a new block. Outdated users are deleted @@ -867,7 +897,7 @@ mod tests { assert!(!gatekeeper .registered_users .lock() - .unwrap() + .await .contains_key(user_id)); assert!(gatekeeper.dbm.load_user(*user_id).await.is_none()); } @@ -883,7 +913,7 @@ mod tests { async fn test_block_disconnected() { // Block disconnected simply updates the last known block let chain = Blockchain::default().with_height(START_HEIGHT); - let gatekeeper = init_gatekeeper(&chain); + let gatekeeper = init_gatekeeper(&chain).await; let height = chain.get_block_count(); let last_known_block_header = chain.tip(); diff --git a/teos/src/responder.rs b/teos/src/responder.rs index dd3db4b6..a6d52379 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -521,27 +521,24 @@ mod tests { impl PartialEq for Responder { fn eq(&self, other: &Self) -> bool { // Same in-memory data. - *self.reorged_trackers.lock().unwrap() == *other.reorged_trackers.lock().unwrap() && - *self.tx_index.lock().unwrap() == *other.tx_index.lock().unwrap() && - // && Same DB data. - self.get_trackers() == other.get_trackers() + *self.reorged_trackers.lock().unwrap() == *other.reorged_trackers.lock().unwrap() + && *self.tx_index.lock().unwrap() == *other.tx_index.lock().unwrap() } } impl Eq for Responder {} impl Responder { - pub(crate) async fn get_trackers(&self) -> HashMap { - self.dbm.load_trackers(None).await - } - pub(crate) fn get_carrier(&self) -> &Mutex { &self.carrier } - pub(crate) fn add_random_tracker(&self, status: ConfirmationStatus) -> TransactionTracker { + pub(crate) async fn add_random_tracker( + &self, + status: ConfirmationStatus, + ) -> TransactionTracker { let user_id = get_random_user_id(); let tracker = get_random_tracker(user_id, status); - self.add_dummy_tracker(&tracker); + self.add_dummy_tracker(&tracker).await; tracker } @@ -558,7 +555,7 @@ mod tests { .unwrap(); } - fn store_dummy_appointment_to_db(&self) -> (UserId, UUID) { + async fn store_dummy_appointment_to_db(&self) -> (UserId, UUID) { let appointment = generate_dummy_appointment(None); let (uuid, user_id) = (appointment.uuid(), appointment.user_id); // Store the appointment and the user to the DB. @@ -599,7 +596,8 @@ mod tests { DURATION, EXPIRY_DELTA, dbm.clone(), - ); + ) + .await; create_responder(chain, Arc::new(gk), dbm, mocked_query).await } @@ -651,25 +649,27 @@ mod tests { let (responder, _s) = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm.clone()) .await; - assert!(responder.is_fresh()); + assert!(responder.is_fresh().await); // If we add some trackers to the system and create a new Responder reusing the same db // (as if simulating a bootstrap from existing data), the data should be properly loaded. for i in 0..10 { - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); let s = if i % 2 == 0 { ConfirmationStatus::InMempoolSince(i) } else { ConfirmationStatus::ConfirmedIn(i) }; - responder.add_tracker(uuid, breach.clone(), user_id, s); + responder + .add_tracker(uuid, breach.clone(), user_id, s) + .await; } // Create a new Responder reusing the same DB and check that the data is loaded let (another_r, _) = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; - assert!(!responder.is_fresh()); + assert!(!responder.is_fresh().await); assert_eq!(responder, another_r); } @@ -678,11 +678,11 @@ mod tests { let start_height = START_HEIGHT as u32; let (responder, _s) = init_responder(MockedServerQuery::Regular).await; - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); assert_eq!( - responder.handle_breach(uuid, breach, user_id), + responder.handle_breach(uuid, breach, user_id).await, ConfirmationStatus::InMempoolSince(start_height) ); let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); @@ -695,7 +695,7 @@ mod tests { // passed twice, the receipt corresponding to the first breach will be handed back. let another_breach = get_random_breach(); assert_eq!( - responder.handle_breach(uuid, another_breach, user_id), + responder.handle_breach(uuid, another_breach, user_id).await, ConfirmationStatus::InMempoolSince(start_height) ); // Getting the tracker should return the old one. @@ -707,11 +707,11 @@ mod tests { let start_height = START_HEIGHT as u32; let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); assert_eq!( - responder.handle_breach(uuid, breach, user_id), + responder.handle_breach(uuid, breach, user_id).await, ConfirmationStatus::InMempoolSince(start_height) ); let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); @@ -725,7 +725,7 @@ mod tests { async fn test_handle_breach_accepted_in_txindex() { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); let penalty_txid = breach.penalty_tx.txid(); @@ -746,7 +746,7 @@ mod tests { .unwrap() as u32; assert_eq!( - responder.handle_breach(uuid, breach, user_id), + responder.handle_breach(uuid, breach, user_id).await, ConfirmationStatus::ConfirmedIn(target_height) ); let tracker = responder.dbm.load_tracker(uuid).await.unwrap(); @@ -768,10 +768,10 @@ mod tests { let breach = get_random_breach(); assert_eq!( - responder.handle_breach(uuid, breach, user_id), + responder.handle_breach(uuid, breach, user_id).await, ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_ERROR) ); - assert!(!responder.has_tracker(uuid)); + assert!(!responder.has_tracker(uuid).await); } #[tokio::test] @@ -779,14 +779,16 @@ mod tests { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; let start_height = START_HEIGHT as u32; - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let mut breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(start_height), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(start_height), + ) + .await; // Check that the data has been added to the responder. assert_eq!( @@ -800,14 +802,16 @@ mod tests { // Adding a confirmed tracker should result in the same but with the height being set. - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(start_height - 1), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn(start_height - 1), + ) + .await; assert_eq!( responder.dbm.load_tracker(uuid).await.unwrap(), @@ -819,13 +823,15 @@ mod tests { ); // Adding another breach with the same penalty transaction (but different uuid) - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(start_height), - ); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn(start_height), + ) + .await; assert_eq!( responder.dbm.load_tracker(uuid).await.unwrap(), @@ -845,20 +851,25 @@ mod tests { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Add a new tracker - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::ConfirmedIn(START_HEIGHT as u32), - ); + responder + .add_tracker( + uuid, + breach, + user_id, + ConfirmationStatus::ConfirmedIn(START_HEIGHT as u32), + ) + .await; - assert!(responder.has_tracker(uuid)); + assert!(responder.has_tracker(uuid).await); // Delete the tracker and check again. - responder.gatekeeper.delete_appointments(vec![uuid], false); - assert!(!responder.has_tracker(uuid)); + responder + .gatekeeper + .delete_appointments(vec![uuid], false) + .await; + assert!(!responder.has_tracker(uuid).await); } #[tokio::test] @@ -868,19 +879,21 @@ mod tests { let (responder, _s) = init_responder(MockedServerQuery::Regular).await; // Store the user and the appointment in the database so we can add the tracker later on (due to FK restrictions) - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; // Data should not be there before adding it assert!(responder.dbm.load_tracker(uuid).await.is_none()); // Data should be there now let breach = get_random_breach(); - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(start_height), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(start_height), + ) + .await; assert_eq!( responder.dbm.load_tracker(uuid).await.unwrap(), TransactionTracker::new( @@ -891,7 +904,10 @@ mod tests { ); // After deleting the data it should be gone - responder.gatekeeper.delete_appointments(vec![uuid], false); + responder + .gatekeeper + .delete_appointments(vec![uuid], false) + .await; assert!(responder.dbm.load_tracker(uuid).await.is_none()); } @@ -908,47 +924,55 @@ mod tests { let mut txids = HashSet::new(); for i in 0..40 { - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let (user_id, uuid) = responder.store_dummy_appointment_to_db().await; let breach = get_random_breach(); match i % 4 { 0 => { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(21), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(21), + ) + .await; in_mempool.insert(uuid); } 1 => { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::InMempoolSince(i), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::InMempoolSince(i), + ) + .await; just_confirmed.insert(uuid); txids.insert(breach.penalty_tx.txid()); } 2 => { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn(42), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn(42), + ) + .await; confirmed.insert(uuid); } _ => { - responder.add_tracker( - uuid, - breach.clone(), - user_id, - ConfirmationStatus::ConfirmedIn( - target_height - constants::IRREVOCABLY_RESOLVED, - ), - ); + responder + .add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn( + target_height - constants::IRREVOCABLY_RESOLVED, + ), + ) + .await; completed.insert(uuid); } } @@ -957,7 +981,12 @@ mod tests { // The trackers that were completed should be returned assert_eq!( completed, - HashSet::from_iter(responder.check_confirmations(txids, target_height).unwrap()) + HashSet::from_iter( + responder + .check_confirmations(txids, target_height) + .await + .unwrap() + ) ); // The ones in mempool should still be there (at the same height) @@ -993,13 +1022,14 @@ mod tests { for _ in 0..10 { let uuid = responder .add_random_tracker(ConfirmationStatus::ConfirmedIn(42)) + .await .uuid(); responder.reorged_trackers.lock().unwrap().insert(uuid); trackers.push(uuid); } let height = 100; - assert!(responder.handle_reorged_txs(height).is_none()); + assert!(responder.handle_reorged_txs(height).await.is_none()); // The reorged trackers buffer should be empty after this. assert!(responder.reorged_trackers.lock().unwrap().is_empty()); @@ -1024,13 +1054,14 @@ mod tests { for _ in 0..n_trackers { let uuid = responder .add_random_tracker(ConfirmationStatus::ConfirmedIn(42)) + .await .uuid(); responder.reorged_trackers.lock().unwrap().insert(uuid); trackers.insert(uuid); } let height = 100; - let rejected = HashSet::from_iter(responder.handle_reorged_txs(height).unwrap()); + let rejected = HashSet::from_iter(responder.handle_reorged_txs(height).await.unwrap()); // All the trackers should be returned as rejected. assert_eq!(trackers, rejected); // The reorged trackers buffer should be empty after this. @@ -1058,12 +1089,12 @@ mod tests { ConfirmationStatus::InMempoolSince(i) }; - let uuid = responder.add_random_tracker(status).uuid(); + let uuid = responder.add_random_tracker(status).await.uuid(); statues.insert(uuid, status); } // There should be no rejected tx. - assert!(responder.rebroadcast_stale_txs(height).is_none()); + assert!(responder.rebroadcast_stale_txs(height).await.is_none()); for (uuid, former_status) in statues { let status = responder.dbm.load_tracker(uuid).await.unwrap().status; @@ -1098,13 +1129,13 @@ mod tests { ConfirmationStatus::InMempoolSince(i) }; - let uuid = responder.add_random_tracker(status).uuid(); + let uuid = responder.add_random_tracker(status).await.uuid(); statues.insert(uuid, status); } // `rebroadcast_stale_txs` will broadcast txs which has been in mempool since `CONFIRMATIONS_BEFORE_RETRY` or more // blocks. Since our backend rejects all the txs, all these broadcasted txs should be returned from this method (rejected). - let rejected = HashSet::from_iter(responder.rebroadcast_stale_txs(height).unwrap()); + let rejected = HashSet::from_iter(responder.rebroadcast_stale_txs(height).await.unwrap()); let should_reject: HashSet<_> = statues .iter() .filter_map(|(&uuid, &status)| { @@ -1152,7 +1183,7 @@ mod tests { let mut users = Vec::new(); for _ in 0..21 { let user_id = get_random_user_id(); - responder.gatekeeper.add_update_user(user_id).unwrap(); + responder.gatekeeper.add_update_user(user_id).await.unwrap(); users.push(user_id); } @@ -1168,6 +1199,7 @@ mod tests { responder .gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); responder .dbm @@ -1180,7 +1212,9 @@ mod tests { let status = ConfirmationStatus::ConfirmedIn( target_block_height - constants::IRREVOCABLY_RESOLVED, ); - responder.add_tracker(uuid, breach.clone(), user_id, status); + responder + .add_tracker(uuid, breach.clone(), user_id, status) + .await; completed_trackers.push(TransactionTracker::new(breach, user_id, status)); } @@ -1194,6 +1228,7 @@ mod tests { responder .gatekeeper .add_update_appointment(user_id, uuid, &appointment) + .await .unwrap(); responder .dbm @@ -1203,14 +1238,17 @@ mod tests { let breach = Breach::new(dispute_tx, get_random_tx()); let status = ConfirmationStatus::InMempoolSince(target_block_height - 1); - responder.add_tracker(uuid, breach.clone(), user_id, status); + responder + .add_tracker(uuid, breach.clone(), user_id, status) + .await; outdated_trackers.push(TransactionTracker::new(breach, user_id, status)); } // Outdate this user so their trackers are deleted responder .gatekeeper - .add_outdated_user(user_id, target_block_height); + .add_outdated_user(user_id, target_block_height) + .await; } // CONFIRMATIONS SETUP @@ -1218,6 +1256,7 @@ mod tests { responder .gatekeeper .add_update_user(standalone_user_id) + .await .unwrap(); let mut missed_confirmation_trackers = Vec::new(); @@ -1229,6 +1268,7 @@ mod tests { responder .gatekeeper .add_update_appointment(standalone_user_id, uuid, &appointment) + .await .unwrap(); responder .dbm @@ -1239,7 +1279,9 @@ mod tests { let breach = Breach::new(dispute_tx, get_random_tx()); let status = ConfirmationStatus::InMempoolSince(target_block_height - 1); - responder.add_tracker(uuid, breach.clone(), standalone_user_id, status); + responder + .add_tracker(uuid, breach.clone(), standalone_user_id, status) + .await; if i % 2 == 0 { just_confirmed_trackers.push(TransactionTracker::new( breach, @@ -1264,6 +1306,7 @@ mod tests { responder .gatekeeper .add_update_appointment(standalone_user_id, uuid, &appointment) + .await .unwrap(); responder .dbm @@ -1275,7 +1318,9 @@ mod tests { let status = ConfirmationStatus::InMempoolSince( target_block_height - CONFIRMATIONS_BEFORE_RETRY as u32, ); - responder.add_tracker(uuid, breach.clone(), standalone_user_id, status); + responder + .add_tracker(uuid, breach.clone(), standalone_user_id, status) + .await; trackers_to_rebroadcast.push(TransactionTracker::new( breach, standalone_user_id, @@ -1322,7 +1367,11 @@ mod tests { // Data should have been removed for tracker in completed_trackers { assert!(responder.dbm.load_tracker(tracker.uuid()).await.is_none()); - let (_, user_locators) = responder.gatekeeper.get_user_info(tracker.user_id).unwrap(); + let (_, user_locators) = responder + .gatekeeper + .get_user_info(tracker.user_id) + .await + .unwrap(); assert!(!user_locators.contains(&tracker.locator())); } @@ -1333,6 +1382,7 @@ mod tests { assert!(responder .gatekeeper .get_user_info(tracker.user_id) + .await .is_none()); } @@ -1387,7 +1437,7 @@ mod tests { // Add user to the database let user_id = get_random_user_id(); - responder.gatekeeper.add_update_user(user_id).unwrap(); + responder.gatekeeper.add_update_user(user_id).await.unwrap(); let mut reorged = Vec::new(); let block_range = START_HEIGHT - 10..START_HEIGHT; @@ -1404,12 +1454,14 @@ mod tests { .unwrap(); let breach = Breach::new(dispute_tx, get_random_tx()); - responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::ConfirmedIn(i as u32), - ); + responder + .add_tracker( + uuid, + breach, + user_id, + ConfirmationStatus::ConfirmedIn(i as u32), + ) + .await; reorged.push(uuid); } diff --git a/teos/src/test_utils.rs b/teos/src/test_utils.rs index dfea978c..c2bb86cd 100644 --- a/teos/src/test_utils.rs +++ b/teos/src/test_utils.rs @@ -474,13 +474,16 @@ pub(crate) async fn create_api_with_config( let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(DBM::test_db().await); - let gk = Arc::new(Gatekeeper::new( - chain.get_block_count(), - api_config.slots, - api_config.duration, - EXPIRY_DELTA, - dbm.clone(), - )); + let gk = Arc::new( + Gatekeeper::new( + chain.get_block_count(), + api_config.slots, + api_config.duration, + EXPIRY_DELTA, + dbm.clone(), + ) + .await, + ); let responder = create_responder(&mut chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await; let (watcher, stopper) = create_watcher( diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index 3bc73d49..512488cc 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -560,7 +560,7 @@ mod tests { use std::collections::HashSet; use std::iter::FromIterator; use std::ops::Deref; - use std::sync::{Arc, Mutex}; + use std::sync::Arc; use crate::dbm::DBM; use crate::responder::ConfirmationStatus; @@ -577,23 +577,23 @@ mod tests { impl PartialEq for Watcher { fn eq(&self, other: &Self) -> bool { // Same in-memory data. - self.last_known_block_height.load(Ordering::Relaxed) == other.last_known_block_height.load(Ordering::Relaxed) && - *self.locator_cache.lock().unwrap() == *other.locator_cache.lock().unwrap() && - // && Same DB data. - self.get_all_watcher_appointments() == other.get_all_watcher_appointments() + self.last_known_block_height.load(Ordering::Relaxed) + == other.last_known_block_height.load(Ordering::Relaxed) + && *self.locator_cache.lock().unwrap() == *other.locator_cache.lock().unwrap() } } impl Eq for Watcher {} impl Watcher { - pub(crate) fn add_dummy_tracker_to_responder(&self, tracker: &TransactionTracker) { - self.responder.add_dummy_tracker(tracker) + pub(crate) async fn add_dummy_tracker_to_responder(&self, tracker: &TransactionTracker) { + self.responder.add_dummy_tracker(tracker).await } - pub(crate) fn add_random_tracker_to_responder(&self) -> TransactionTracker { + pub(crate) async fn add_random_tracker_to_responder(&self) -> TransactionTracker { // The confirmation status can be whatever here. Using the most common. self.responder .add_random_tracker(ConfirmationStatus::ConfirmedIn(100)) + .await } } @@ -608,13 +608,16 @@ mod tests { ) -> (Watcher, BitcoindStopper) { let bitcoind_mock = BitcoindMock::new(MockOptions::default()); - let gk = Arc::new(Gatekeeper::new( - chain.get_block_count(), - SLOTS, - DURATION, - EXPIRY_DELTA, - dbm.clone(), - )); + let gk = Arc::new( + Gatekeeper::new( + chain.get_block_count(), + SLOTS, + DURATION, + EXPIRY_DELTA, + dbm.clone(), + ) + .await, + ); let responder = create_responder(chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await; create_watcher( chain, @@ -649,11 +652,11 @@ mod tests { let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(DBM::test_db().await); let (watcher, _s) = init_watcher_with_db(&mut chain, dbm.clone()).await; - assert!(watcher.is_fresh()); + assert!(watcher.is_fresh().await); let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); // If we add some appointments to the system and create a new Watcher reusing the same db // (as if simulating a bootstrap from existing data), the data should be properly loaded. @@ -662,12 +665,13 @@ mod tests { let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); watcher .add_appointment(appointment.clone(), user_sig.clone()) + .await .unwrap(); } // Create a new watcher reusing the same DB and check that the data is loaded let (another_w, _as) = init_watcher_with_db(&mut chain, dbm).await; - assert!(!another_w.is_fresh()); + assert!(!another_w.is_fresh().await); assert_eq!(watcher, another_w); } @@ -682,7 +686,7 @@ mod tests { let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - let receipt = watcher.register(user_id).unwrap(); + let receipt = watcher.register(user_id).await.unwrap(); assert_eq!(receipt.user_id(), user_id); assert_eq!(receipt.available_slots(), SLOTS); @@ -719,7 +723,7 @@ mod tests { )); let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let appointment = generate_dummy_appointment(None).inner; let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); @@ -727,6 +731,7 @@ mod tests { for _ in 0..2 { let (receipt, slots, expiry) = watcher .add_appointment(appointment.clone(), user_sig.clone()) + .await .unwrap(); assert_appointment_added(slots, SLOTS - 1, expiry, receipt, &user_sig, tower_id); @@ -735,18 +740,19 @@ mod tests { // Add the same appointment but for another user let (user2_sk, user2_pk) = get_random_keypair(); let user2_id = UserId(user2_pk); - watcher.register(user2_id).unwrap(); + watcher.register(user2_id).await.unwrap(); let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(appointment.clone(), user2_sig.clone()) + .await .unwrap(); assert_appointment_added(slots, SLOTS - 1, expiry, receipt, &user2_sig, tower_id); // There should be now two appointments in the Watcher - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 0); + assert_eq!(watcher.get_appointments_count().await, 2); + assert_eq!(watcher.responder.get_trackers_count().await, 0); // If an appointment is already in the Responder, it should bounce let dispute_tx = get_random_tx(); @@ -756,27 +762,33 @@ mod tests { cryptography::sign(&triggered_appointment.inner.to_vec(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(triggered_appointment.inner.clone(), signature.clone()) + .await .unwrap(); assert_appointment_added(slots, SLOTS - 2, expiry, receipt, &signature, tower_id); - assert_eq!(watcher.get_appointments_count(), 3); - assert_eq!(watcher.responder.get_trackers_count(), 0); + assert_eq!(watcher.get_appointments_count().await, 3); + assert_eq!(watcher.responder.get_trackers_count().await, 0); let breach = Breach::new(dispute_tx, get_random_tx()); - watcher.responder.add_tracker( - uuid, - breach, - user_id, - ConfirmationStatus::InMempoolSince(chain.get_block_count()), - ); - let receipt = watcher.add_appointment(triggered_appointment.inner, signature); + watcher + .responder + .add_tracker( + uuid, + breach, + user_id, + ConfirmationStatus::InMempoolSince(chain.get_block_count()), + ) + .await; + let receipt = watcher + .add_appointment(triggered_appointment.inner, signature) + .await; assert!(matches!( receipt, Err(AddAppointmentFailure::AlreadyTriggered) )); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 1); + assert_eq!(watcher.get_appointments_count().await, 2); + assert_eq!(watcher.responder.get_trackers_count().await, 1); // If the trigger is already in the cache, the appointment will go straight to the Responder let dispute_tx = tip_txs.last().unwrap(); @@ -785,14 +797,15 @@ mod tests { let user_sig = cryptography::sign(&appointment_in_cache.inner.to_vec(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(appointment_in_cache.inner, user_sig.clone()) + .await .unwrap(); // The appointment should have been accepted, slots should have been decreased, and a new tracker should be found in the Responder assert_appointment_added(slots, SLOTS - 3, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 2); + assert_eq!(watcher.get_appointments_count().await, 2); + assert_eq!(watcher.responder.get_trackers_count().await, 2); // Data should be in the database - assert!(watcher.responder.has_tracker(uuid)); + assert!(watcher.responder.has_tracker(uuid).await); // If an appointment is rejected by the Responder, it is considered misbehavior and the slot count is kept // Wrong penalty @@ -803,13 +816,14 @@ mod tests { let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(invalid_appointment.inner, user_sig.clone()) + .await .unwrap(); assert_appointment_added(slots, SLOTS - 4, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 2); + assert_eq!(watcher.get_appointments_count().await, 2); + assert_eq!(watcher.responder.get_trackers_count().await, 2); // Data should not be in the database - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); // Transaction rejected @@ -826,13 +840,14 @@ mod tests { let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(invalid_appointment.inner, user_sig.clone()) + .await .unwrap(); assert_appointment_added(slots, SLOTS - 5, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 2); + assert_eq!(watcher.get_appointments_count().await, 2); + assert_eq!(watcher.responder.get_trackers_count().await, 2); // Data should not be in the database - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); // FAIL cases (non-registered, subscription expired and not enough slots) @@ -842,7 +857,7 @@ mod tests { let user3_sig = String::from_utf8((0..65).collect()).unwrap(); assert!(matches!( - watcher.add_appointment(appointment, user3_sig), + watcher.add_appointment(appointment, user3_sig).await, Err(AddAppointmentFailure::AuthenticationFailure) )); // Data should not be in the database @@ -854,7 +869,7 @@ mod tests { .gatekeeper .get_registered_users() .lock() - .unwrap() + .await .get_mut(&user_id) .unwrap() .available_slots = 0; @@ -863,7 +878,7 @@ mod tests { let signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk).unwrap(); assert!(matches!( - watcher.add_appointment(appointment.inner, signature), + watcher.add_appointment(appointment.inner, signature).await, Err(AddAppointmentFailure::NotEnoughSlots) )); // Data should not be in the database @@ -872,13 +887,14 @@ mod tests { // If the user subscription has expired, the appointment should be rejected. watcher .gatekeeper - .add_outdated_user(user2_id, START_HEIGHT as u32); + .add_outdated_user(user2_id, START_HEIGHT as u32) + .await; let (uuid, appointment) = generate_dummy_appointment_with_user(user2_id, None); let signature = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); assert!(matches!( - watcher.add_appointment(appointment.inner, signature), + watcher.add_appointment(appointment.inner, signature).await, Err(AddAppointmentFailure::SubscriptionExpired { .. }) )); // Data should not be in the database @@ -893,7 +909,7 @@ mod tests { // Register the user let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let dispute_txid = get_random_tx().txid(); let (uuid, appointment) = @@ -901,11 +917,11 @@ mod tests { // Storing a new appointment should return New assert_eq!( - watcher.store_appointment(uuid, &appointment), + watcher.store_appointment(uuid, &appointment).await, StoredAppointment::New, ); assert_eq!( - watcher.get_all_watcher_appointments(), + watcher.get_all_watcher_appointments().await, HashMap::from_iter([(uuid, appointment)]) ); @@ -915,11 +931,11 @@ mod tests { generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); assert_eq!(new_uuid, uuid); assert_eq!( - watcher.store_appointment(uuid, &appointment), + watcher.store_appointment(uuid, &appointment).await, StoredAppointment::Update, ); assert_eq!( - watcher.get_all_watcher_appointments(), + watcher.get_all_watcher_appointments().await, HashMap::from_iter([(uuid, appointment)]) ); } @@ -932,7 +948,7 @@ mod tests { // Register the user let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let dispute_tx = get_random_tx(); let (uuid, appointment) = @@ -940,11 +956,13 @@ mod tests { // Valid triggered appointments should be accepted by the Responder assert_eq!( - watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), + watcher + .store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx) + .await, TriggeredAppointment::Accepted, ); // In this case the appointment is kept in the Responder and, therefore, in the database - assert!(watcher.responder.has_tracker(uuid)); + assert!(watcher.responder.has_tracker(uuid).await); assert!(watcher.dbm.appointment_exists(uuid).await); // A properly formatted but invalid transaction should be rejected by the Responder @@ -958,11 +976,13 @@ mod tests { let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); assert_eq!( - watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), + watcher + .store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx) + .await, TriggeredAppointment::Rejected, ); // In this case the appointment is not kept in the Responder nor in the database - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); // Invalid triggered appointments should not be passed to the Responder @@ -970,11 +990,13 @@ mod tests { // (the same applies to invalid formatted transactions) let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); assert_eq!( - watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), + watcher + .store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx) + .await, TriggeredAppointment::Invalid, ); // The appointment is not kept anywhere - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); } @@ -989,25 +1011,29 @@ mod tests { // If the user cannot be properly identified, the request will fail. This can be simulated by providing a wrong signature let wrong_sig = String::from_utf8((0..65).collect()).unwrap(); assert!(matches!( - watcher.get_appointment(appointment.locator, &wrong_sig), + watcher + .get_appointment(appointment.locator, &wrong_sig) + .await, Err(GetAppointmentFailure::AuthenticationFailure) )); // If the user does exist and there's an appointment with the given locator belonging to him, it will be returned let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); watcher .add_appointment( appointment.clone(), cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(), ) + .await .unwrap(); let message = format!("get appointment {}", appointment.locator); let signature = cryptography::sign(message.as_bytes(), &user_sk).unwrap(); let info = watcher .get_appointment(appointment.locator, &signature) + .await .unwrap(); match info { @@ -1027,13 +1053,15 @@ mod tests { let status = ConfirmationStatus::InMempoolSince(chain.get_block_count()); watcher .responder - .add_tracker(uuid, breach.clone(), user_id, status); + .add_tracker(uuid, breach.clone(), user_id, status) + .await; let tracker = TransactionTracker::new(breach, user_id, status); let tracker_message = format!("get appointment {}", appointment.locator); let tracker_signature = cryptography::sign(tracker_message.as_bytes(), &user_sk).unwrap(); let info = watcher .get_appointment(appointment.locator, &tracker_signature) + .await .unwrap(); match info { @@ -1047,21 +1075,26 @@ mod tests { // NotFound should be returned. let (user2_sk, user2_pk) = get_random_keypair(); let user2_id = UserId(user2_pk); - watcher.register(user2_id).unwrap(); + watcher.register(user2_id).await.unwrap(); let signature2 = cryptography::sign(message.as_bytes(), &user2_sk).unwrap(); assert!(matches!( - watcher.get_appointment(appointment.locator, &signature2), + watcher + .get_appointment(appointment.locator, &signature2) + .await, Err(GetAppointmentFailure::NotFound { .. }) )); // If the user subscription has expired, the request will fail watcher .gatekeeper - .add_outdated_user(user_id, START_HEIGHT as u32); + .add_outdated_user(user_id, START_HEIGHT as u32) + .await; assert!(matches!( - watcher.get_appointment(appointment.locator, &signature), + watcher + .get_appointment(appointment.locator, &signature) + .await, Err(GetAppointmentFailure::SubscriptionExpired { .. }) )); } @@ -1079,7 +1112,7 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); // Add some of them to the Watcher let mut breaches = HashMap::new(); @@ -1088,13 +1121,16 @@ mod tests { if i % 2 == 0 { let appointment = generate_dummy_appointment(Some(&tx.txid())).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); - watcher.add_appointment(appointment, signature).unwrap(); + watcher + .add_appointment(appointment, signature) + .await + .unwrap(); breaches.insert(*l, tx.clone()); } } // Check that breaches are correctly detected - assert_eq!(watcher.get_breaches(locator_tx_map), breaches); + assert_eq!(watcher.get_breaches(locator_tx_map).await, breaches); } #[tokio::test] @@ -1110,16 +1146,19 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); // Let the watcher track these breaches. for (_, tx) in breaches.iter() { let appointment = generate_dummy_appointment(Some(&tx.txid())).inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); - watcher.add_appointment(appointment, signature).unwrap(); + watcher + .add_appointment(appointment, signature) + .await + .unwrap(); } - assert!(watcher.handle_breaches(breaches).is_none()) + assert!(watcher.handle_breaches(breaches).await.is_none()) } #[tokio::test] @@ -1135,7 +1174,7 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let mut rejected = HashSet::new(); // Let the watcher track these breaches. @@ -1149,12 +1188,15 @@ mod tests { rejected.insert(uuid); }; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); - watcher.add_appointment(appointment, signature).unwrap(); + watcher + .add_appointment(appointment, signature) + .await + .unwrap(); } assert_eq!( rejected, - HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + HashSet::from_iter(watcher.handle_breaches(breaches).await.unwrap()) ); } @@ -1178,7 +1220,7 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let mut uuids = HashSet::new(); // Let the watcher track these breaches. @@ -1187,13 +1229,16 @@ mod tests { generate_dummy_appointment_with_user(user_id, Some(&tx.txid())); let appointment = appointment.inner; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); - watcher.add_appointment(appointment, signature).unwrap(); + watcher + .add_appointment(appointment, signature) + .await + .unwrap(); uuids.insert(uuid); } assert_eq!( uuids, - HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + HashSet::from_iter(watcher.handle_breaches(breaches).await.unwrap()) ); } @@ -1210,7 +1255,7 @@ mod tests { let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); - watcher.register(user_id).unwrap(); + watcher.register(user_id).await.unwrap(); let mut rejected_breaches = HashSet::new(); // Let the watcher track these breaches. @@ -1224,12 +1269,15 @@ mod tests { rejected_breaches.insert(uuid); }; let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); - watcher.add_appointment(appointment, signature).unwrap(); + watcher + .add_appointment(appointment, signature) + .await + .unwrap(); } assert_eq!( rejected_breaches, - HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + HashSet::from_iter(watcher.handle_breaches(breaches).await.unwrap()) ); } @@ -1269,8 +1317,8 @@ mod tests { let user_id = UserId(user_pk); let (user2_sk, user2_pk) = get_random_keypair(); let user2_id = UserId(user2_pk); - watcher.register(user_id).unwrap(); - watcher.register(user2_id).unwrap(); + watcher.register(user_id).await.unwrap(); + watcher.register(user2_id).await.unwrap(); let appointment = generate_dummy_appointment(None).inner; let uuid1 = UUID::new(appointment.locator, user_id); @@ -1279,14 +1327,19 @@ mod tests { let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(); watcher .add_appointment(appointment.clone(), user_sig) + .await .unwrap(); let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk).unwrap(); - watcher.add_appointment(appointment, user2_sig).unwrap(); + watcher + .add_appointment(appointment, user2_sig) + .await + .unwrap(); // Outdate the first user's registration. watcher .gatekeeper - .add_outdated_user(user_id, chain.get_block_count()); + .add_outdated_user(user_id, chain.get_block_count()) + .await; // Both appointments can be found before mining a block, only the user's 2 can be found afterwards for &uuid in &[uuid1, uuid2] { @@ -1309,14 +1362,14 @@ mod tests { .gatekeeper .get_registered_users() .lock() - .unwrap() + .await .contains_key(&user_id)); assert!(watcher.dbm.appointment_exists(uuid2).await); assert!(watcher .gatekeeper .get_registered_users() .lock() - .unwrap() + .await .contains_key(&user2_id)); // Check triggers. Add a new appointment and trigger it with valid data. @@ -1324,7 +1377,10 @@ mod tests { let (uuid, appointment) = generate_dummy_appointment_with_user(user2_id, Some(&dispute_tx.txid())); let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - watcher.add_appointment(appointment.inner, sig).unwrap(); + watcher + .add_appointment(appointment.inner, sig) + .await + .unwrap(); assert!(watcher.dbm.appointment_exists(uuid).await); @@ -1338,7 +1394,7 @@ mod tests { .await; // Data should have been kept in the database - assert!(watcher.responder.has_tracker(uuid)); + assert!(watcher.responder.has_tracker(uuid).await); // Checks invalid triggers. Add a new appointment and trigger it with invalid data. let dispute_tx = get_random_tx(); @@ -1347,7 +1403,10 @@ mod tests { // Modify the encrypted blob so the data is invalid. appointment.inner.encrypted_blob.reverse(); let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - watcher.add_appointment(appointment.inner, sig).unwrap(); + watcher + .add_appointment(appointment.inner, sig) + .await + .unwrap(); let block = chain.generate(Some(vec![dispute_tx])); watcher @@ -1359,7 +1418,7 @@ mod tests { .await; // Data should have been wiped from the database - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); // Check triggering with a valid formatted transaction but that is rejected by the Responder. @@ -1367,7 +1426,10 @@ mod tests { let (uuid, appointment) = generate_dummy_appointment_with_user(user2_id, Some(&dispute_tx.txid())); let sig = cryptography::sign(&appointment.inner.to_vec(), &user2_sk).unwrap(); - watcher.add_appointment(appointment.inner, sig).unwrap(); + watcher + .add_appointment(appointment.inner, sig) + .await + .unwrap(); // Set the carrier response // Both non-decryptable blobs and blobs with invalid transactions will yield an invalid trigger. @@ -1387,7 +1449,7 @@ mod tests { .await; // Data should have been wiped from the database - assert!(!watcher.responder.has_tracker(uuid)); + assert!(!watcher.responder.has_tracker(uuid).await); assert!(!watcher.dbm.appointment_exists(uuid).await); } From 4bd205113c1ac26931e2a3403964058fabca3817 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Wed, 20 Sep 2023 19:29:02 +0300 Subject: [PATCH 07/15] fixing lint errors --- teos/src/dbm.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 57a6a05e..e2f7cc84 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -656,14 +656,14 @@ mod tests { #[cfg(feature = "postgres")] async fn postgres() -> Self { - (|| async { + async { return_db_if_matching!( "postgres://user:pass@localhost/teos", postgres, "migrations/postgres" ); - Err(format!("Unreachable (the macro above will always match)")) - })() + Err("Unreachable (the macro above will always match)".to_string()) + } .await .unwrap() } @@ -763,7 +763,7 @@ mod tests { SUBSCRIPTION_START + i, SUBSCRIPTION_EXPIRY + i, ); - users.insert(user_id, user_info.clone()); + users.insert(user_id, user_info); dbm.store_user(user_id, &user_info).await.unwrap(); } @@ -1155,7 +1155,7 @@ mod tests { assert_eq!( dbm.batch_remove_appointments(to_be_deleted, updated_users) .await, - i as usize + i ); // Check appointment data was deleted and users properly updated assert_eq!( @@ -1188,7 +1188,7 @@ mod tests { dbm.batch_remove_appointments( vec![uuid], - HashMap::from_iter([(appointment.user_id, info.clone())]), + HashMap::from_iter([(appointment.user_id, info)]), ) .await; assert!(dbm.load_appointment(uuid).await.is_none()); From ba321f4d9f8e51494f3be2c2592b72ef440db0da Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 30 Sep 2023 17:27:22 +0300 Subject: [PATCH 08/15] fix grammar issues --- teos/src/async_listener.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teos/src/async_listener.rs b/teos/src/async_listener.rs index 09f1baf1..4243a1fa 100644 --- a/teos/src/async_listener.rs +++ b/teos/src/async_listener.rs @@ -48,7 +48,7 @@ enum BlockListenerAction { } /// A helper struct that wraps a listener that implements [AsyncListen] and feeds it connected and disconnected -/// blocks received from [UnboundedReceiver] in the background. +/// blocks received from an [UnboundedReceiver] in the background. pub struct AsyncBlockListener { listener: L, dbm: Arc, @@ -93,7 +93,7 @@ impl AsyncBlockListener { } /// A block listener that implements the sync [chain::Listen] trait. All it does is forward the blocks received -/// another (async) block listener through an [UnboundedSender]. +/// to another (async) block listener through an [UnboundedSender]. pub struct SyncBlockListener { tx: UnboundedSender, } From e5519d0cb435a8aa5a0df0e04f30f081196d40be Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 30 Sep 2023 17:32:22 +0300 Subject: [PATCH 09/15] no need for type = --- teos/src/dbm.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index e2f7cc84..8d0f64cc 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -8,7 +8,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; use bitcoin::{consensus, BlockHash}; use sqlx::any::{install_drivers, AnyRow}; -use sqlx::{AnyPool, Row}; +use sqlx::{AnyPool, Error, Row}; use teos_common::appointment::{Appointment, Locator}; use teos_common::UserId; @@ -18,8 +18,6 @@ use crate::gatekeeper::UserInfo; use crate::responder::{ConfirmationStatus, PenaltySummary, TransactionTracker}; use crate::watcher::Breach; -type Error = sqlx::Error; - #[cfg(not(test))] /// The maximum number of bind variables per SQL query. /// `32766` for SQLite & `65535` for PostgreSQL. Picking `32766` for both since it's big enough already. From 233b6049353b685cbdffbfa9d1eaa2f76334d65a Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 30 Sep 2023 18:14:38 +0300 Subject: [PATCH 10/15] mysql todo --- teos/src/dbm.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 8d0f64cc..cc125b7d 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -54,6 +54,7 @@ macro_rules! return_db_if_matching { /// Component in charge of interacting with the underlying database. #[derive(Debug)] pub struct DBM { + // TODO: Add MySQL support. The queries used right now isn't MySQL-compatible. pool: AnyPool, } From 178729f953b16b34dc560f65b945ac47a47c6bcb Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Mon, 2 Oct 2023 19:56:30 +0300 Subject: [PATCH 11/15] fix postgresql tests this still needs an external test db running on the background. one can easily be spawned with docker --- teos/src/dbm.rs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index cc125b7d..0c04d5d9 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -379,7 +379,9 @@ impl DBM { pub(crate) async fn load_uuids(&self, locator: Locator) -> Vec { sqlx::query("SELECT UUID from appointments WHERE locator=($1)") .bind(locator.to_vec()) - .map(|row: AnyRow| UUID::from_slice(row.get("UUID")).unwrap()) + // NOTE: For some reason, indexing using the string "UUID" fails on PostgreSQL. + // Using numerical index for interoperability with SQLite. + .map(|row: AnyRow| UUID::from_slice(row.get(0)).unwrap()) .fetch_all(&self.pool) .await .unwrap() @@ -577,7 +579,8 @@ impl DBM { /// Stores the last known block into the database. pub(crate) async fn store_last_known_block(&self, block_hash: &BlockHash) -> Result<(), Error> { - sqlx::query("INSERT OR REPLACE INTO last_known_block (id, block_hash) VALUES (0, $1)") + let sql = "INSERT INTO last_known_block (id, block_hash) VALUES (0, $1) ON CONFLICT (id) DO UPDATE SET block_hash = excluded.block_hash"; + sqlx::query(sql) .bind(block_hash.to_vec()) .execute(&self.pool) .await @@ -655,7 +658,7 @@ mod tests { #[cfg(feature = "postgres")] async fn postgres() -> Self { - async { + let dbm = async { return_db_if_matching!( "postgres://user:pass@localhost/teos", postgres, @@ -664,7 +667,26 @@ mod tests { Err("Unreachable (the macro above will always match)".to_string()) } .await - .unwrap() + .unwrap(); + // The DBM could have been used in a previous test, so make sure it is clear. + dbm.clear_db().await; + dbm + } + + async fn clear_db(&self) { + let tables_to_clear = [ + "users", + "appointments", + "trackers", + "last_known_block", + "keys", + ]; + for table in tables_to_clear { + sqlx::query(&format!("DELETE FROM {table}")) + .execute(&self.pool) + .await + .unwrap(); + } } #[allow(unreachable_code)] @@ -673,6 +695,8 @@ mod tests { #[cfg(feature = "sqlite")] return Self::memory().await; + // WARNING: When running the tests on PostgreSQL set the environment variable RUST_TEST_THREADS=1. + // Otherwise tests will run on parallel and contaminate the database. #[cfg(feature = "postgres")] return Self::postgres().await; From d42d4bc97024d2a1504909cfa5fa93d7f5907753 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Oct 2023 12:32:17 +0300 Subject: [PATCH 12/15] document `clear_db` method --- teos/src/dbm.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 0c04d5d9..7f3af054 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -673,6 +673,7 @@ mod tests { dbm } + /// Clears all the DB tables. To be used to emulate starting a brand new database. async fn clear_db(&self) { let tables_to_clear = [ "users", From 3aaa7c9a508109ab951a02d40bfbc71ae08a4eb2 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Oct 2023 12:44:44 +0300 Subject: [PATCH 13/15] remove DB logic from teos-common remove rusqlite dependency from teos-common --- Cargo.lock | 2 - teos-common/Cargo.toml | 1 - teos-common/src/dbm.rs | 72 ------------------------------ teos-common/src/lib.rs | 1 - teos/Cargo.toml | 1 - watchtower-plugin/src/dbm.rs | 59 ++++++++++++++++++++++-- watchtower-plugin/src/wt_client.rs | 2 +- 7 files changed, 57 insertions(+), 81 deletions(-) delete mode 100644 teos-common/src/dbm.rs diff --git a/Cargo.lock b/Cargo.lock index 82714f1d..bfe6f7b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3091,7 +3091,6 @@ dependencies = [ "prost 0.9.0", "rand 0.8.5", "rcgen", - "rusqlite", "serde", "serde_json", "simple_logger", @@ -3119,7 +3118,6 @@ dependencies = [ "lightning", "prost 0.9.0", "rand 0.8.5", - "rusqlite", "serde", "serde_json", "tonic 0.6.2", diff --git a/teos-common/Cargo.toml b/teos-common/Cargo.toml index 83c12817..ceceffed 100644 --- a/teos-common/Cargo.toml +++ b/teos-common/Cargo.toml @@ -10,7 +10,6 @@ edition = "2018" # General hex = { version = "0.4.3", features = [ "serde" ] } prost = "0.9" -rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } serde = "1.0.130" serde_json = "1.0" tonic = "0.6" diff --git a/teos-common/src/dbm.rs b/teos-common/src/dbm.rs deleted file mode 100644 index d94369f6..00000000 --- a/teos-common/src/dbm.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Logic related to a common database manager, component in charge of persisting data on disk. This is the base of more complex managers -//! that can be used by both clients and towers. -//! - -use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; -use rusqlite::{Connection, Error as SqliteError, ErrorCode, Params}; - -/// Packs the errors than can raise when interacting with the underlying database. -#[derive(Debug)] -pub enum Error { - AlreadyExists, - MissingForeignKey, - MissingField, - NotFound, - Unknown(SqliteError), -} - -pub trait DatabaseConnection { - fn get_connection(&self) -> &Connection; - fn get_mut_connection(&mut self) -> &mut Connection; -} - -pub trait DatabaseManager: Sized { - fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError>; - fn store_data(&self, query: &str, params: P) -> Result<(), Error>; - fn remove_data(&self, query: &str, params: P) -> Result<(), Error>; - fn update_data(&self, query: &str, params: P) -> Result<(), Error>; -} - -impl DatabaseManager for T { - /// Creates the database tables if not present. - fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - for table in tables.iter() { - tx.execute(table, [])?; - } - tx.commit() - } - - /// Generic method to store data into the database. - fn store_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params) { - Ok(_) => Ok(()), - Err(e) => match e { - SqliteError::SqliteFailure(ie, _) => match ie.code { - ErrorCode::ConstraintViolation => match ie.extended_code { - SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), - SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - } - } - - /// Generic method to remove data from the database. - fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params).unwrap() { - 0 => Err(Error::NotFound), - _ => Ok(()), - } - } - - /// Generic method to update data from the database. - fn update_data(&self, query: &str, params: P) -> Result<(), Error> { - // Updating data is fundamentally the same as deleting it in terms of interface. - // A query is sent and either no row is modified or some rows are - self.remove_data(query, params) - } -} diff --git a/teos-common/src/lib.rs b/teos-common/src/lib.rs index 12a7df0d..49145da1 100644 --- a/teos-common/src/lib.rs +++ b/teos-common/src/lib.rs @@ -11,7 +11,6 @@ pub mod protos { pub mod appointment; pub mod constants; pub mod cryptography; -pub mod dbm; pub mod errors; pub mod net; pub mod receipts; diff --git a/teos/Cargo.toml b/teos/Cargo.toml index 0348ec79..b1868083 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -27,7 +27,6 @@ home = "0.5.3" log = "0.4" prost = "0.9" rcgen = { version = "0.8", features = ["pem", "x509-parser"] } -rusqlite = { version = "0.29.0", features = [ "bundled", "limits" ] } sqlx = { version = "0.7", features = ["runtime-tokio", "tls-native-tls", "migrate", "any"] } serde = "1.0.130" serde_json = "1.0" diff --git a/watchtower-plugin/src/dbm.rs b/watchtower-plugin/src/dbm.rs index bf271bb8..09197aa6 100755 --- a/watchtower-plugin/src/dbm.rs +++ b/watchtower-plugin/src/dbm.rs @@ -3,12 +3,12 @@ use std::iter::FromIterator; use std::path::PathBuf; use std::str::FromStr; -use rusqlite::{params, Connection, Error as SqliteError}; +use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; +use rusqlite::{params, Connection, Error as SqliteError, ErrorCode, Params}; use bitcoin::secp256k1::SecretKey; use teos_common::appointment::{Appointment, Locator}; -use teos_common::dbm::{DatabaseConnection, DatabaseManager, Error}; use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; use teos_common::{TowerId, UserId}; @@ -83,6 +83,16 @@ const TABLES: [&str; 8] = [ )", ]; +/// Packs the errors than can raise when interacting with the underlying database. +#[derive(Debug)] +pub enum Error { + AlreadyExists, + MissingForeignKey, + MissingField, + NotFound, + Unknown(SqliteError), +} + /// Component in charge of interacting with the underlying database. /// /// Currently works for `SQLite`. `PostgreSQL` should also be added in the future. @@ -92,7 +102,7 @@ pub struct DBM { connection: Connection, } -impl DatabaseConnection for DBM { +impl DBM { fn get_connection(&self) -> &Connection { &self.connection } @@ -100,6 +110,49 @@ impl DatabaseConnection for DBM { fn get_mut_connection(&mut self) -> &mut Connection { &mut self.connection } + + /// Creates the database tables if not present. + fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { + let tx = self.get_mut_connection().transaction().unwrap(); + for table in tables.iter() { + tx.execute(table, [])?; + } + tx.commit() + } + + /// Generic method to store data into the database. + fn store_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.get_connection().execute(query, params) { + Ok(_) => Ok(()), + Err(e) => match e { + SqliteError::SqliteFailure(ie, _) => match ie.code { + ErrorCode::ConstraintViolation => match ie.extended_code { + SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), + SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + } + } + + /// Generic method to remove data from the database. + fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.get_connection().execute(query, params).unwrap() { + 0 => Err(Error::NotFound), + _ => Ok(()), + } + } + + #[allow(dead_code)] + /// Generic method to update data from the database. + fn update_data(&self, query: &str, params: P) -> Result<(), Error> { + // Updating data is fundamentally the same as deleting it in terms of interface. + // A query is sent and either no row is modified or some rows are + self.remove_data(query, params) + } } impl DBM { diff --git a/watchtower-plugin/src/wt_client.rs b/watchtower-plugin/src/wt_client.rs index ecf26db0..713bde09 100644 --- a/watchtower-plugin/src/wt_client.rs +++ b/watchtower-plugin/src/wt_client.rs @@ -8,10 +8,10 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use teos_common::appointment::{Appointment, Locator}; use teos_common::cryptography; -use teos_common::dbm::Error as DBError; use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; use teos_common::{TowerId, UserId}; +use crate::dbm::Error as DBError; use crate::dbm::DBM; use crate::net::ProxyInfo; use crate::retrier::RetrierStatus; From 0d4e6eab3841c4e03fbee84e717cb523f53200a8 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Oct 2023 12:55:58 +0300 Subject: [PATCH 14/15] elaborating a comment --- teos/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teos/src/main.rs b/teos/src/main.rs index 0ac14c66..1e13056d 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -328,7 +328,7 @@ async fn main() { // first so it updates the users' states and both the Watcher and the Responder operate only on registered users. let listeners = (gatekeeper, (watcher.clone(), responder)); - // This spawns a separate async actor that will be fed new blocks from a sync block listener. + // This spawns a separate async actor in the background that will be fed new blocks from a sync block listener. // In this way we can have our components listen to blocks in an async manner from the async actor. let listener = AsyncBlockListener::wrap_listener(listeners, dbm); From 782ed39eca02c31ef0ea70014e4fb2169e3bdb37 Mon Sep 17 00:00:00 2001 From: Omer Yacine Date: Sat, 14 Oct 2023 13:25:00 +0300 Subject: [PATCH 15/15] fix a typo --- teos/src/watcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index 512488cc..fd6ca6e4 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -418,7 +418,7 @@ impl Watcher { (!invalid_breaches.is_empty()).then_some(invalid_breaches) } - /// Ges the number of users currently registered with the tower. + /// Gets the number of users currently registered with the tower. pub(crate) async fn get_registered_users_count(&self) -> usize { self.gatekeeper.get_registered_users_count().await }