diff --git a/database/src/env.rs b/database/src/env.rs index d21595c5..437928a6 100644 --- a/database/src/env.rs +++ b/database/src/env.rs @@ -12,7 +12,7 @@ use crate::{ tables::{ call_fn_on_all_tables_or_early_return, BlockBlobs, BlockHeights, BlockInfos, KeyImages, NumOutputs, Outputs, PrunableHashes, PrunableTxBlobs, PrunedTxBlobs, RctOutputs, Tables, - TablesMut, TxHeights, TxIds, TxUnlockTime, + TablesIter, TablesMut, TxHeights, TxIds, TxUnlockTime, }, transaction::{TxRo, TxRw}, }; @@ -233,7 +233,7 @@ where /// /// # Errors /// TODO - fn open_tables(&self, tx_ro: &Ro) -> Result { + fn open_tables(&self, tx_ro: &Ro) -> Result { call_fn_on_all_tables_or_early_return! { Self::open_db_ro(self, tx_ro) } diff --git a/database/src/service/tests.rs b/database/src/service/tests.rs index 143fedef..eb73dd40 100644 --- a/database/src/service/tests.rs +++ b/database/src/service/tests.rs @@ -7,14 +7,21 @@ // This is only imported on `#[cfg(test)]` in `mod.rs`. -#![allow(unused_mut, clippy::significant_drop_tightening)] - -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Arc, -}; +#![allow( + clippy::significant_drop_tightening, + clippy::await_holding_lock, + clippy::too_many_lines +)] //---------------------------------------------------------------------------------------------------- Use +use std::{ + collections::{HashMap, HashSet}, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, +}; + use tower::{Service, ServiceExt}; use cuprate_test_utils::data::{block_v16_tx0, block_v1_tx2, block_v9_tx3}; @@ -25,11 +32,16 @@ use cuprate_types::{ use crate::{ config::Config, - ops::block::{get_block_extended_header_from_height, get_block_info}, + ops::{ + block::{get_block_extended_header_from_height, get_block_info}, + blockchain::top_block_height, + output::get_output, + }, service::{init, DatabaseReadHandle, DatabaseWriteHandle}, - tables::Tables, + tables::{KeyImages, Tables, TablesIter}, tests::AssertTableLen, - ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError, + types::{Amount, KeyImage}, + ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, RuntimeError, }; //---------------------------------------------------------------------------------------------------- Helper functions @@ -47,22 +59,161 @@ fn init_service() -> ( (reader, writer, env, tempdir) } -/// Send a write request, and receive a response, -/// asserting the response the expected value. -async fn write_request( - writer: &mut DatabaseWriteHandle, - block_fn: fn() -> &'static VerifiedBlockInformation, +/// This is the template used in the actual test functions below. +/// +/// - Send write request(s) +/// - Receive response(s) +/// - Assert proper tables were mutated +/// - Assert read requests lead to expected responses +#[allow(clippy::future_not_send)] // INVARIANT: tests are using a single threaded runtime +async fn test_template( + // Which block(s) to add? + block_fns: &[fn() -> &'static VerifiedBlockInformation], + // Total amount of generated coins after the block(s) have been added. + cumulative_generated_coins: u64, + // What are the table lengths be after the block(s) have been added? + assert_table_len: AssertTableLen, ) { + //----------------------------------------------------------------------- Write requests + let (reader, mut writer, env, _tempdir) = init_service(); + + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + // HACK: `add_block()` asserts blocks with non-sequential heights // cannot be added, to get around this, manually edit the block height. - let mut block = block_fn().clone(); - block.height = 0; + for (i, block_fn) in block_fns.iter().enumerate() { + let mut block = block_fn().clone(); + block.height = i as u64; - // Request a block to be written, assert it was written. - let request = WriteRequest::WriteBlock(block); - let response_channel = writer.call(request); - let response = response_channel.await.unwrap(); - assert_eq!(response, Response::WriteBlockOk); + // Request a block to be written, assert it was written. + let request = WriteRequest::WriteBlock(block); + let response_channel = writer.call(request); + let response = response_channel.await.unwrap(); + assert_eq!(response, Response::WriteBlockOk); + } + + //----------------------------------------------------------------------- Reset the transaction + drop(tables); + drop(tx_ro); + let tx_ro = env_inner.tx_ro().unwrap(); + let tables = env_inner.open_tables(&tx_ro).unwrap(); + + //----------------------------------------------------------------------- Assert all table lengths are correct + assert_table_len.assert(&tables); + + //----------------------------------------------------------------------- Read request prep + let extended_block_header_0 = Ok(Response::BlockExtendedHeader( + get_block_extended_header_from_height(&0, &tables).unwrap(), + )); + + let extended_block_header_1 = if block_fns.len() > 1 { + Ok(Response::BlockExtendedHeader( + get_block_extended_header_from_height(&1, &tables).unwrap(), + )) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let block_hash_0 = Ok(Response::BlockHash( + get_block_info(&0, tables.block_infos()).unwrap().block_hash, + )); + + let block_hash_1 = if block_fns.len() > 1 { + Ok(Response::BlockHash( + get_block_info(&1, tables.block_infos()).unwrap().block_hash, + )) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let range_0_1 = Ok(Response::BlockExtendedHeaderInRange(vec![ + get_block_extended_header_from_height(&0, &tables).unwrap(), + ])); + + let range_0_2 = if block_fns.len() >= 2 { + Ok(Response::BlockExtendedHeaderInRange(vec![ + get_block_extended_header_from_height(&0, &tables).unwrap(), + get_block_extended_header_from_height(&1, &tables).unwrap(), + ])) + } else { + Err(RuntimeError::KeyNotFound) + }; + + let chain_height = { + let height = top_block_height(tables.block_heights()).unwrap(); + let block_info = get_block_info(&height, tables.block_infos()).unwrap(); + Ok(Response::ChainHeight(height, block_info.block_hash)) + }; + + let cumulative_generated_coins = Ok(Response::GeneratedCoins(cumulative_generated_coins)); + + // let outputs = tables + // .outputs_iter() + // .keys() + // .unwrap() + // .map(Result::unwrap) + // .map(key.amount); + // .collect::>>(); + + let num_req = tables + .outputs_iter() + .keys() + .unwrap() + .map(Result::unwrap) + .map(|key| key.amount) + .collect::>(); + + let num_resp = Ok(Response::NumberOutputsWithAmount( + num_req + .iter() + .map(|amount| match tables.num_outputs().get(amount) { + // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` + #[allow(clippy::cast_possible_truncation)] + Ok(count) => (*amount, count as usize), + Err(RuntimeError::KeyNotFound) => (*amount, 0), + Err(e) => panic!(), + }) + .collect::>(), + )); + + // Contains a fake non-spent key-image. + let ki_req = HashSet::from([[0; 32]]); + let ki_resp = Ok(Response::CheckKIsNotSpent(true)); + + //----------------------------------------------------------------------- Assert expected response + // Assert read requests lead to the expected responses. + for (request, expected_response) in [ + (ReadRequest::BlockExtendedHeader(0), extended_block_header_0), + (ReadRequest::BlockExtendedHeader(1), extended_block_header_1), + (ReadRequest::BlockHash(0), block_hash_0), + (ReadRequest::BlockHash(1), block_hash_1), + (ReadRequest::BlockExtendedHeaderInRange(0..1), range_0_1), + (ReadRequest::BlockExtendedHeaderInRange(0..2), range_0_2), + (ReadRequest::ChainHeight, chain_height), + (ReadRequest::GeneratedCoins, cumulative_generated_coins), + // (ReadRequest::Outputs(HashMap>), ), + (ReadRequest::NumberOutputsWithAmount(num_req), num_resp), + (ReadRequest::CheckKIsNotSpent(ki_req), ki_resp), + ] { + let response = reader.clone().oneshot(request).await; + println!("response: {response:#?}, expected_response: {expected_response:#?}"); + match response { + Ok(resp) => assert_eq!(resp, expected_response.unwrap()), + Err(ref e) => assert!(matches!(response, expected_response)), + } + } + + //----------------------------------------------------------------------- Key image checks + // Assert each key image we inserted comes back as "spent". + for key_image in tables.key_images_iter().keys().unwrap() { + let key_image = key_image.unwrap(); + let request = ReadRequest::CheckKIsNotSpent(HashSet::from([key_image])); + let response = reader.clone().oneshot(request).await; + println!("response: {response:#?}, key_image: {key_image:#?}"); + assert_eq!(response.unwrap(), Response::CheckKIsNotSpent(false)); + } } //---------------------------------------------------------------------------------------------------- Tests @@ -77,152 +228,77 @@ fn init_drop() { /// Assert write/read correctness of [`block_v1_tx2`]. #[tokio::test] async fn v1_tx2() { - let (reader, mut writer, env, _tempdir) = init_service(); - - write_request(&mut writer, block_v1_tx2).await; - - // Assert the actual database tables were correctly modified. - let env_inner = env.env_inner(); - let tx_ro = env_inner.tx_ro().unwrap(); - let tables = env_inner.open_tables(&tx_ro).unwrap(); - - AssertTableLen { - block_infos: 1, - block_blobs: 1, - block_heights: 1, - key_images: 65, - num_outputs: 38, - pruned_tx_blobs: 0, - prunable_hashes: 0, - outputs: 107, - prunable_tx_blobs: 0, - rct_outputs: 0, - tx_blobs: 2, - tx_ids: 2, - tx_heights: 2, - tx_unlock_time: 0, - } - .assert(&tables); - - let height = 0; - let extended_block_header = get_block_extended_header_from_height(&height, &tables).unwrap(); - let block_info = get_block_info(&height, tables.block_infos()).unwrap(); - - // Assert reads are correct. - for (request, expected_response) in [ - // Each tuple is a `Request` + `Result` pair. - ( - ReadRequest::BlockExtendedHeader(0), // The request to send to the service - Ok(Response::BlockExtendedHeader(extended_block_header)), // The expected response - ), - ( - ReadRequest::BlockExtendedHeader(1), - Err(RuntimeError::KeyNotFound), - ), - ( - ReadRequest::BlockHash(0), - Ok(Response::BlockHash(block_info.block_hash)), - ), - (ReadRequest::BlockHash(1), Err(RuntimeError::KeyNotFound)), - ( - ReadRequest::BlockExtendedHeaderInRange(0..1), - Ok(Response::BlockExtendedHeaderInRange(vec![ - extended_block_header, - ])), - ), - ( - ReadRequest::BlockExtendedHeaderInRange(0..2), - Err(RuntimeError::KeyNotFound), - ), - ( - ReadRequest::ChainHeight, - Ok(Response::ChainHeight(height, block_info.block_hash)), - ), - (ReadRequest::GeneratedCoins, Ok(Response::GeneratedCoins(0))), - // (ReadRequest::Outputs(HashMap>), ), - // (ReadRequest::NumberOutputsWithAmount(Vec), ), - // (ReadRequest::CheckKIsNotSpent(HashSet<[u8; 32]>), ), - ] { - let response_channel = reader.clone().oneshot(request); - let response = response_channel.await; - println!("response: {response:#?}, expected_response: {expected_response:#?}"); - assert!(matches!(response, expected_response)); - } + test_template( + &[block_v1_tx2], + 13_138_270_467_918, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 65, + num_outputs: 38, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 107, + prunable_tx_blobs: 0, + rct_outputs: 0, + tx_blobs: 2, + tx_ids: 2, + tx_heights: 2, + tx_unlock_time: 0, + }, + ) + .await; } /// Assert write/read correctness of [`block_v9_tx3`]. #[tokio::test] async fn v9_tx3() { - let (reader, mut writer, env, _tempdir) = init_service(); - - write_request(&mut writer, block_v9_tx3).await; - - // Assert the actual database tables were correctly modified. - let env_inner = env.env_inner(); - let tx_ro = env_inner.tx_ro().unwrap(); - let tables = env_inner.open_tables(&tx_ro).unwrap(); - - assert_eq!(tables.block_infos().len().unwrap(), 1); - assert_eq!(tables.block_blobs().len().unwrap(), 1); - assert_eq!(tables.block_heights().len().unwrap(), 1); - assert_eq!(tables.key_images().len().unwrap(), 4); - assert_eq!(tables.num_outputs().len().unwrap(), 0); - assert_eq!(tables.pruned_tx_blobs().len().unwrap(), 0); - assert_eq!(tables.prunable_hashes().len().unwrap(), 0); - assert_eq!(tables.outputs().len().unwrap(), 0); - assert_eq!(tables.prunable_tx_blobs().len().unwrap(), 0); - assert_eq!(tables.rct_outputs().len().unwrap(), 6); - assert_eq!(tables.tx_blobs().len().unwrap(), 3); - assert_eq!(tables.tx_ids().len().unwrap(), 3); - assert_eq!(tables.tx_heights().len().unwrap(), 3); - assert_eq!(tables.tx_unlock_time().len().unwrap(), 0); + test_template( + &[block_v9_tx3], + 3_403_774_022_163, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 4, + num_outputs: 0, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 0, + prunable_tx_blobs: 0, + rct_outputs: 6, + tx_blobs: 3, + tx_ids: 3, + tx_heights: 3, + tx_unlock_time: 0, + }, + ) + .await; } /// Assert write/read correctness of [`block_v16_tx0`]. #[tokio::test] async fn v16_tx0() { - let (reader, mut writer, env, _tempdir) = init_service(); - - write_request(&mut writer, block_v16_tx0).await; - - // Assert the actual database tables were correctly modified. - let env_inner = env.env_inner(); - let tx_ro = env_inner.tx_ro().unwrap(); - let tables = env_inner.open_tables(&tx_ro).unwrap(); - - assert_eq!(tables.block_infos().len().unwrap(), 1); - assert_eq!(tables.block_blobs().len().unwrap(), 1); - assert_eq!(tables.block_heights().len().unwrap(), 1); - assert_eq!(tables.key_images().len().unwrap(), 0); - assert_eq!(tables.num_outputs().len().unwrap(), 0); - assert_eq!(tables.pruned_tx_blobs().len().unwrap(), 0); - assert_eq!(tables.prunable_hashes().len().unwrap(), 0); - assert_eq!(tables.outputs().len().unwrap(), 0); - assert_eq!(tables.prunable_tx_blobs().len().unwrap(), 0); - assert_eq!(tables.rct_outputs().len().unwrap(), 0); - assert_eq!(tables.tx_blobs().len().unwrap(), 0); - assert_eq!(tables.tx_ids().len().unwrap(), 0); - assert_eq!(tables.tx_heights().len().unwrap(), 0); - assert_eq!(tables.tx_unlock_time().len().unwrap(), 0); + test_template( + &[block_v16_tx0], + 600_000_000_000, + AssertTableLen { + block_infos: 1, + block_blobs: 1, + block_heights: 1, + key_images: 0, + num_outputs: 0, + pruned_tx_blobs: 0, + prunable_hashes: 0, + outputs: 0, + prunable_tx_blobs: 0, + rct_outputs: 0, + tx_blobs: 0, + tx_ids: 0, + tx_heights: 0, + tx_unlock_time: 0, + }, + ) + .await; } - -// /// Send a read request, and receive a response, -// /// asserting the response the expected value. -// #[tokio::test] -// async fn read_request() { -// let (reader, writer, _tempdir) = init_service(); - -// for (request, expected_response) in [ -// (ReadRequest::Example1, Response::Example1), -// (ReadRequest::Example2(123), Response::Example2(123)), -// ( -// ReadRequest::Example3("hello".into()), -// Response::Example3("hello".into()), -// ), -// ] { -// // This calls `poll_ready()` asserting we have a permit before `call()`. -// let response_channel = reader.clone().oneshot(request); -// let response = response_channel.await.unwrap(); -// assert_eq!(response, expected_response); -// } -// }