From b4b73ec61c14aa0b383e63d75c3fa60365e16eed Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 16 Aug 2024 02:23:18 +0100 Subject: [PATCH 01/46] add cuprated skeleton --- Cargo.lock | 4 ++++ Cargo.toml | 1 + binaries/cuprated/Cargo.toml | 13 +++++++++++++ binaries/cuprated/src/blockchain.rs | 6 ++++++ binaries/cuprated/src/blockchain/manager.rs | 0 binaries/cuprated/src/blockchain/syncer.rs | 0 binaries/cuprated/src/config.rs | 1 + binaries/cuprated/src/main.rs | 9 +++++++++ binaries/cuprated/src/p2p.rs | 5 +++++ binaries/cuprated/src/p2p/request_handler.rs | 0 binaries/cuprated/src/rpc.rs | 5 +++++ binaries/cuprated/src/rpc/request_handler.rs | 0 binaries/cuprated/src/txpool.rs | 3 +++ 13 files changed, 47 insertions(+) create mode 100644 binaries/cuprated/Cargo.toml create mode 100644 binaries/cuprated/src/blockchain.rs create mode 100644 binaries/cuprated/src/blockchain/manager.rs create mode 100644 binaries/cuprated/src/blockchain/syncer.rs create mode 100644 binaries/cuprated/src/config.rs create mode 100644 binaries/cuprated/src/main.rs create mode 100644 binaries/cuprated/src/p2p.rs create mode 100644 binaries/cuprated/src/p2p/request_handler.rs create mode 100644 binaries/cuprated/src/rpc.rs create mode 100644 binaries/cuprated/src/rpc/request_handler.rs create mode 100644 binaries/cuprated/src/txpool.rs diff --git a/Cargo.lock b/Cargo.lock index 394589629..052b1ee60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,6 +881,10 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cuprated" +version = "0.1.0" + [[package]] name = "curve25519-dalek" version = "4.1.3" diff --git a/Cargo.toml b/Cargo.toml index 71efcca46..06b49a0a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "binaries/cuprated", "consensus", "consensus/fast-sync", "consensus/rules", diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml new file mode 100644 index 000000000..b52439061 --- /dev/null +++ b/binaries/cuprated/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cuprated" +version = "0.1.0" +edition = "2021" +description = "The Cuprate Monero Rust node." +license = "AGPL-3.0-only" +authors = ["Boog900", "hinto-janai", "SyntheticBird45"] +repository = "https://github.com/Cuprate/cuprate/tree/main/binaries/cuprated" + +[dependencies] + +[lints] +workspace = true diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs new file mode 100644 index 000000000..4abebeb61 --- /dev/null +++ b/binaries/cuprated/src/blockchain.rs @@ -0,0 +1,6 @@ +//! Blockchain +//! +//! Will contain the chain manager and syncer. + +mod manager; +mod syncer; diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs new file mode 100644 index 000000000..e69de29bb diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs new file mode 100644 index 000000000..e69de29bb diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs new file mode 100644 index 000000000..d613c1fcc --- /dev/null +++ b/binaries/cuprated/src/config.rs @@ -0,0 +1 @@ +//! cuprated config diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs new file mode 100644 index 000000000..918429c9a --- /dev/null +++ b/binaries/cuprated/src/main.rs @@ -0,0 +1,9 @@ +mod blockchain; +mod config; +mod p2p; +mod rpc; +mod txpool; + +fn main() { + todo!() +} diff --git a/binaries/cuprated/src/p2p.rs b/binaries/cuprated/src/p2p.rs new file mode 100644 index 000000000..f5b72ba3a --- /dev/null +++ b/binaries/cuprated/src/p2p.rs @@ -0,0 +1,5 @@ +//! P2P +//! +//! Will handle initiating the P2P and contains a protocol request handler. + +mod request_handler; diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs new file mode 100644 index 000000000..e69de29bb diff --git a/binaries/cuprated/src/rpc.rs b/binaries/cuprated/src/rpc.rs new file mode 100644 index 000000000..80b2789ea --- /dev/null +++ b/binaries/cuprated/src/rpc.rs @@ -0,0 +1,5 @@ +//! RPC +//! +//! Will contain the code to initiate the RPC and a request handler. + +mod request_handler; diff --git a/binaries/cuprated/src/rpc/request_handler.rs b/binaries/cuprated/src/rpc/request_handler.rs new file mode 100644 index 000000000..e69de29bb diff --git a/binaries/cuprated/src/txpool.rs b/binaries/cuprated/src/txpool.rs new file mode 100644 index 000000000..a6f05e751 --- /dev/null +++ b/binaries/cuprated/src/txpool.rs @@ -0,0 +1,3 @@ +//! Transaction Pool +//! +//! Will handle initiating the tx-pool, providing the preprocessor required for the dandelion pool. From 39d48fe9fc83fce523d812b669c9a1197fe574d9 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 16 Aug 2024 15:01:42 +0100 Subject: [PATCH 02/46] fmt and add deny exception --- binaries/cuprated/src/blockchain/manager.rs | 1 + binaries/cuprated/src/blockchain/syncer.rs | 1 + binaries/cuprated/src/p2p/request_handler.rs | 1 + binaries/cuprated/src/rpc/request_handler.rs | 1 + deny.toml | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index e69de29bb..8b1378917 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -0,0 +1 @@ + diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index e69de29bb..8b1378917 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -0,0 +1 @@ + diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index e69de29bb..8b1378917 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -0,0 +1 @@ + diff --git a/binaries/cuprated/src/rpc/request_handler.rs b/binaries/cuprated/src/rpc/request_handler.rs index e69de29bb..8b1378917 100644 --- a/binaries/cuprated/src/rpc/request_handler.rs +++ b/binaries/cuprated/src/rpc/request_handler.rs @@ -0,0 +1 @@ + diff --git a/deny.toml b/deny.toml index 85e7da289..f469d062a 100644 --- a/deny.toml +++ b/deny.toml @@ -133,7 +133,7 @@ confidence-threshold = 0.8 # aren't accepted for every possible crate as with the normal allow list exceptions = [ # Cuprate (AGPL-3.0) - # { allow = ["AGPL-3.0"], name = "cuprated", version = "*" } + { allow = ["AGPL-3.0"], name = "cuprated", version = "*" } # Each entry is the crate and version constraint, and its specific allow # list From a01846954dc35ec2095a2cbbf5d5b12da188ed35 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Tue, 20 Aug 2024 16:39:23 +0100 Subject: [PATCH 03/46] add main chain batch handler --- Cargo.lock | 14 ++ binaries/cuprated/Cargo.toml | 13 ++ binaries/cuprated/src/blockchain.rs | 1 + binaries/cuprated/src/blockchain/manager.rs | 13 ++ .../src/blockchain/manager/batch_handler.rs | 143 ++++++++++++++++++ binaries/cuprated/src/blockchain/syncer.rs | 121 +++++++++++++++ binaries/cuprated/src/blockchain/types.rs | 27 ++++ consensus/fast-sync/src/fast_sync.rs | 3 +- consensus/src/block.rs | 4 +- p2p/p2p/src/lib.rs | 2 +- 10 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 binaries/cuprated/src/blockchain/manager/batch_handler.rs create mode 100644 binaries/cuprated/src/blockchain/types.rs diff --git a/Cargo.lock b/Cargo.lock index 052b1ee60..fbcd326a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -884,6 +884,20 @@ dependencies = [ [[package]] name = "cuprated" version = "0.1.0" +dependencies = [ + "cuprate-blockchain", + "cuprate-consensus", + "cuprate-p2p", + "cuprate-p2p-core", + "cuprate-types", + "futures", + "hex", + "rayon", + "thiserror", + "tokio", + "tower", + "tracing", +] [[package]] name = "curve25519-dalek" diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index b52439061..dd6cdc1d5 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -8,6 +8,19 @@ authors = ["Boog900", "hinto-janai", "SyntheticBird45"] repository = "https://github.com/Cuprate/cuprate/tree/main/binaries/cuprated" [dependencies] +cuprate-consensus = { path = "../../consensus" } +cuprate-blockchain = { path = "../../storage/blockchain" } +cuprate-p2p = { path = "../../p2p/p2p" } +cuprate-p2p-core = { path = "../../p2p/p2p-core" } +cuprate-types = { path = "../../types" } + +rayon = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +tower = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +hex = { workspace = true } [lints] workspace = true diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 4abebeb61..0b6cd3b4b 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -4,3 +4,4 @@ mod manager; mod syncer; +mod types; diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 8b1378917..c9f9b06b5 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1 +1,14 @@ +mod batch_handler; +use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::{BlockChainContextService, BlockVerifierService, TxVerifierService}; + +struct BlockchainManager { + blockchain_write_handle: BlockchainWriteHandle, + blockchain_context_service: BlockChainContextService, + block_verifier_service: BlockVerifierService< + BlockChainContextService, + TxVerifierService, + BlockchainReadHandle, + >, +} diff --git a/binaries/cuprated/src/blockchain/manager/batch_handler.rs b/binaries/cuprated/src/blockchain/manager/batch_handler.rs new file mode 100644 index 000000000..f080f3100 --- /dev/null +++ b/binaries/cuprated/src/blockchain/manager/batch_handler.rs @@ -0,0 +1,143 @@ +//! Block batch handling functions. + +use crate::blockchain::types::ConsensusBlockchainReadHandle; +use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::context::NewBlockData; +use cuprate_consensus::{ + BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, + BlockVerifierService, BlockchainReadRequest, BlockchainResponse, ExtendedConsensusError, + VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, +}; +use cuprate_p2p::block_downloader::BlockBatch; +use cuprate_types::blockchain::BlockchainWriteRequest; +use cuprate_types::{Chain, HardFork}; +use tower::{Service, ServiceExt}; +use tracing::{debug, error, info}; + +pub async fn handle_incoming_block_batch( + batch: BlockBatch, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_read_handle: &mut BlockchainReadHandle, + blockchain_write_handle: &mut BlockchainWriteHandle, +) where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + let (first_block, _) = batch + .blocks + .first() + .expect("Block batch should not be empty"); + + match blockchain_read_handle + .oneshot(BlockchainReadRequest::FindBlock( + first_block.header.previous, + )) + .await + { + Err(_) | Ok(BlockchainResponse::FindBlock(None)) => { + // The block downloader shouldn't be downloading orphan blocks + error!("Failed to find parent block for first block in batch."); + return; + } + Ok(BlockchainResponse::FindBlock(Some((chain, _)))) => match chain { + Chain::Main => { + handle_incoming_block_batch_main_chain( + batch, + block_verifier_service, + blockchain_context_service, + blockchain_write_handle, + ) + .await; + } + Chain::Alt(_) => todo!(), + }, + + Ok(_) => panic!("Blockchain service returned incorrect response"), + } +} + +async fn handle_incoming_block_batch_main_chain( + batch: BlockBatch, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_write_handle: &mut BlockchainWriteHandle, +) where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + let Ok(VerifyBlockResponse::MainChainBatchPrepped(prepped)) = block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { + blocks: batch.blocks, + }) + .await + else { + info!("Error verifying batch, banning peer"); + todo!() + }; + + for (block, txs) in prepped { + let Ok(VerifyBlockResponse::MainChain(verified_block)) = block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainPrepped { block, txs }) + .await + else { + info!("Error verifying batch, banning peer"); + todo!() + }; + + blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::Update(NewBlockData { + block_hash: verified_block.block_hash, + height: verified_block.height, + timestamp: verified_block.block.header.timestamp, + weight: verified_block.weight, + long_term_weight: verified_block.long_term_weight, + generated_coins: verified_block.generated_coins, + vote: HardFork::from_vote(verified_block.block.header.hardfork_signal), + cumulative_difficulty: verified_block.cumulative_difficulty, + })) + .await + .expect("TODO"); + + blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::WriteBlock(verified_block)) + .await + .expect("TODO"); + } +} diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index 8b1378917..21c367b82 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1 +1,122 @@ +use std::time::Duration; +use futures::StreamExt; +use tokio::{sync::mpsc, time::sleep}; +use tower::{Service, ServiceExt}; +use tracing::instrument; + +use cuprate_consensus::{BlockChainContext, BlockChainContextRequest, BlockChainContextResponse}; +use cuprate_p2p::{ + block_downloader::{BlockBatch, BlockDownloaderConfig, ChainSvcRequest, ChainSvcResponse}, + NetworkInterface, +}; +use cuprate_p2p_core::ClearNet; + +/// An error returned from the [`syncer`]. +#[derive(Debug, thiserror::Error)] +enum SyncerError { + #[error("Incoming block channel closed.")] + IncomingBlockChannelClosed, + #[error("One of our services returned an error: {0}.")] + ServiceError(#[from] tower::BoxError), +} + +#[instrument(level = "debug", skip_all)] +pub async fn syncer( + mut context_svc: C, + our_chain: CN, + clearnet_interface: NetworkInterface, + incoming_block_batch_tx: mpsc::Sender, + block_downloader_config: BlockDownloaderConfig, +) -> Result<(), SyncerError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + >, + C::Future: Send + 'static, + CN: Service + + Clone + + Send + + 'static, + CN::Future: Send + 'static, +{ + tracing::info!("Starting blockchain syncer"); + + let BlockChainContextResponse::Context(mut blockchain_ctx) = context_svc + .ready() + .await? + .call(BlockChainContextRequest::GetContext) + .await? + else { + panic!("Blockchain context service returned wrong response!"); + }; + + let mut peer_sync_watch = clearnet_interface.top_sync_stream(); + + tracing::debug!("Waiting for new sync info in top sync channel"); + + while let Some(top_sync_info) = peer_sync_watch.next().await { + tracing::debug!( + "New sync info seen, top height: {}, top block hash: {}", + top_sync_info.chain_height, + hex::encode(top_sync_info.top_hash) + ); + + // The new info could be from a peer giving us a block, so wait a couple seconds to allow the block to + // be added to our blockchain. + sleep(Duration::from_secs(2)).await; + + check_update_blockchain_context(&mut context_svc, &mut blockchain_ctx).await?; + let raw_blockchain_context = blockchain_ctx.unchecked_blockchain_context(); + + if top_sync_info.cumulative_difficulty <= raw_blockchain_context.cumulative_difficulty { + tracing::debug!("New peer sync info is not ahead, nothing to do."); + continue; + } + + tracing::debug!( + "We are behind peers claimed cumulative difficulty, starting block downloader" + ); + let mut block_batch_stream = + clearnet_interface.block_downloader(our_chain.clone(), block_downloader_config); + + while let Some(batch) = block_batch_stream.next().await { + tracing::debug!("Got batch, len: {}", batch.blocks.len()); + if incoming_block_batch_tx.send(batch).await.is_err() { + return Err(SyncerError::IncomingBlockChannelClosed); + } + } + } + + Ok(()) +} + +async fn check_update_blockchain_context( + context_svc: C, + old_context: &mut BlockChainContext, +) -> Result<(), tower::BoxError> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + >, + C::Future: Send + 'static, +{ + if old_context.blockchain_context().is_ok() { + return Ok(()); + } + + let BlockChainContextResponse::Context(ctx) = context_svc + .oneshot(BlockChainContextRequest::GetContext) + .await? + else { + panic!("Blockchain context service returned wrong response!"); + }; + + *old_context = ctx; + + Ok(()) +} diff --git a/binaries/cuprated/src/blockchain/types.rs b/binaries/cuprated/src/blockchain/types.rs new file mode 100644 index 000000000..0387a7df7 --- /dev/null +++ b/binaries/cuprated/src/blockchain/types.rs @@ -0,0 +1,27 @@ +use cuprate_blockchain::cuprate_database::RuntimeError; +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +use futures::future::MapErr; +use futures::TryFutureExt; +use std::task::{Context, Poll}; +use tower::Service; + +#[derive(Clone)] +pub struct ConsensusBlockchainReadHandle(BlockchainReadHandle); + +impl Service for ConsensusBlockchainReadHandle { + type Response = BlockchainResponse; + type Error = tower::BoxError; + type Future = MapErr< + >::Future, + fn(RuntimeError) -> tower::BoxError, + >; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.0.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: BlockchainReadRequest) -> Self::Future { + self.0.call(req).map_err(Into::into) + } +} diff --git a/consensus/fast-sync/src/fast_sync.rs b/consensus/fast-sync/src/fast_sync.rs index 35fa67424..6baac8d32 100644 --- a/consensus/fast-sync/src/fast_sync.rs +++ b/consensus/fast-sync/src/fast_sync.rs @@ -40,11 +40,12 @@ static HASHES_OF_HASHES: &[HashOfHashes] = &[ const BATCH_SIZE: usize = 4; #[inline] -fn max_height() -> u64 { +pub fn max_height() -> u64 { (HASHES_OF_HASHES.len() * BATCH_SIZE) as u64 } #[derive(Debug, PartialEq)] +#[repr(transparent)] pub struct ValidBlockId(BlockId); fn valid_block_ids(block_ids: &[BlockId]) -> Vec { diff --git a/consensus/src/block.rs b/consensus/src/block.rs index e785a6b78..7297a5a6a 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -250,7 +250,7 @@ where + Clone + Send + 'static, - D: Database + Clone + Send + Sync + 'static, + D: Database + Clone + Send + 'static, D::Future: Send + 'static, { /// Creates a new block verifier. @@ -284,7 +284,7 @@ where + 'static, TxV::Future: Send + 'static, - D: Database + Clone + Send + Sync + 'static, + D: Database + Clone + Send + 'static, D::Future: Send + 'static, { type Response = VerifyBlockResponse; diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index be18c2a3f..82ecfce73 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -21,7 +21,7 @@ use cuprate_p2p_core::{ CoreSyncSvc, NetworkZone, ProtocolRequestHandler, }; -mod block_downloader; +pub mod block_downloader; mod broadcast; mod client_pool; pub mod config; From f909c262faf912647e7b800662f467d07a7636ce Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Wed, 21 Aug 2024 16:02:00 +0100 Subject: [PATCH 04/46] add blockchain init --- Cargo.lock | 70 +++++++++++++++++++ binaries/cuprated/Cargo.toml | 8 ++- binaries/cuprated/src/blockchain.rs | 67 ++++++++++++++++++ binaries/cuprated/src/blockchain/manager.rs | 49 +++++++++++++- binaries/cuprated/src/blockchain/syncer.rs | 4 +- binaries/cuprated/src/blockchain/types.rs | 75 ++++++++++++++++++++- binaries/cuprated/src/main.rs | 25 ++++++- consensus/src/lib.rs | 15 ++--- 8 files changed, 292 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbcd326a5..58b66fe6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,12 +44,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -352,8 +395,10 @@ version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", ] [[package]] @@ -374,6 +419,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "core-foundation" version = "0.9.4" @@ -885,6 +936,7 @@ dependencies = [ name = "cuprated" version = "0.1.0" dependencies = [ + "clap", "cuprate-blockchain", "cuprate-consensus", "cuprate-p2p", @@ -1590,6 +1642,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itoa" version = "1.0.11" @@ -2623,6 +2681,12 @@ dependencies = [ "spin", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2992,6 +3056,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.4" diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index dd6cdc1d5..74eb2d599 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -16,11 +16,13 @@ cuprate-types = { path = "../../types" } rayon = { workspace = true } futures = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tower = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } hex = { workspace = true } -[lints] -workspace = true +clap = { workspace = true, features = ["default", "derive"] } + +#[lints] +#workspace = true diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 0b6cd3b4b..a9af7d720 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -2,6 +2,73 @@ //! //! Will contain the chain manager and syncer. +use crate::blockchain::manager::BlockchainManager; +use crate::blockchain::types::{ + ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, + ConsensusBlockchainReadHandle, +}; +use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::{ + BlockChainContextService, BlockVerifierService, ContextConfig, TxVerifierService, +}; +use cuprate_p2p::block_downloader::BlockDownloaderConfig; +use cuprate_p2p::NetworkInterface; +use cuprate_p2p_core::ClearNet; +use tokio::sync::mpsc; + mod manager; mod syncer; mod types; + +pub async fn init_consensus( + blockchain_read_handle: BlockchainReadHandle, + context_config: ContextConfig, +) -> Result< + ( + ConcreteBlockVerifierService, + ConcreteTxVerifierService, + BlockChainContextService, + ), + tower::BoxError, +> { + let ctx_service = cuprate_consensus::initialize_blockchain_context( + context_config, + ConsensusBlockchainReadHandle(blockchain_read_handle.clone()), + ) + .await?; + + let (block_verifier_svc, tx_verifier_svc) = cuprate_consensus::initialize_verifier( + ConsensusBlockchainReadHandle(blockchain_read_handle), + ctx_service.clone(), + ); + + Ok((block_verifier_svc, tx_verifier_svc, ctx_service)) +} + +pub fn init_blockchain_manager( + clearnet_interface: NetworkInterface, + block_downloader_config: BlockDownloaderConfig, + blockchain_write_handle: BlockchainWriteHandle, + blockchain_read_handle: BlockchainReadHandle, + blockchain_context_service: BlockChainContextService, + block_verifier_service: ConcreteBlockVerifierService, +) { + let (batch_tx, batch_rx) = mpsc::channel(1); + + tokio::spawn(syncer::syncer( + blockchain_context_service.clone(), + ChainService(blockchain_read_handle.clone()), + clearnet_interface, + batch_tx, + block_downloader_config, + )); + + let manager = BlockchainManager::new( + blockchain_write_handle, + blockchain_read_handle, + blockchain_context_service, + block_verifier_service, + ); + + tokio::spawn(manager.run(batch_rx)); +} diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index c9f9b06b5..5a526a590 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,14 +1,57 @@ mod batch_handler; +use crate::blockchain::manager::batch_handler::handle_incoming_block_batch; +use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::{BlockChainContextService, BlockVerifierService, TxVerifierService}; +use cuprate_p2p::block_downloader::BlockBatch; +use futures::StreamExt; +use tokio::sync::mpsc::Receiver; -struct BlockchainManager { +pub struct BlockchainManager { blockchain_write_handle: BlockchainWriteHandle, + blockchain_read_handle: BlockchainReadHandle, blockchain_context_service: BlockChainContextService, block_verifier_service: BlockVerifierService< BlockChainContextService, - TxVerifierService, - BlockchainReadHandle, + TxVerifierService, + ConsensusBlockchainReadHandle, >, } + +impl BlockchainManager { + pub const fn new( + blockchain_write_handle: BlockchainWriteHandle, + blockchain_read_handle: BlockchainReadHandle, + blockchain_context_service: BlockChainContextService, + block_verifier_service: BlockVerifierService< + BlockChainContextService, + TxVerifierService, + ConsensusBlockchainReadHandle, + >, + ) -> Self { + Self { + blockchain_write_handle, + blockchain_read_handle, + blockchain_context_service, + block_verifier_service, + } + } + + pub async fn run(mut self, mut batch_rx: Receiver) { + tokio::select! { + Some(batch) = batch_rx.recv() => { + handle_incoming_block_batch( + batch, + &mut self.block_verifier_service, + &mut self.blockchain_context_service, + &mut self.blockchain_read_handle, + &mut self.blockchain_write_handle + ).await; + } + else => { + todo!("Exit the BC manager") + } + } + } +} diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index 21c367b82..dc7381239 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -14,7 +14,7 @@ use cuprate_p2p_core::ClearNet; /// An error returned from the [`syncer`]. #[derive(Debug, thiserror::Error)] -enum SyncerError { +pub enum SyncerError { #[error("Incoming block channel closed.")] IncomingBlockChannelClosed, #[error("One of our services returned an error: {0}.")] @@ -58,7 +58,7 @@ where tracing::debug!("Waiting for new sync info in top sync channel"); while let Some(top_sync_info) = peer_sync_watch.next().await { - tracing::debug!( + tracing::info!( "New sync info seen, top height: {}, top block hash: {}", top_sync_info.chain_height, hex::encode(top_sync_info.top_hash) diff --git a/binaries/cuprated/src/blockchain/types.rs b/binaries/cuprated/src/blockchain/types.rs index 0387a7df7..46576a46d 100644 --- a/binaries/cuprated/src/blockchain/types.rs +++ b/binaries/cuprated/src/blockchain/types.rs @@ -1,13 +1,23 @@ use cuprate_blockchain::cuprate_database::RuntimeError; use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_consensus::{BlockChainContextService, BlockVerifierService, TxVerifierService}; +use cuprate_p2p::block_downloader::{ChainSvcRequest, ChainSvcResponse}; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use futures::future::MapErr; -use futures::TryFutureExt; +use futures::future::{BoxFuture, MapErr}; +use futures::{FutureExt, TryFutureExt}; use std::task::{Context, Poll}; use tower::Service; +pub type ConcreteBlockVerifierService = BlockVerifierService< + BlockChainContextService, + TxVerifierService, + ConsensusBlockchainReadHandle, +>; + +pub type ConcreteTxVerifierService = TxVerifierService; + #[derive(Clone)] -pub struct ConsensusBlockchainReadHandle(BlockchainReadHandle); +pub struct ConsensusBlockchainReadHandle(pub BlockchainReadHandle); impl Service for ConsensusBlockchainReadHandle { type Response = BlockchainResponse; @@ -25,3 +35,62 @@ impl Service for ConsensusBlockchainReadHandle { self.0.call(req).map_err(Into::into) } } + +#[derive(Clone)] +pub struct ChainService(pub BlockchainReadHandle); + +impl Service for ChainService { + type Response = ChainSvcResponse; + type Error = tower::BoxError; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.0.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: ChainSvcRequest) -> Self::Future { + let map_res = |res: BlockchainResponse| match res { + BlockchainResponse::CompactChainHistory { + block_ids, + cumulative_difficulty, + } => ChainSvcResponse::CompactHistory { + block_ids, + cumulative_difficulty, + }, + BlockchainResponse::FindFirstUnknown(res) => ChainSvcResponse::FindFirstUnknown(res), + _ => panic!("Blockchain returned wrong response"), + }; + + match req { + ChainSvcRequest::CompactHistory => self + .0 + .call(BlockchainReadRequest::CompactChainHistory) + .map_ok(map_res) + .map_err(Into::into) + .boxed(), + ChainSvcRequest::FindFirstUnknown(req) => self + .0 + .call(BlockchainReadRequest::FindFirstUnknown(req)) + .map_ok(map_res) + .map_err(Into::into) + .boxed(), + ChainSvcRequest::CumulativeDifficulty => self + .0 + .call(BlockchainReadRequest::CompactChainHistory) + .map_ok(|res| { + // TODO create a custom request instead of hijacking this one. + let BlockchainResponse::CompactChainHistory { + cumulative_difficulty, + .. + } = res + else { + panic!("Blockchain returned wrong response"); + }; + + ChainSvcResponse::CumulativeDifficulty(cumulative_difficulty) + }) + .map_err(Into::into) + .boxed(), + } + } +} diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 918429c9a..4d205a15b 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -1,9 +1,32 @@ +use clap::Parser; + mod blockchain; mod config; mod p2p; mod rpc; mod txpool; +#[derive(Parser)] +struct Args {} fn main() { - todo!() + let _args = Args::parse(); + + let (bc_read_handle, bc_write_handle, _) = + cuprate_blockchain::service::init(cuprate_blockchain::config::Config::default()).unwrap(); + + let async_rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + async_rt.block_on(async move { + let (block_verifier, tx_verifier, context_svc) = blockchain::init_consensus( + bc_read_handle, + cuprate_consensus::ContextConfig::main_net(), + ) + .await + .unwrap(); + + //blockchain::init_blockchain_manager() + }); } diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 3b7f2ae11..2c69e6677 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -50,16 +50,13 @@ pub enum ExtendedConsensusError { } /// Initialize the 2 verifier [`tower::Service`]s (block and transaction). -pub async fn initialize_verifier( +pub fn initialize_verifier( database: D, ctx_svc: Ctx, -) -> Result< - ( - BlockVerifierService, D>, - TxVerifierService, - ), - ConsensusError, -> +) -> ( + BlockVerifierService, D>, + TxVerifierService, +) where D: Database + Clone + Send + Sync + 'static, D::Future: Send + 'static, @@ -75,7 +72,7 @@ where { let tx_svc = TxVerifierService::new(database.clone()); let block_svc = BlockVerifierService::new(ctx_svc, tx_svc.clone(), database); - Ok((block_svc, tx_svc)) + (block_svc, tx_svc) } use __private::Database; From f25588d3489e69d1b8f75da6bc821afca5c1ef26 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 23 Aug 2024 23:55:54 +0100 Subject: [PATCH 05/46] very rough block manager --- .idea/vcs.xml | 6 + .idea/workspace.xml | 637 ++++++++++++++++++ Cargo.lock | 80 ++- Cargo.toml | 6 +- binaries/cuprated/Cargo.toml | 8 + binaries/cuprated/src/blockchain.rs | 54 +- binaries/cuprated/src/blockchain/manager.rs | 26 +- .../src/blockchain/manager/batch_handler.rs | 29 +- binaries/cuprated/src/main.rs | 43 +- binaries/cuprated/src/p2p.rs | 26 +- binaries/cuprated/src/p2p/core_sync_svc.rs | 51 ++ binaries/cuprated/src/p2p/request_handler.rs | 32 + consensus/src/block.rs | 2 +- consensus/src/lib.rs | 1 + consensus/src/transactions.rs | 4 + consensus/src/transactions/free.rs | 3 +- p2p/p2p-core/src/lib.rs | 1 + p2p/p2p/src/config.rs | 3 +- p2p/p2p/src/lib.rs | 2 +- p2p_state.bin | Bin 0 -> 162918 bytes 20 files changed, 965 insertions(+), 49 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 binaries/cuprated/src/p2p/core_sync_svc.rs create mode 100644 p2p_state.bin diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..8ddf4f0ee --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,637 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "associatedIndex": 4 +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1722869508802 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 58b66fe6c..22e7fdd0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -939,6 +939,7 @@ dependencies = [ "clap", "cuprate-blockchain", "cuprate-consensus", + "cuprate-cryptonight", "cuprate-p2p", "cuprate-p2p-core", "cuprate-types", @@ -949,6 +950,7 @@ dependencies = [ "tokio", "tower", "tracing", + "tracing-subscriber", ] [[package]] @@ -983,7 +985,7 @@ dependencies = [ [[package]] name = "dalek-ff-group" version = "0.4.1" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "crypto-bigint", "curve25519-dalek", @@ -1138,7 +1140,7 @@ dependencies = [ [[package]] name = "flexible-transcript" version = "0.3.2" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "blake2", "digest", @@ -1802,7 +1804,7 @@ dependencies = [ [[package]] name = "monero-address" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-io", @@ -1815,7 +1817,7 @@ dependencies = [ [[package]] name = "monero-borromean" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1828,7 +1830,7 @@ dependencies = [ [[package]] name = "monero-bulletproofs" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1843,7 +1845,7 @@ dependencies = [ [[package]] name = "monero-clsag" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "dalek-ff-group", @@ -1863,7 +1865,7 @@ dependencies = [ [[package]] name = "monero-generators" version = "0.4.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "dalek-ff-group", @@ -1877,7 +1879,7 @@ dependencies = [ [[package]] name = "monero-io" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "std-shims", @@ -1886,7 +1888,7 @@ dependencies = [ [[package]] name = "monero-mlsag" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1900,7 +1902,7 @@ dependencies = [ [[package]] name = "monero-primitives" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1913,7 +1915,7 @@ dependencies = [ [[package]] name = "monero-rpc" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "async-trait", "curve25519-dalek", @@ -1930,7 +1932,7 @@ dependencies = [ [[package]] name = "monero-serai" version = "0.1.4-alpha" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "hex-literal", @@ -1948,7 +1950,7 @@ dependencies = [ [[package]] name = "monero-simple-request-rpc" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "async-trait", "digest_auth", @@ -1958,6 +1960,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2005,6 +2017,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "page_size" version = "0.6.0" @@ -2607,6 +2625,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2619,7 +2646,7 @@ dependencies = [ [[package]] name = "simple-request" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "http-body-util", "hyper", @@ -2675,7 +2702,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "std-shims" version = "0.1.1" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "hashbrown", "spin", @@ -2974,6 +3001,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", ] [[package]] @@ -2982,7 +3021,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", "tracing-core", + "tracing-log", ] [[package]] @@ -3062,6 +3106,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 06b49a0a8..e1f068eb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ futures = { version = "0.3.29", default-features = false } hex = { version = "0.4.3", default-features = false } hex-literal = { version = "0.4", default-features = false } indexmap = { version = "2.2.5", default-features = false } -monero-serai = { git = "https://github.com/Cuprate/serai.git", rev = "d5205ce", default-features = false } +monero-serai = { git = "https://github.com/Cuprate/serai.git", rev = "50686e8", default-features = false } paste = { version = "1.0.14", default-features = false } pin-project = { version = "1.1.3", default-features = false } randomx-rs = { git = "https://github.com/Cuprate/randomx-rs.git", rev = "0028464", default-features = false } @@ -85,8 +85,8 @@ tracing-subscriber = { version = "0.3.17", default-features = false } tracing = { version = "0.1.40", default-features = false } ## workspace.dev-dependencies -monero-rpc = { git = "https://github.com/Cuprate/serai.git", rev = "d5205ce" } -monero-simple-request-rpc = { git = "https://github.com/Cuprate/serai.git", rev = "d5205ce" } +monero-rpc = { git = "https://github.com/Cuprate/serai.git", rev = "50686e8" } +monero-simple-request-rpc = { git = "https://github.com/Cuprate/serai.git", rev = "50686e8" } tempfile = { version = "3" } pretty_assertions = { version = "1.4.0" } proptest = { version = "1" } diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index 74eb2d599..c01e2cca8 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -13,6 +13,7 @@ cuprate-blockchain = { path = "../../storage/blockchain" } cuprate-p2p = { path = "../../p2p/p2p" } cuprate-p2p-core = { path = "../../p2p/p2p-core" } cuprate-types = { path = "../../types" } +cuprate-cryptonight = { path = "../../cryptonight" } rayon = { workspace = true } futures = { workspace = true } @@ -23,6 +24,13 @@ thiserror = { workspace = true } hex = { workspace = true } clap = { workspace = true, features = ["default", "derive"] } +tracing-subscriber = { workspace = true, features = ["default"] } #[lints] #workspace = true + +[profile.dev] +panic = 'abort' + +[profile.release] +panic = 'abort' diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index a9af7d720..c0b7e6cef 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -8,18 +8,64 @@ use crate::blockchain::types::{ ConsensusBlockchainReadHandle, }; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::{ - BlockChainContextService, BlockVerifierService, ContextConfig, TxVerifierService, -}; +use cuprate_consensus::{generate_genesis_block, BlockChainContextService, ContextConfig}; +use cuprate_cryptonight::cryptonight_hash_v0; use cuprate_p2p::block_downloader::BlockDownloaderConfig; use cuprate_p2p::NetworkInterface; -use cuprate_p2p_core::ClearNet; +use cuprate_p2p_core::{ClearNet, Network}; +use cuprate_types::blockchain::{ + BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, +}; +use cuprate_types::VerifiedBlockInformation; use tokio::sync::mpsc; +use tower::{Service, ServiceExt}; mod manager; mod syncer; mod types; +pub async fn check_add_genesis( + blockchain_read_handle: &mut BlockchainReadHandle, + blockchain_write_handle: &mut BlockchainWriteHandle, + network: &Network, +) { + if blockchain_read_handle + .ready() + .await + .unwrap() + .call(BlockchainReadRequest::ChainHeight) + .await + .is_ok() + { + return; + } + + let genesis = generate_genesis_block(network); + + blockchain_write_handle + .ready() + .await + .unwrap() + .call(BlockchainWriteRequest::WriteBlock( + VerifiedBlockInformation { + block_blob: genesis.serialize(), + txs: vec![], + block_hash: genesis.hash(), + pow_hash: cryptonight_hash_v0(&genesis.serialize_pow_hash()), + height: 0, + generated_coins: genesis.miner_transaction.prefix().outputs[0] + .amount + .unwrap(), + weight: genesis.miner_transaction.weight(), + long_term_weight: genesis.miner_transaction.weight(), + cumulative_difficulty: 1, + block: genesis, + }, + )) + .await + .unwrap(); +} + pub async fn init_consensus( blockchain_read_handle: BlockchainReadHandle, context_config: ContextConfig, diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 5a526a590..e13c9073d 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -39,18 +39,20 @@ impl BlockchainManager { } pub async fn run(mut self, mut batch_rx: Receiver) { - tokio::select! { - Some(batch) = batch_rx.recv() => { - handle_incoming_block_batch( - batch, - &mut self.block_verifier_service, - &mut self.blockchain_context_service, - &mut self.blockchain_read_handle, - &mut self.blockchain_write_handle - ).await; - } - else => { - todo!("Exit the BC manager") + loop { + tokio::select! { + Some(batch) = batch_rx.recv() => { + handle_incoming_block_batch( + batch, + &mut self.block_verifier_service, + &mut self.blockchain_context_service, + &mut self.blockchain_read_handle, + &mut self.blockchain_write_handle + ).await; + } + else => { + todo!("TODO: exit the BC manager") + } } } } diff --git a/binaries/cuprated/src/blockchain/manager/batch_handler.rs b/binaries/cuprated/src/blockchain/manager/batch_handler.rs index f080f3100..c4a3d6e58 100644 --- a/binaries/cuprated/src/blockchain/manager/batch_handler.rs +++ b/binaries/cuprated/src/blockchain/manager/batch_handler.rs @@ -41,6 +41,16 @@ pub async fn handle_incoming_block_batch( .first() .expect("Block batch should not be empty"); + handle_incoming_block_batch_main_chain( + batch, + block_verifier_service, + blockchain_context_service, + blockchain_write_handle, + ) + .await; + + // TODO: alt block to the DB + /* match blockchain_read_handle .oneshot(BlockchainReadRequest::FindBlock( first_block.header.previous, @@ -67,6 +77,8 @@ pub async fn handle_incoming_block_batch( Ok(_) => panic!("Blockchain service returned incorrect response"), } + + */ } async fn handle_incoming_block_batch_main_chain( @@ -90,7 +102,12 @@ async fn handle_incoming_block_batch_main_chain( + 'static, TxV::Future: Send + 'static, { - let Ok(VerifyBlockResponse::MainChainBatchPrepped(prepped)) = block_verifier_service + info!( + "Handling batch to main chain height: {}", + batch.blocks.first().unwrap().0.number().unwrap() + ); + + let VerifyBlockResponse::MainChainBatchPrepped(prepped) = block_verifier_service .ready() .await .expect("TODO") @@ -98,21 +115,21 @@ async fn handle_incoming_block_batch_main_chain( blocks: batch.blocks, }) .await + .unwrap() else { - info!("Error verifying batch, banning peer"); - todo!() + panic!("Incorrect response!"); }; for (block, txs) in prepped { - let Ok(VerifyBlockResponse::MainChain(verified_block)) = block_verifier_service + let VerifyBlockResponse::MainChain(verified_block) = block_verifier_service .ready() .await .expect("TODO") .call(VerifyBlockRequest::MainChainPrepped { block, txs }) .await + .unwrap() else { - info!("Error verifying batch, banning peer"); - todo!() + panic!("Incorrect response!"); }; blockchain_context_service diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 4d205a15b..5ccb8382f 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -1,4 +1,10 @@ +use crate::blockchain::check_add_genesis; use clap::Parser; +use cuprate_p2p::block_downloader::BlockDownloaderConfig; +use cuprate_p2p::P2PConfig; +use cuprate_p2p_core::Network; +use std::time::Duration; +use tracing::Level; mod blockchain; mod config; @@ -11,7 +17,11 @@ struct Args {} fn main() { let _args = Args::parse(); - let (bc_read_handle, bc_write_handle, _) = + tracing_subscriber::fmt() + .with_max_level(Level::DEBUG) + .init(); + + let (mut bc_read_handle, mut bc_write_handle, _) = cuprate_blockchain::service::init(cuprate_blockchain::config::Config::default()).unwrap(); let async_rt = tokio::runtime::Builder::new_multi_thread() @@ -20,13 +30,38 @@ fn main() { .unwrap(); async_rt.block_on(async move { - let (block_verifier, tx_verifier, context_svc) = blockchain::init_consensus( - bc_read_handle, + check_add_genesis(&mut bc_read_handle, &mut bc_write_handle, &Network::Mainnet).await; + + let (block_verifier, _tx_verifier, context_svc) = blockchain::init_consensus( + bc_read_handle.clone(), cuprate_consensus::ContextConfig::main_net(), ) .await .unwrap(); - //blockchain::init_blockchain_manager() + let net = cuprate_p2p::initialize_network( + p2p::request_handler::P2pProtocolRequestHandler, + p2p::core_sync_svc::CoreSyncService(context_svc.clone()), + p2p::dummy_config(), + ) + .await + .unwrap(); + + blockchain::init_blockchain_manager( + net, + BlockDownloaderConfig { + buffer_size: 50_000_000, + in_progress_queue_size: 50_000_000, + check_client_pool_interval: Duration::from_secs(45), + target_batch_size: 10_000_000, + initial_batch_size: 1, + }, + bc_write_handle, + bc_read_handle, + context_svc, + block_verifier, + ); + + tokio::time::sleep(Duration::MAX).await; }); } diff --git a/binaries/cuprated/src/p2p.rs b/binaries/cuprated/src/p2p.rs index f5b72ba3a..0560320b1 100644 --- a/binaries/cuprated/src/p2p.rs +++ b/binaries/cuprated/src/p2p.rs @@ -2,4 +2,28 @@ //! //! Will handle initiating the P2P and contains a protocol request handler. -mod request_handler; +use cuprate_p2p::AddressBookConfig; +use cuprate_p2p_core::Network; +use std::time::Duration; + +pub mod core_sync_svc; +pub mod request_handler; + +pub fn dummy_config() -> cuprate_p2p::P2PConfig { + cuprate_p2p::P2PConfig { + network: Network::Mainnet, + outbound_connections: 64, + extra_outbound_connections: 0, + max_inbound_connections: 0, + gray_peers_percent: 0.7, + server_config: None, + p2p_port: 0, + rpc_port: 0, + address_book_config: AddressBookConfig { + max_white_list_length: 1000, + max_gray_list_length: 5000, + peer_store_file: "p2p_state.bin".into(), + peer_save_period: Duration::from_secs(60), + }, + } +} diff --git a/binaries/cuprated/src/p2p/core_sync_svc.rs b/binaries/cuprated/src/p2p/core_sync_svc.rs new file mode 100644 index 000000000..34c91e477 --- /dev/null +++ b/binaries/cuprated/src/p2p/core_sync_svc.rs @@ -0,0 +1,51 @@ +use cuprate_blockchain::cuprate_database::RuntimeError; +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_consensus::{ + BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, +}; +use cuprate_p2p_core::services::{CoreSyncDataRequest, CoreSyncDataResponse}; +use cuprate_p2p_core::CoreSyncData; +use cuprate_types::blockchain::BlockchainReadRequest; +use futures::future::{BoxFuture, MapErr, MapOk}; +use futures::{FutureExt, TryFutureExt}; +use std::task::{Context, Poll}; +use tower::Service; + +#[derive(Clone)] +pub struct CoreSyncService(pub BlockChainContextService); + +impl Service for CoreSyncService { + type Response = CoreSyncDataResponse; + type Error = tower::BoxError; + type Future = MapOk< + >::Future, + fn(BlockChainContextResponse) -> CoreSyncDataResponse, + >; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.0.poll_ready(cx) + } + + fn call(&mut self, _: CoreSyncDataRequest) -> Self::Future { + self.0 + .call(BlockChainContextRequest::GetContext) + .map_ok(|res| { + let BlockChainContextResponse::Context(ctx) = res else { + panic!("blockchain context service returned wrong response."); + }; + + let raw_ctx = ctx.unchecked_blockchain_context(); + + // TODO: the hardfork here should be the version of the top block not the current HF, + // on HF boundaries these will be different. + CoreSyncDataResponse(CoreSyncData::new( + raw_ctx.cumulative_difficulty, + // TODO: + raw_ctx.chain_height as u64, + 0, + raw_ctx.top_hash, + raw_ctx.current_hf.as_u8(), + )) + }) + } +} diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index 8b1378917..76554e929 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -1 +1,33 @@ +use cuprate_p2p_core::{ProtocolRequest, ProtocolResponse}; +use futures::future::BoxFuture; +use futures::FutureExt; +use std::task::{Context, Poll}; +use tower::Service; +use tracing::trace; +#[derive(Clone)] +pub struct P2pProtocolRequestHandler; + +impl Service for P2pProtocolRequestHandler { + type Response = ProtocolResponse; + type Error = tower::BoxError; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: ProtocolRequest) -> Self::Future { + match req { + ProtocolRequest::GetObjects(_) => trace!("TODO: GetObjects"), + ProtocolRequest::GetChain(_) => trace!("TODO: GetChain"), + ProtocolRequest::FluffyMissingTxs(_) => trace!("TODO: FluffyMissingTxs"), + ProtocolRequest::GetTxPoolCompliment(_) => trace!("TODO: GetTxPoolCompliment"), + ProtocolRequest::NewBlock(_) => trace!("TODO: NewBlock"), + ProtocolRequest::NewFluffyBlock(_) => trace!("TODO: NewFluffyBlock"), + ProtocolRequest::NewTransactions(_) => trace!("TODO: NewTransactions"), + } + + async { Ok(ProtocolResponse::NA) }.boxed() + } +} diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 7297a5a6a..b33b58577 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -123,7 +123,7 @@ impl PreparedBlock { /// /// The randomX VM must be Some if RX is needed or this will panic. /// The randomX VM must also be initialised with the correct seed. - fn new( + pub fn new( block: Block, randomx_vm: Option<&R>, ) -> Result { diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 2c69e6677..29f590388 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -27,6 +27,7 @@ pub use context::{ pub use transactions::{TxVerifierService, VerifyTxRequest, VerifyTxResponse}; // re-export. +pub use cuprate_consensus_rules::genesis::generate_genesis_block; pub use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; /// An Error returned from one of the consensus services. diff --git a/consensus/src/transactions.rs b/consensus/src/transactions.rs index 91de67cdf..82d3100e6 100644 --- a/consensus/src/transactions.rs +++ b/consensus/src/transactions.rs @@ -393,6 +393,10 @@ async fn verify_transactions_decoy_info( where D: Database + Clone + Sync + Send + 'static, { + if hf == HardFork::V1 { + return Ok(()); + } + batch_get_decoy_info(&txs, hf, database) .await? .try_for_each(|decoy_info| decoy_info.and_then(|di| Ok(check_decoy_info(&di, &hf)?)))?; diff --git a/consensus/src/transactions/free.rs b/consensus/src/transactions/free.rs index 02c523581..67b675a46 100644 --- a/consensus/src/transactions/free.rs +++ b/consensus/src/transactions/free.rs @@ -78,7 +78,8 @@ pub fn tx_fee(tx: &Transaction) -> Result { } for output in &prefix.outputs { - fee.checked_sub(output.amount.unwrap_or(0)) + fee = fee + .checked_sub(output.amount.unwrap_or(0)) .ok_or(TransactionError::OutputsTooHigh)?; } } diff --git a/p2p/p2p-core/src/lib.rs b/p2p/p2p-core/src/lib.rs index 83cc4d2e8..1f7e631ac 100644 --- a/p2p/p2p-core/src/lib.rs +++ b/p2p/p2p-core/src/lib.rs @@ -79,6 +79,7 @@ pub use protocol::*; use services::*; //re-export pub use cuprate_helper::network::Network; +pub use cuprate_wire::CoreSyncData; /// The direction of a connection. #[derive(Debug, Copy, Clone, Eq, PartialEq)] diff --git a/p2p/p2p/src/config.rs b/p2p/p2p/src/config.rs index 90d7f8ffe..98bef68e5 100644 --- a/p2p/p2p/src/config.rs +++ b/p2p/p2p/src/config.rs @@ -1,8 +1,9 @@ -use cuprate_address_book::AddressBookConfig; use cuprate_helper::network::Network; use cuprate_p2p_core::NetworkZone; use cuprate_wire::{common::PeerSupportFlags, BasicNodeData}; +pub use cuprate_address_book::AddressBookConfig; + /// P2P config. #[derive(Clone, Debug)] pub struct P2PConfig { diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index 82ecfce73..6f95948fc 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -33,7 +33,7 @@ mod sync_states; use block_downloader::{BlockBatch, BlockDownloaderConfig, ChainSvcRequest, ChainSvcResponse}; pub use broadcast::{BroadcastRequest, BroadcastSvc}; use client_pool::ClientPoolDropGuard; -pub use config::P2PConfig; +pub use config::{AddressBookConfig, P2PConfig}; use connection_maintainer::MakeConnectionRequest; /// Initializes the P2P [`NetworkInterface`] for a specific [`NetworkZone`]. diff --git a/p2p_state.bin b/p2p_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..bf2d673dd8c3bca6473f8a64e5c96f5145cd2331 GIT binary patch literal 162918 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 AQ2+n{ literal 0 HcmV?d00001 From 1c93ea14b46b033fe77568edcbe270bf4c8a98ab Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 29 Aug 2024 01:58:10 +0100 Subject: [PATCH 06/46] misc changes --- binaries/cuprated/Cargo.toml | 4 ++-- binaries/cuprated/src/blockchain.rs | 25 +++++++++++---------- binaries/cuprated/src/config.rs | 20 +++++++++++++++++ binaries/cuprated/src/main.rs | 34 +++++++++++++++++++---------- consensus/src/transactions.rs | 2 +- 5 files changed, 58 insertions(+), 27 deletions(-) diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index c01e2cca8..a04c0be26 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -30,7 +30,7 @@ tracing-subscriber = { workspace = true, features = ["default"] } #workspace = true [profile.dev] -panic = 'abort' +panic = "abort" [profile.release] -panic = 'abort' +panic = "abort" diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index c0b7e6cef..366f9620a 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -2,28 +2,29 @@ //! //! Will contain the chain manager and syncer. -use crate::blockchain::manager::BlockchainManager; -use crate::blockchain::types::{ - ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, - ConsensusBlockchainReadHandle, -}; +use tokio::sync::mpsc; +use tower::{Service, ServiceExt}; + use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::{generate_genesis_block, BlockChainContextService, ContextConfig}; use cuprate_cryptonight::cryptonight_hash_v0; -use cuprate_p2p::block_downloader::BlockDownloaderConfig; -use cuprate_p2p::NetworkInterface; +use cuprate_p2p::{block_downloader::BlockDownloaderConfig, NetworkInterface}; use cuprate_p2p_core::{ClearNet, Network}; -use cuprate_types::blockchain::{ - BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, +use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainWriteRequest}, + VerifiedBlockInformation, }; -use cuprate_types::VerifiedBlockInformation; -use tokio::sync::mpsc; -use tower::{Service, ServiceExt}; mod manager; mod syncer; mod types; +use manager::BlockchainManager; +use types::{ + ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, + ConsensusBlockchainReadHandle, +}; + pub async fn check_add_genesis( blockchain_read_handle: &mut BlockchainReadHandle, blockchain_write_handle: &mut BlockchainWriteHandle, diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index d613c1fcc..1fd02f74b 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -1 +1,21 @@ //! cuprated config + +use cuprate_blockchain::config::{ + Config as BlockchainConfig, ConfigBuilder as BlockchainConfigBuilder, +}; + +pub fn config() -> CupratedConfig { + // TODO: read config options from the conf files & cli args. + + CupratedConfig {} +} + +pub struct CupratedConfig { + // TODO: expose config options we want to allow changing. +} + +impl CupratedConfig { + pub fn blockchain_config(&self) -> BlockchainConfig { + BlockchainConfigBuilder::new().fast().build() + } +} diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 5ccb8382f..023c3a81c 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -1,9 +1,11 @@ use crate::blockchain::check_add_genesis; +use crate::config::CupratedConfig; use clap::Parser; use cuprate_p2p::block_downloader::BlockDownloaderConfig; use cuprate_p2p::P2PConfig; use cuprate_p2p_core::Network; use std::time::Duration; +use tokio::runtime::Runtime; use tracing::Level; mod blockchain; @@ -12,22 +14,15 @@ mod p2p; mod rpc; mod txpool; -#[derive(Parser)] -struct Args {} fn main() { - let _args = Args::parse(); + let config = config::config(); - tracing_subscriber::fmt() - .with_max_level(Level::DEBUG) - .init(); + init_log(&config); let (mut bc_read_handle, mut bc_write_handle, _) = - cuprate_blockchain::service::init(cuprate_blockchain::config::Config::default()).unwrap(); + cuprate_blockchain::service::init(config.blockchain_config()).unwrap(); - let async_rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); + let async_rt = init_tokio_rt(&config); async_rt.block_on(async move { check_add_genesis(&mut bc_read_handle, &mut bc_write_handle, &Network::Mainnet).await; @@ -62,6 +57,21 @@ fn main() { block_verifier, ); - tokio::time::sleep(Duration::MAX).await; + futures::future::pending::<()>().await; }); + + // TODO: add command handling. +} + +fn init_log(_config: &CupratedConfig) { + tracing_subscriber::fmt() + .with_max_level(Level::DEBUG) + .init(); +} + +fn init_tokio_rt(_config: &CupratedConfig) -> Runtime { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() } diff --git a/consensus/src/transactions.rs b/consensus/src/transactions.rs index 82d3100e6..23f7a4d18 100644 --- a/consensus/src/transactions.rs +++ b/consensus/src/transactions.rs @@ -393,7 +393,7 @@ async fn verify_transactions_decoy_info( where D: Database + Clone + Sync + Send + 'static, { - if hf == HardFork::V1 { + if hf == HardFork::V1 || txs.is_empty() { return Ok(()); } From 05d0cf2295e759801a67f12f5e190c7d0dfc5fdf Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 29 Aug 2024 16:04:17 +0100 Subject: [PATCH 07/46] move more config values --- binaries/cuprated/src/blockchain.rs | 7 +++-- binaries/cuprated/src/config.rs | 41 +++++++++++++++++++++++++++++ binaries/cuprated/src/main.rs | 34 +++++++++--------------- binaries/cuprated/src/p2p.rs | 23 ---------------- 4 files changed, 58 insertions(+), 47 deletions(-) diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 366f9620a..e1f5765bf 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -1,7 +1,6 @@ //! Blockchain //! //! Will contain the chain manager and syncer. - use tokio::sync::mpsc; use tower::{Service, ServiceExt}; @@ -25,11 +24,13 @@ use types::{ ConsensusBlockchainReadHandle, }; +/// Checks if the genesis block is in the blockchain and adds it if not. pub async fn check_add_genesis( blockchain_read_handle: &mut BlockchainReadHandle, blockchain_write_handle: &mut BlockchainWriteHandle, network: &Network, ) { + // Try to get the chain height, will fail if the genesis block is not in the DB. if blockchain_read_handle .ready() .await @@ -67,6 +68,7 @@ pub async fn check_add_genesis( .unwrap(); } +/// Initializes the consensus services. pub async fn init_consensus( blockchain_read_handle: BlockchainReadHandle, context_config: ContextConfig, @@ -92,13 +94,14 @@ pub async fn init_consensus( Ok((block_verifier_svc, tx_verifier_svc, ctx_service)) } +/// Initializes the blockchain manager task and syncer. pub fn init_blockchain_manager( clearnet_interface: NetworkInterface, - block_downloader_config: BlockDownloaderConfig, blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, blockchain_context_service: BlockChainContextService, block_verifier_service: ConcreteBlockVerifierService, + block_downloader_config: BlockDownloaderConfig, ) { let (batch_tx, batch_rx) = mpsc::channel(1); diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index 1fd02f74b..c71c40c87 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -1,8 +1,12 @@ //! cuprated config +use std::time::Duration; use cuprate_blockchain::config::{ Config as BlockchainConfig, ConfigBuilder as BlockchainConfigBuilder, }; +use cuprate_consensus::ContextConfig; +use cuprate_p2p::{block_downloader::BlockDownloaderConfig, AddressBookConfig, P2PConfig}; +use cuprate_p2p_core::{ClearNet, Network}; pub fn config() -> CupratedConfig { // TODO: read config options from the conf files & cli args. @@ -18,4 +22,41 @@ impl CupratedConfig { pub fn blockchain_config(&self) -> BlockchainConfig { BlockchainConfigBuilder::new().fast().build() } + + pub fn clearnet_config(&self) -> P2PConfig { + P2PConfig { + network: Network::Mainnet, + outbound_connections: 64, + extra_outbound_connections: 0, + max_inbound_connections: 0, + gray_peers_percent: 0.7, + server_config: None, + p2p_port: 0, + rpc_port: 0, + address_book_config: AddressBookConfig { + max_white_list_length: 1000, + max_gray_list_length: 5000, + peer_store_file: "p2p_state.bin".into(), + peer_save_period: Duration::from_secs(60), + }, + } + } + + pub fn block_downloader_config(&self) -> BlockDownloaderConfig { + BlockDownloaderConfig { + buffer_size: 50_000_000, + in_progress_queue_size: 50_000_000, + check_client_pool_interval: Duration::from_secs(45), + target_batch_size: 10_000_000, + initial_batch_size: 1, + } + } + + pub fn network(&self) -> Network { + Network::Mainnet + } + + pub fn context_config(&self) -> ContextConfig { + ContextConfig::main_net() + } } diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 023c3a81c..87fc7aa5b 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -1,10 +1,3 @@ -use crate::blockchain::check_add_genesis; -use crate::config::CupratedConfig; -use clap::Parser; -use cuprate_p2p::block_downloader::BlockDownloaderConfig; -use cuprate_p2p::P2PConfig; -use cuprate_p2p_core::Network; -use std::time::Duration; use tokio::runtime::Runtime; use tracing::Level; @@ -14,6 +7,9 @@ mod p2p; mod rpc; mod txpool; +use blockchain::check_add_genesis; +use config::CupratedConfig; + fn main() { let config = config::config(); @@ -25,38 +21,32 @@ fn main() { let async_rt = init_tokio_rt(&config); async_rt.block_on(async move { - check_add_genesis(&mut bc_read_handle, &mut bc_write_handle, &Network::Mainnet).await; + check_add_genesis(&mut bc_read_handle, &mut bc_write_handle, &config.network()).await; - let (block_verifier, _tx_verifier, context_svc) = blockchain::init_consensus( - bc_read_handle.clone(), - cuprate_consensus::ContextConfig::main_net(), - ) - .await - .unwrap(); + let (block_verifier, _tx_verifier, context_svc) = + blockchain::init_consensus(bc_read_handle.clone(), config.context_config()) + .await + .unwrap(); let net = cuprate_p2p::initialize_network( p2p::request_handler::P2pProtocolRequestHandler, p2p::core_sync_svc::CoreSyncService(context_svc.clone()), - p2p::dummy_config(), + config.clearnet_config(), ) .await .unwrap(); blockchain::init_blockchain_manager( net, - BlockDownloaderConfig { - buffer_size: 50_000_000, - in_progress_queue_size: 50_000_000, - check_client_pool_interval: Duration::from_secs(45), - target_batch_size: 10_000_000, - initial_batch_size: 1, - }, bc_write_handle, bc_read_handle, context_svc, block_verifier, + config.block_downloader_config(), ); + // TODO: this can be removed as long as the main thread does not exit, so when command handling + // is added futures::future::pending::<()>().await; }); diff --git a/binaries/cuprated/src/p2p.rs b/binaries/cuprated/src/p2p.rs index 0560320b1..7715be7c3 100644 --- a/binaries/cuprated/src/p2p.rs +++ b/binaries/cuprated/src/p2p.rs @@ -2,28 +2,5 @@ //! //! Will handle initiating the P2P and contains a protocol request handler. -use cuprate_p2p::AddressBookConfig; -use cuprate_p2p_core::Network; -use std::time::Duration; - pub mod core_sync_svc; pub mod request_handler; - -pub fn dummy_config() -> cuprate_p2p::P2PConfig { - cuprate_p2p::P2PConfig { - network: Network::Mainnet, - outbound_connections: 64, - extra_outbound_connections: 0, - max_inbound_connections: 0, - gray_peers_percent: 0.7, - server_config: None, - p2p_port: 0, - rpc_port: 0, - address_book_config: AddressBookConfig { - max_white_list_length: 1000, - max_gray_list_length: 5000, - peer_store_file: "p2p_state.bin".into(), - peer_save_period: Duration::from_secs(60), - }, - } -} From d6488719661f82461a24351ab41bfa2fcf3e2eb9 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 29 Aug 2024 18:44:34 +0100 Subject: [PATCH 08/46] add new tables & types --- storage/blockchain/src/lib.rs | 2 +- storage/blockchain/src/tables.rs | 25 ++++++-- storage/blockchain/src/types.rs | 101 ++++++++++++++++++++++++++++++- types/src/types.rs | 3 +- 4 files changed, 123 insertions(+), 8 deletions(-) diff --git a/storage/blockchain/src/lib.rs b/storage/blockchain/src/lib.rs index e544a69e9..0dea345b3 100644 --- a/storage/blockchain/src/lib.rs +++ b/storage/blockchain/src/lib.rs @@ -52,7 +52,7 @@ unused_crate_dependencies, unused_doc_comments, unused_mut, - missing_docs, + //missing_docs, deprecated, unused_comparisons, nonstandard_style diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index 122ac31b4..6db768167 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -16,11 +16,7 @@ //! accessing _all_ tables defined here at once. //---------------------------------------------------------------------------------------------------- Import -use crate::types::{ - Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, - Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, - TxId, UnlockTime, -}; +use crate::types::{Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, TxId, UnlockTime, RawChainId, AltChainInfo, AltBlockHeight, CompactAltBlockInfo, AltTransactionInfo}; //---------------------------------------------------------------------------------------------------- Tables // Notes: @@ -129,6 +125,25 @@ cuprate_database::define_tables! { /// Transactions without unlock times will not exist in this table. 14 => TxUnlockTime, TxId => UnlockTime, + + 15 => AltChainInfos, + RawChainId => AltChainInfo, + + 16 => AltBlockHeights, + BlockHash => AltBlockHeight, + + 17 => AltBlocksInfo, + AltBlockHeight => CompactAltBlockInfo, + + 18 => AltBlockBlobs, + AltBlockHeight => BlockBlob, + + 19 => AltTransactionBlobs, + TxHash => TxBlob, + + 20 => AltTransactionInfos, + TxHash => AltTransactionInfo, + } //---------------------------------------------------------------------------------------------------- Tests diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index eb1dc6479..73c7614f3 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -41,13 +41,15 @@ #![forbid(unsafe_code)] // if you remove this line i will steal your monero //---------------------------------------------------------------------------------------------------- Import +use std::num::NonZero; + use bytemuck::{Pod, Zeroable}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use cuprate_database::{Key, StorableVec}; - +use cuprate_types::{Chain, ChainId}; //---------------------------------------------------------------------------------------------------- Aliases // These type aliases exist as many Monero-related types are the exact same. // For clarity, they're given type aliases as to not confuse them. @@ -324,6 +326,103 @@ pub struct RctOutput { } // TODO: local_index? +//---------------------------------------------------------------------------------------------------- RawChain +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(transparent)] +pub struct RawChain(u64); + +impl From for RawChain { + fn from(value: Chain) -> Self { + match value { + Chain::Main => RawChain(0), + Chain::Alt(chain_id) => RawChain(chain_id.0.get()), + } + } +} + +impl From for Chain { + fn from(value: RawChain) -> Self { + NonZero::new(value.0) + .map(|id| Chain::Alt(ChainId(id))) + .unwrap_or(Chain::Main) + } +} + +//---------------------------------------------------------------------------------------------------- RawChainId +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(transparent)] +pub struct RawChainId(u64); + +impl From for RawChainId { + fn from(value: ChainId) -> Self { + RawChainId(value.0.get()) + } +} + +impl From for ChainId { + fn from(value: RawChainId) -> Self { + ChainId(NonZero::new(value.0).expect("RawChainId mut not have a value of `0`")) + } +} + +impl Key for RawChainId {} + +//---------------------------------------------------------------------------------------------------- AltChainInfo +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltChainInfo { + parent_chain: RawChain, + common_ancestor_height: u64 +} + +//---------------------------------------------------------------------------------------------------- AltBlockHeight +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltBlockHeight { + chain_id: u64, + height: u64, +} + +impl Key for AltBlockHeight {} + +//---------------------------------------------------------------------------------------------------- CompactAltBlockInfo +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct CompactAltBlockInfo { + /// The block's hash. + /// + /// [`Block::hash`]. + pub block_hash: [u8; 32], + /// The block's proof-of-work hash. + pub pow_hash: [u8; 32], + /// The block's height. + pub height: u64, + /// The adjusted block size, in bytes. + pub weight: usize, + /// The long term block weight, which is the weight factored in with previous block weights. + pub long_term_weight: usize, + /// The cumulative difficulty of all blocks up until and including this block. + pub cumulative_difficulty_low: u64, + pub cumulative_difficulty_high: u64, + +} + +//---------------------------------------------------------------------------------------------------- AltTransactionInfo +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] +#[repr(C)] +pub struct AltTransactionInfo { + /// The transaction's weight. + /// + /// [`Transaction::weight`]. + pub tx_weight: usize, + /// The transaction's total fees. + pub fee: u64, + /// The transaction's hash. + /// + /// [`Transaction::hash`]. + pub tx_hash: [u8; 32], +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/types/src/types.rs b/types/src/types.rs index 4b6e2e126..da4422a15 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -1,5 +1,6 @@ //! Various shared data types in Cuprate. +use std::num::NonZero; //---------------------------------------------------------------------------------------------------- Import use curve25519_dalek::edwards::EdwardsPoint; use monero_serai::{ @@ -97,7 +98,7 @@ pub struct VerifiedBlockInformation { /// /// The inner value is meaningless. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct ChainId(pub u64); +pub struct ChainId(pub NonZero); //---------------------------------------------------------------------------------------------------- Chain /// An identifier for a chain. From e1ae84836911acab79fe3eb853725ca94fc4ff3e Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 30 Aug 2024 03:01:23 +0100 Subject: [PATCH 09/46] add function to fully add an alt block --- storage/blockchain/src/ops/alt_block.rs | 104 ++++++++++++++++++++++++ storage/blockchain/src/ops/mod.rs | 1 + storage/blockchain/src/tables.rs | 7 +- storage/blockchain/src/types.rs | 19 +++-- 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 storage/blockchain/src/ops/alt_block.rs diff --git a/storage/blockchain/src/ops/alt_block.rs b/storage/blockchain/src/ops/alt_block.rs new file mode 100644 index 000000000..d04899efc --- /dev/null +++ b/storage/blockchain/src/ops/alt_block.rs @@ -0,0 +1,104 @@ +use bytemuck::TransparentWrapper; + +use cuprate_database::{DatabaseRw, RuntimeError, StorableVec, DatabaseRo}; +use cuprate_helper::map::split_u128_into_low_high_bits; +use cuprate_types::{AltBlockInformation, Chain, VerifiedTransactionInformation}; + +use crate::{ + tables::TablesMut, + types::{AltBlockHeight, AltChainInfo, AltTransactionInfo, BlockHash, CompactAltBlockInfo}, +}; + +pub fn add_alt_block( + alt_block: &AltBlockInformation, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + let alt_block_height = AltBlockHeight { + chain_id: alt_block.chain_id.into(), + height: alt_block.height, + }; + + tables + .alt_block_heights_mut() + .put(&alt_block.block_hash, &alt_block_height)?; + + check_add_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?; + + let (cumulative_difficulty_low, cumulative_difficulty_high) = + split_u128_into_low_high_bits(alt_block.cumulative_difficulty); + + let alt_block_info = CompactAltBlockInfo { + block_hash: alt_block.block_hash, + pow_hash: alt_block.pow_hash, + height: alt_block.height, + weight: alt_block.weight, + long_term_weight: alt_block.long_term_weight, + cumulative_difficulty_low, + cumulative_difficulty_high, + }; + + tables + .alt_blocks_info_mut() + .put(&alt_block_height, &alt_block_info)?; + + tables.alt_block_blobs_mut().put( + &alt_block_height, + StorableVec::wrap_ref(&alt_block.block_blob), + )?; + + for tx in &alt_block.txs { + add_alt_transaction(&tx, tables)?; + } + + Ok(()) +} + +pub fn add_alt_transaction( + tx: &VerifiedTransactionInformation, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + if tables.tx_ids().get(&tx.tx_hash).is_ok() + || tables.alt_transaction_infos().get(&tx.tx_hash).is_ok() + { + return Ok(()); + } + + tables.alt_transaction_infos_mut().put( + &tx.tx_hash, + &AltTransactionInfo { + tx_weight: tx.tx_weight, + fee: tx.fee, + tx_hash: tx.tx_hash, + }, + )?; + + tables + .alt_transaction_blobs_mut() + .put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob)) +} + +pub fn check_add_alt_chain_info( + alt_block_height: &AltBlockHeight, + prev_hash: &BlockHash, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + match tables.alt_chain_infos().get(&alt_block_height.chain_id) { + Ok(_) => return Ok(()), + Err(RuntimeError::KeyNotFound) => (), + Err(e) => return Err(e), + } + + let parent_chain = match tables.alt_block_heights().get(prev_hash) { + Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()), + Err(RuntimeError::KeyNotFound) => Chain::Main, + Err(e) => return Err(e), + }; + + tables.alt_chain_infos_mut().put( + &alt_block_height.chain_id, + &AltChainInfo { + parent_chain: parent_chain.into(), + common_ancestor_height: alt_block_height.height - 1, + }, + ) +} diff --git a/storage/blockchain/src/ops/mod.rs b/storage/blockchain/src/ops/mod.rs index 4ff7dff1e..1ec9c237a 100644 --- a/storage/blockchain/src/ops/mod.rs +++ b/storage/blockchain/src/ops/mod.rs @@ -108,5 +108,6 @@ pub mod key_image; pub mod output; pub mod property; pub mod tx; +pub mod alt_block; mod macros; diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index 6db768167..381430d73 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -16,7 +16,12 @@ //! accessing _all_ tables defined here at once. //---------------------------------------------------------------------------------------------------- Import -use crate::types::{Amount, AmountIndex, AmountIndices, BlockBlob, BlockHash, BlockHeight, BlockInfo, KeyImage, Output, PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RctOutput, TxBlob, TxHash, TxId, UnlockTime, RawChainId, AltChainInfo, AltBlockHeight, CompactAltBlockInfo, AltTransactionInfo}; +use crate::types::{ + AltBlockHeight, AltChainInfo, AltTransactionInfo, Amount, AmountIndex, AmountIndices, + BlockBlob, BlockHash, BlockHeight, BlockInfo, CompactAltBlockInfo, KeyImage, Output, + PreRctOutputId, PrunableBlob, PrunableHash, PrunedBlob, RawChainId, RctOutput, TxBlob, TxHash, + TxId, UnlockTime, +}; //---------------------------------------------------------------------------------------------------- Tables // Notes: diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index 73c7614f3..88ece10b0 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -348,6 +348,14 @@ impl From for Chain { } } +impl From for RawChain { + fn from(value: RawChainId) -> Self { + assert_ne!(value.0, 0); + + RawChain(value.0) + } +} + //---------------------------------------------------------------------------------------------------- RawChainId #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(transparent)] @@ -371,16 +379,16 @@ impl Key for RawChainId {} #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct AltChainInfo { - parent_chain: RawChain, - common_ancestor_height: u64 + pub parent_chain: RawChain, + pub common_ancestor_height: usize, } //---------------------------------------------------------------------------------------------------- AltBlockHeight #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct AltBlockHeight { - chain_id: u64, - height: u64, + pub chain_id: RawChainId, + pub height: usize, } impl Key for AltBlockHeight {} @@ -396,7 +404,7 @@ pub struct CompactAltBlockInfo { /// The block's proof-of-work hash. pub pow_hash: [u8; 32], /// The block's height. - pub height: u64, + pub height: usize, /// The adjusted block size, in bytes. pub weight: usize, /// The long term block weight, which is the weight factored in with previous block weights. @@ -404,7 +412,6 @@ pub struct CompactAltBlockInfo { /// The cumulative difficulty of all blocks up until and including this block. pub cumulative_difficulty_low: u64, pub cumulative_difficulty_high: u64, - } //---------------------------------------------------------------------------------------------------- AltTransactionInfo From ed887a7c859d45fce68cbf4e70f56e4b46449492 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 30 Aug 2024 23:06:30 +0100 Subject: [PATCH 10/46] resolve current todo!s --- storage/blockchain/src/lib.rs | 2 + storage/blockchain/src/ops/alt_block.rs | 137 ++++++++++++++++++++++-- storage/blockchain/src/ops/block.rs | 10 +- storage/blockchain/src/ops/mod.rs | 2 +- storage/blockchain/src/service/read.rs | 43 +++++++- 5 files changed, 178 insertions(+), 16 deletions(-) diff --git a/storage/blockchain/src/lib.rs b/storage/blockchain/src/lib.rs index 0dea345b3..8a6f96b6c 100644 --- a/storage/blockchain/src/lib.rs +++ b/storage/blockchain/src/lib.rs @@ -98,6 +98,8 @@ clippy::too_many_lines ) )] +extern crate core; + // Only allow building 64-bit targets. // // This allows us to assume 64-bit diff --git a/storage/blockchain/src/ops/alt_block.rs b/storage/blockchain/src/ops/alt_block.rs index d04899efc..82a37ba8e 100644 --- a/storage/blockchain/src/ops/alt_block.rs +++ b/storage/blockchain/src/ops/alt_block.rs @@ -1,12 +1,21 @@ -use bytemuck::TransparentWrapper; +use std::cmp::max; -use cuprate_database::{DatabaseRw, RuntimeError, StorableVec, DatabaseRo}; -use cuprate_helper::map::split_u128_into_low_high_bits; -use cuprate_types::{AltBlockInformation, Chain, VerifiedTransactionInformation}; +use bytemuck::TransparentWrapper; +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; +use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; +use cuprate_types::{ + AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork, + VerifiedTransactionInformation, +}; +use monero_serai::block::BlockHeader; use crate::{ - tables::TablesMut, - types::{AltBlockHeight, AltChainInfo, AltTransactionInfo, BlockHash, CompactAltBlockInfo}, + ops::block::{get_block_extended_header_from_height, get_block_info}, + tables::{Tables, TablesMut}, + types::{ + AltBlockHeight, AltChainInfo, AltTransactionInfo, BlockHash, BlockHeight, + CompactAltBlockInfo, + }, }; pub fn add_alt_block( @@ -102,3 +111,119 @@ pub fn check_add_alt_chain_info( }, ) } + +pub fn alt_block_hash( + block_height: &BlockHeight, + alt_chain: ChainId, + tables: &mut impl Tables, +) -> Result { + let alt_chains = tables.alt_chain_infos(); + + let original_chain = { + let mut chain = alt_chain.into(); + loop { + let chain_info = alt_chains.get(&chain)?; + + if chain_info.common_ancestor_height < *block_height { + break Chain::Alt(chain.into()); + } + + match chain_info.parent_chain.into() { + Chain::Main => break Chain::Main, + Chain::Alt(alt_chain_id) => { + chain = alt_chain_id.into(); + continue; + } + } + } + }; + + match original_chain { + Chain::Main => { + get_block_info(&block_height, tables.block_infos()).map(|info| info.block_hash) + } + Chain::Alt(chain_id) => tables + .alt_blocks_info() + .get(&AltBlockHeight { + chain_id: chain_id.into(), + height: *block_height, + }) + .map(|info| info.block_hash), + } +} + +pub fn alt_extended_headers_in_range( + range: std::ops::Range, + alt_chain: ChainId, + tables: &impl Tables, +) -> Result, RuntimeError> { + // TODO: this function does not use rayon, however it probably should. + + let mut ranges = Vec::with_capacity(5); + let alt_chains = tables.alt_chain_infos(); + + let mut i = range.end; + let mut current_chain_id = alt_chain.into(); + while i > range.start { + let chain_info = alt_chains.get(¤t_chain_id)?; + + let start_height = max(range.start, chain_info.common_ancestor_height + 1); + + ranges.push((chain_info.parent_chain.into(), start_height..i)); + i = chain_info.common_ancestor_height; + + match chain_info.parent_chain.into() { + Chain::Main => { + ranges.push((Chain::Main, range.start..i)); + break; + } + Chain::Alt(alt_chain_id) => { + current_chain_id = alt_chain_id.into(); + continue; + } + } + } + + let res = ranges + .into_iter() + .rev() + .map(|(chain, range)| { + range.into_iter().map(move |height| match chain { + Chain::Main => get_block_extended_header_from_height(&height, tables), + Chain::Alt(chain_id) => get_alt_block_extended_header_from_height( + &AltBlockHeight { + chain_id: chain_id.into(), + height, + }, + tables, + ), + }) + }) + .flatten() + .collect::>()?; + + Ok(res) +} + +pub fn get_alt_block_extended_header_from_height( + height: &AltBlockHeight, + table: &impl Tables, +) -> Result { + let block_info = table.alt_blocks_info().get(height)?; + + let block_blob = table.alt_block_blobs().get(height)?.0; + + let block_header = BlockHeader::read(&mut block_blob.as_slice())?; + + Ok(ExtendedBlockHeader { + version: HardFork::from_version(0).expect("Block in DB must have correct version"), + vote: block_header.hardfork_version, + timestamp: block_header.timestamp, + cumulative_difficulty: combine_low_high_bits_to_u128( + block_info.cumulative_difficulty_low, + block_info.cumulative_difficulty_high, + ), + block_weight: block_info.weight, + long_term_weight: block_info.long_term_weight, + }) +} diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 4f77d736e..2e110fedf 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -2,7 +2,7 @@ //---------------------------------------------------------------------------------------------------- Import use bytemuck::TransparentWrapper; -use monero_serai::block::Block; +use monero_serai::block::{Block, BlockHeader}; use cuprate_database::{ RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, @@ -190,7 +190,7 @@ pub fn get_block_extended_header_from_height( ) -> Result { let block_info = tables.block_infos().get(block_height)?; let block_blob = tables.block_blobs().get(block_height)?.0; - let block = Block::read(&mut block_blob.as_slice())?; + let block_header = BlockHeader::read(&mut block_blob.as_slice())?; let cumulative_difficulty = combine_low_high_bits_to_u128( block_info.cumulative_difficulty_low, @@ -201,10 +201,10 @@ pub fn get_block_extended_header_from_height( #[allow(clippy::cast_possible_truncation)] Ok(ExtendedBlockHeader { cumulative_difficulty, - version: HardFork::from_version(block.header.hardfork_version) + version: HardFork::from_version(block_header.hardfork_version) .expect("Stored block must have a valid hard-fork"), - vote: block.header.hardfork_signal, - timestamp: block.header.timestamp, + vote: block_header.hardfork_signal, + timestamp: block_header.timestamp, block_weight: block_info.weight as usize, long_term_weight: block_info.long_term_weight as usize, }) diff --git a/storage/blockchain/src/ops/mod.rs b/storage/blockchain/src/ops/mod.rs index 1ec9c237a..8a8f0f158 100644 --- a/storage/blockchain/src/ops/mod.rs +++ b/storage/blockchain/src/ops/mod.rs @@ -102,12 +102,12 @@ //! # Ok(()) } //! ``` +pub mod alt_block; pub mod block; pub mod blockchain; pub mod key_image; pub mod output; pub mod property; pub mod tx; -pub mod alt_block; mod macros; diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 207da4163..eef40b5e5 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -22,6 +22,7 @@ use cuprate_types::{ use crate::{ ops::{ + alt_block::{alt_block_hash, alt_extended_headers_in_range}, block::{ block_exists, get_block_extended_header_from_height, get_block_height, get_block_info, }, @@ -33,7 +34,7 @@ use crate::{ free::{compact_history_genesis_not_included, compact_history_index_to_height_offset}, types::{BlockchainReadHandle, ResponseResult}, }, - tables::{BlockHeights, BlockInfos, OpenTables, Tables}, + tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables}, types::{Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId}, }; @@ -87,7 +88,7 @@ fn map_request( match request { R::BlockExtendedHeader(block) => block_extended_header(env, block), R::BlockHash(block, chain) => block_hash(env, block, chain), - R::FindBlock(_) => todo!("Add alt blocks to DB"), + R::FindBlock(block_hash) => find_block(env, block_hash), R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes), R::BlockExtendedHeaderInRange(range, chain) => { block_extended_header_in_range(env, range, chain) @@ -198,12 +199,39 @@ fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> Res let block_hash = match chain { Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash, - Chain::Alt(_) => todo!("Add alt blocks to DB"), + Chain::Alt(chain) => { + alt_block_hash(&block_height, chain, &mut env_inner.open_tables(&tx_ro)?)? + } }; Ok(BlockchainResponse::BlockHash(block_hash)) } +/// [`BlockchainReadRequest::FindBlock`] +fn find_block(env: &ConcreteEnv, block_hash: BlockHash) -> ResponseResult { + // Single-threaded, no `ThreadLocal` required. + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + + let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; + + // Check the main chain first. + match table_block_heights.get(&block_hash) { + Ok(height) => return Ok(BlockchainResponse::FindBlock(Some((Chain::Main, height)))), + Err(RuntimeError::KeyNotFound) => (), + Err(e) => return Err(e), + } + + let table_alt_block_heights = env_inner.open_db_ro::(&tx_ro)?; + + let height = table_alt_block_heights.get(&block_hash)?; + + Ok(BlockchainResponse::FindBlock(Some(( + Chain::Alt(height.chain_id.into()), + height.height, + )))) +} + /// [`BlockchainReadRequest::FilterUnknownHashes`]. #[inline] fn filter_unknown_hashes(env: &ConcreteEnv, mut hashes: HashSet) -> ResponseResult { @@ -254,7 +282,14 @@ fn block_extended_header_in_range( get_block_extended_header_from_height(&block_height, tables) }) .collect::, RuntimeError>>()?, - Chain::Alt(_) => todo!("Add alt blocks to DB"), + Chain::Alt(chain_id) => { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + alt_extended_headers_in_range( + range, + chain_id, + get_tables!(env_inner, tx_ro, tables)?.as_ref(), + )? + } }; Ok(BlockchainResponse::BlockExtendedHeaderInRange(vec)) From bc619b61eb06d54647dbba14530ebba6e110697f Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 31 Aug 2024 01:22:30 +0100 Subject: [PATCH 11/46] add new requests --- storage/blockchain/src/service/write.rs | 4 +++ types/src/blockchain.rs | 42 +++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 816afc4f5..a2cc71c7b 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -29,6 +29,10 @@ fn handle_blockchain_request( ) -> Result { match req { BlockchainWriteRequest::WriteBlock(block) => write_block(env, block), + BlockchainWriteRequest::WriteAltBlock(_) => todo!(), + BlockchainWriteRequest::StartReorg(_) => todo!(), + BlockchainWriteRequest::ReverseReorg(_) => todo!(), + BlockchainWriteRequest::FlushAltBlocks => todo!(), } } diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index b502c3fa8..48eab292e 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -8,7 +8,7 @@ use std::{ collections::{HashMap, HashSet}, ops::Range, }; - +use crate::{AltBlockInformation, ChainId}; use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; //---------------------------------------------------------------------------------------------------- ReadRequest @@ -112,6 +112,27 @@ pub enum BlockchainWriteRequest { /// /// Input is an already verified block. WriteBlock(VerifiedBlockInformation), + /// Write an alternative block to the database, + /// + /// Input is the alternative block. + WriteAltBlock(AltBlockInformation), + /// A request to start the re-org process. + /// + /// The inner value is the [`ChainId`] of the alt-chain we want to re-org to. + /// + /// This will: + /// - pop blocks from the main chain + /// - retrieve all alt-blocks in this alt-chain + /// - flush all other alt blocks + StartReorg(ChainId), + /// A request to reverse the re-org process. + /// + /// The inner value is the [`ChainId`] of the old main chain. + /// + /// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::StartReorg`]. + ReverseReorg(ChainId), + /// A request to flush all alternative blocks. + FlushAltBlocks, } //---------------------------------------------------------------------------------------------------- Response @@ -198,11 +219,20 @@ pub enum BlockchainResponse { FindFirstUnknown(Option<(usize, usize)>), //------------------------------------------------------ Writes - /// Response to [`BlockchainWriteRequest::WriteBlock`]. - /// - /// This response indicates that the requested block has - /// successfully been written to the database without error. - WriteBlockOk, + /// A generic Ok response to indicate a request was successfully handled. + /// + /// currently the response for: + /// - [`BlockchainWriteRequest::WriteBlock`] + /// - [`BlockchainWriteRequest::ReverseReorg`] + /// - [`BlockchainWriteRequest::FlushAltBlocks`] + Ok, + /// The response for [`BlockchainWriteRequest::StartReorg`]. + StartReorg { + /// The [`ChainId`] of the old main chain blocks that were popped. + old_main_chain_id: ChainId, + /// The next alt chain blocks. + alt_chain: Vec + }, } //---------------------------------------------------------------------------------------------------- Tests From 029f439f0b5e48230c34fb7598a2f7d4a149f11d Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 1 Sep 2024 02:15:16 +0100 Subject: [PATCH 12/46] WIP: starting re-orgs --- .../ops/{alt_block.rs => alt_block/block.rs} | 134 ++++++------------ storage/blockchain/src/ops/alt_block/chain.rs | 63 ++++++++ storage/blockchain/src/ops/alt_block/mod.rs | 7 + storage/blockchain/src/ops/alt_block/tx.rs | 35 +++++ storage/blockchain/src/service/read.rs | 6 +- storage/blockchain/src/service/write.rs | 57 +++++++- storage/blockchain/src/tables.rs | 4 - types/src/blockchain.rs | 7 +- types/src/types.rs | 5 +- 9 files changed, 214 insertions(+), 104 deletions(-) rename storage/blockchain/src/ops/{alt_block.rs => alt_block/block.rs} (61%) create mode 100644 storage/blockchain/src/ops/alt_block/chain.rs create mode 100644 storage/blockchain/src/ops/alt_block/mod.rs create mode 100644 storage/blockchain/src/ops/alt_block/tx.rs diff --git a/storage/blockchain/src/ops/alt_block.rs b/storage/blockchain/src/ops/alt_block/block.rs similarity index 61% rename from storage/blockchain/src/ops/alt_block.rs rename to storage/blockchain/src/ops/alt_block/block.rs index 82a37ba8e..171cdd7b5 100644 --- a/storage/blockchain/src/ops/alt_block.rs +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -1,22 +1,20 @@ -use std::cmp::max; - +use crate::ops::alt_block::{ + add_alt_transaction_blob, check_add_alt_chain_info, get_alt_chain_history_ranges, + get_alt_transaction_blob, +}; +use crate::ops::block::{get_block_extended_header_from_height, get_block_info}; +use crate::tables::{Tables, TablesMut}; +use crate::types::{ + AltBlockHeight, AltTransactionInfo, BlockHash, BlockHeight, CompactAltBlockInfo, +}; use bytemuck::TransparentWrapper; -use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; +use cuprate_database::{RuntimeError, StorableVec}; use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; use cuprate_types::{ AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork, VerifiedTransactionInformation, }; -use monero_serai::block::BlockHeader; - -use crate::{ - ops::block::{get_block_extended_header_from_height, get_block_info}, - tables::{Tables, TablesMut}, - types::{ - AltBlockHeight, AltChainInfo, AltTransactionInfo, BlockHash, BlockHeight, - CompactAltBlockInfo, - }, -}; +use monero_serai::block::{Block, BlockHeader}; pub fn add_alt_block( alt_block: &AltBlockInformation, @@ -55,64 +53,48 @@ pub fn add_alt_block( StorableVec::wrap_ref(&alt_block.block_blob), )?; - for tx in &alt_block.txs { - add_alt_transaction(&tx, tables)?; + assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len()); + for (tx, tx_hash) in alt_block.txs.iter().zip(&alt_block.block.transactions) { + add_alt_transaction_blob(tx_hash, StorableVec::wrap_ref(tx), tables)?; } Ok(()) } -pub fn add_alt_transaction( - tx: &VerifiedTransactionInformation, - tables: &mut impl TablesMut, -) -> Result<(), RuntimeError> { - if tables.tx_ids().get(&tx.tx_hash).is_ok() - || tables.alt_transaction_infos().get(&tx.tx_hash).is_ok() - { - return Ok(()); - } - - tables.alt_transaction_infos_mut().put( - &tx.tx_hash, - &AltTransactionInfo { - tx_weight: tx.tx_weight, - fee: tx.fee, - tx_hash: tx.tx_hash, - }, - )?; - - tables - .alt_transaction_blobs_mut() - .put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob)) -} - -pub fn check_add_alt_chain_info( +pub fn get_alt_block( alt_block_height: &AltBlockHeight, - prev_hash: &BlockHash, - tables: &mut impl TablesMut, -) -> Result<(), RuntimeError> { - match tables.alt_chain_infos().get(&alt_block_height.chain_id) { - Ok(_) => return Ok(()), - Err(RuntimeError::KeyNotFound) => (), - Err(e) => return Err(e), - } - - let parent_chain = match tables.alt_block_heights().get(prev_hash) { - Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()), - Err(RuntimeError::KeyNotFound) => Chain::Main, - Err(e) => return Err(e), - }; - - tables.alt_chain_infos_mut().put( - &alt_block_height.chain_id, - &AltChainInfo { - parent_chain: parent_chain.into(), - common_ancestor_height: alt_block_height.height - 1, - }, - ) + tables: &impl Tables, +) -> Result { + let block_info = tables.alt_blocks_info().get(alt_block_height)?; + + let block_blob = tables.alt_block_blobs().get(alt_block_height)?.0; + + let block = Block::read(&mut block_blob.as_slice())?; + + let txs = block + .transactions + .iter() + .map(|tx_hash| get_alt_transaction_blob(tx_hash, tables)) + .collect()?; + + Ok(AltBlockInformation { + block, + block_blob, + txs, + block_hash: block_info.block_hash, + pow_hash: block_info.pow_hash, + height: block_info.height, + weight: block_info.weight, + long_term_weight: block_info.long_term_weight, + cumulative_difficulty: combine_low_high_bits_to_u128( + block_info.cumulative_difficulty_low, + block_info.cumulative_difficulty_high, + ), + chain_id: alt_block_height.chain_id.into(), + }) } -pub fn alt_block_hash( +pub fn get_alt_block_hash( block_height: &BlockHeight, alt_chain: ChainId, tables: &mut impl Tables, @@ -152,37 +134,15 @@ pub fn alt_block_hash( } } -pub fn alt_extended_headers_in_range( +pub fn get_alt_extended_headers_in_range( range: std::ops::Range, alt_chain: ChainId, tables: &impl Tables, ) -> Result, RuntimeError> { // TODO: this function does not use rayon, however it probably should. - let mut ranges = Vec::with_capacity(5); let alt_chains = tables.alt_chain_infos(); - - let mut i = range.end; - let mut current_chain_id = alt_chain.into(); - while i > range.start { - let chain_info = alt_chains.get(¤t_chain_id)?; - - let start_height = max(range.start, chain_info.common_ancestor_height + 1); - - ranges.push((chain_info.parent_chain.into(), start_height..i)); - i = chain_info.common_ancestor_height; - - match chain_info.parent_chain.into() { - Chain::Main => { - ranges.push((Chain::Main, range.start..i)); - break; - } - Chain::Alt(alt_chain_id) => { - current_chain_id = alt_chain_id.into(); - continue; - } - } - } + let ranges = get_alt_chain_history_ranges(range, alt_chain, alt_chains)?; let res = ranges .into_iter() diff --git a/storage/blockchain/src/ops/alt_block/chain.rs b/storage/blockchain/src/ops/alt_block/chain.rs new file mode 100644 index 000000000..4259d4dc9 --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/chain.rs @@ -0,0 +1,63 @@ +use crate::tables::{AltChainInfos, TablesMut}; +use crate::types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight}; +use cuprate_database::{DatabaseRo, RuntimeError}; +use cuprate_types::{Chain, ChainId}; +use std::cmp::max; + +pub fn check_add_alt_chain_info( + alt_block_height: &AltBlockHeight, + prev_hash: &BlockHash, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + match tables.alt_chain_infos().get(&alt_block_height.chain_id) { + Ok(_) => return Ok(()), + Err(RuntimeError::KeyNotFound) => (), + Err(e) => return Err(e), + } + + let parent_chain = match tables.alt_block_heights().get(prev_hash) { + Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()), + Err(RuntimeError::KeyNotFound) => Chain::Main, + Err(e) => return Err(e), + }; + + tables.alt_chain_infos_mut().put( + &alt_block_height.chain_id, + &AltChainInfo { + parent_chain: parent_chain.into(), + common_ancestor_height: alt_block_height.height - 1, + }, + ) +} + +pub fn get_alt_chain_history_ranges( + range: std::ops::Range, + alt_chain: ChainId, + alt_chain_infos: &impl DatabaseRo, +) -> Result)>, RuntimeError> { + let mut ranges = Vec::with_capacity(5); + + let mut i = range.end; + let mut current_chain_id = alt_chain.into(); + while i > range.start { + let chain_info = alt_chain_infos.get(¤t_chain_id)?; + + let start_height = max(range.start, chain_info.common_ancestor_height + 1); + + ranges.push((chain_info.parent_chain.into(), start_height..i)); + i = chain_info.common_ancestor_height; + + match chain_info.parent_chain.into() { + Chain::Main => { + ranges.push((Chain::Main, range.start..i)); + break; + } + Chain::Alt(alt_chain_id) => { + current_chain_id = alt_chain_id.into(); + continue; + } + } + } + + Ok(ranges) +} diff --git a/storage/blockchain/src/ops/alt_block/mod.rs b/storage/blockchain/src/ops/alt_block/mod.rs new file mode 100644 index 000000000..8b2d1f172 --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/mod.rs @@ -0,0 +1,7 @@ +mod block; +mod chain; +mod tx; + +pub use block::*; +pub use chain::*; +pub use tx::*; diff --git a/storage/blockchain/src/ops/alt_block/tx.rs b/storage/blockchain/src/ops/alt_block/tx.rs new file mode 100644 index 000000000..aad4dc3dd --- /dev/null +++ b/storage/blockchain/src/ops/alt_block/tx.rs @@ -0,0 +1,35 @@ +use crate::tables::{Tables, TablesMut}; +use crate::types::{AltTransactionInfo, TxHash}; +use bytemuck::TransparentWrapper; +use cuprate_database::{RuntimeError, StorableVec}; +use cuprate_types::VerifiedTransactionInformation; + +pub fn add_alt_transaction_blob( + tx_hash: &TxHash, + tx_block: &StorableVec, + tables: &mut impl TablesMut, +) -> Result<(), RuntimeError> { + if tables.tx_ids().get(&tx_hash).is_ok() || tables.alt_transaction_blobs().get(&tx_hash).is_ok() + { + return Ok(()); + } + + tables.alt_transaction_blobs_mut().put(&tx_hash, tx_block) +} + +pub fn get_alt_transaction_blob( + tx_hash: &TxHash, + tables: &impl Tables, +) -> Result, RuntimeError> { + match tables.alt_transaction_blobs().get(tx_hash) { + Ok(blob) => Ok(blob.0), + Err(RuntimeError::KeyNotFound) => { + let tx_id = tables.tx_ids().get(tx_hash)?; + + let blob = tables.tx_blobs().get(&tx_id)?; + + Ok(blob.0) + } + Err(e) => return Err(e), + } +} diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index eef40b5e5..70da01b3e 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -22,7 +22,7 @@ use cuprate_types::{ use crate::{ ops::{ - alt_block::{alt_block_hash, alt_extended_headers_in_range}, + alt_block::{get_alt_block_hash, get_alt_extended_headers_in_range}, block::{ block_exists, get_block_extended_header_from_height, get_block_height, get_block_info, }, @@ -200,7 +200,7 @@ fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> Res let block_hash = match chain { Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash, Chain::Alt(chain) => { - alt_block_hash(&block_height, chain, &mut env_inner.open_tables(&tx_ro)?)? + get_alt_block_hash(&block_height, chain, &mut env_inner.open_tables(&tx_ro)?)? } }; @@ -284,7 +284,7 @@ fn block_extended_header_in_range( .collect::, RuntimeError>>()?, Chain::Alt(chain_id) => { let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; - alt_extended_headers_in_range( + get_alt_extended_headers_in_range( range, chain_id, get_tables!(env_inner, tx_ro, tables)?.as_ref(), diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index a2cc71c7b..067ba7f18 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -7,7 +7,7 @@ use cuprate_database::{ConcreteEnv, Env, EnvInner, RuntimeError, TxRw}; use cuprate_database_service::DatabaseWriteHandle; use cuprate_types::{ blockchain::{BlockchainResponse, BlockchainWriteRequest}, - VerifiedBlockInformation, + AltBlockInformation, VerifiedBlockInformation, }; use crate::{ @@ -29,10 +29,10 @@ fn handle_blockchain_request( ) -> Result { match req { BlockchainWriteRequest::WriteBlock(block) => write_block(env, block), - BlockchainWriteRequest::WriteAltBlock(_) => todo!(), + BlockchainWriteRequest::WriteAltBlock(alt_block) => write_alt_block(env, alt_block), BlockchainWriteRequest::StartReorg(_) => todo!(), BlockchainWriteRequest::ReverseReorg(_) => todo!(), - BlockchainWriteRequest::FlushAltBlocks => todo!(), + BlockchainWriteRequest::FlushAltBlocks => flush_alt_blocks(env), } } @@ -59,7 +59,56 @@ fn write_block(env: &ConcreteEnv, block: &VerifiedBlockInformation) -> ResponseR match result { Ok(()) => { TxRw::commit(tx_rw)?; - Ok(BlockchainResponse::WriteBlockOk) + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::WriteAltBlock`]. +#[inline] +fn write_alt_block(env: &ConcreteEnv, block: &AltBlockInformation) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw()?; + + let result = { + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + crate::ops::alt_block::add_alt_block(block, &mut tables_mut) + }; + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::FlushAltBlocks`]. +#[inline] +fn flush_alt_blocks(env: &ConcreteEnv) -> ResponseResult { + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw()?; + + let result = { crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw) }; + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) } Err(e) => { // INVARIANT: ensure database atomicity by aborting diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index 381430d73..deb957ea7 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -145,10 +145,6 @@ cuprate_database::define_tables! { 19 => AltTransactionBlobs, TxHash => TxBlob, - - 20 => AltTransactionInfos, - TxHash => AltTransactionInfo, - } //---------------------------------------------------------------------------------------------------- Tests diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 48eab292e..33c3e8bd4 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -4,12 +4,12 @@ //! responses are also tested in Cuprate's blockchain database crate. //---------------------------------------------------------------------------------------------------- Import +use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; +use crate::{AltBlockInformation, ChainId}; use std::{ collections::{HashMap, HashSet}, ops::Range, }; -use crate::{AltBlockInformation, ChainId}; -use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. @@ -223,6 +223,7 @@ pub enum BlockchainResponse { /// /// currently the response for: /// - [`BlockchainWriteRequest::WriteBlock`] + /// - [`BlockchainWriteRequest::WriteAltBlock`] /// - [`BlockchainWriteRequest::ReverseReorg`] /// - [`BlockchainWriteRequest::FlushAltBlocks`] Ok, @@ -231,7 +232,7 @@ pub enum BlockchainResponse { /// The [`ChainId`] of the old main chain blocks that were popped. old_main_chain_id: ChainId, /// The next alt chain blocks. - alt_chain: Vec + alt_chain: Vec, }, } diff --git a/types/src/types.rs b/types/src/types.rs index da4422a15..cc4543e67 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -39,8 +39,7 @@ pub struct ExtendedBlockHeader { //---------------------------------------------------------------------------------------------------- VerifiedTransactionInformation /// Verified information of a transaction. /// -/// - If this is in a [`VerifiedBlockInformation`] this represents a valid transaction -/// - If this is in an [`AltBlockInformation`] this represents a potentially valid transaction +/// This represents a valid transaction #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedTransactionInformation { /// The transaction itself. @@ -121,7 +120,7 @@ pub struct AltBlockInformation { /// [`Block::serialize`]. pub block_blob: Vec, /// All the transactions in the block, excluding the [`Block::miner_transaction`]. - pub txs: Vec, + pub txs: Vec>, /// The block's hash. /// /// [`Block::hash`]. From 6927b05f81c0de3a82a406d11ff365bb3a4aa826 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 6 Sep 2024 00:23:55 +0100 Subject: [PATCH 13/46] add last service request --- Cargo.lock | 1 + storage/blockchain/Cargo.toml | 3 +- storage/blockchain/src/free.rs | 32 +++++ storage/blockchain/src/ops/alt_block/block.rs | 21 ++-- storage/blockchain/src/ops/alt_block/chain.rs | 3 +- storage/blockchain/src/ops/alt_block/mod.rs | 17 +++ storage/blockchain/src/ops/alt_block/tx.rs | 44 +++++-- storage/blockchain/src/ops/block.rs | 61 ++++++--- storage/blockchain/src/service/free.rs | 37 +++++- storage/blockchain/src/service/write.rs | 118 +++++++++++++++++- storage/blockchain/src/tables.rs | 3 + storage/blockchain/src/types.rs | 5 +- types/src/blockchain.rs | 24 ++-- types/src/types.rs | 3 +- 14 files changed, 306 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77531897a..d5d64902a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,7 @@ dependencies = [ "monero-serai", "pretty_assertions", "proptest", + "rand", "rayon", "serde", "tempfile", diff --git a/storage/blockchain/Cargo.toml b/storage/blockchain/Cargo.toml index 7e79305ac..b03ef0386 100644 --- a/storage/blockchain/Cargo.toml +++ b/storage/blockchain/Cargo.toml @@ -25,11 +25,12 @@ cuprate-database = { path = "../database" } cuprate-database-service = { path = "../service" } cuprate-helper = { path = "../../helper", features = ["fs", "thread", "map"] } cuprate-types = { path = "../../types", features = ["blockchain"] } +cuprate-pruning = { path = "../../pruning" } bitflags = { workspace = true, features = ["std", "serde", "bytemuck"] } bytemuck = { workspace = true, features = ["must_cast", "derive", "min_const_generics", "extern_crate_alloc"] } curve25519-dalek = { workspace = true } -cuprate-pruning = { path = "../../pruning" } +rand = { workspace = true } monero-serai = { workspace = true, features = ["std"] } serde = { workspace = true, optional = true } diff --git a/storage/blockchain/src/free.rs b/storage/blockchain/src/free.rs index 8288e65f7..20d56226d 100644 --- a/storage/blockchain/src/free.rs +++ b/storage/blockchain/src/free.rs @@ -1,5 +1,6 @@ //! General free functions (related to the database). +use monero_serai::transaction::{Input, Transaction}; //---------------------------------------------------------------------------------------------------- Import use cuprate_database::{ConcreteEnv, Env, EnvInner, InitError, RuntimeError, TxRw}; @@ -61,6 +62,37 @@ pub fn open(config: Config) -> Result { Ok(env) } +//---------------------------------------------------------------------------------------------------- Tx Fee +/// Calculates the fee of the [`Transaction`]. +/// +/// # Panics +/// This will panic if the inputs overflow or the transaction outputs too much. +pub(crate) fn tx_fee(tx: &Transaction) -> u64 { + let mut fee = 0_u64; + + match &tx { + Transaction::V1 { prefix, .. } => { + for input in &prefix.inputs { + match input { + Input::Gen(_) => return 0, + Input::ToKey { amount, .. } => { + fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); + } + } + } + + for output in &prefix.outputs { + fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); + } + } + Transaction::V2 { proofs, .. } => { + fee = proofs.as_ref().unwrap().base.fee; + } + }; + + fee +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/storage/blockchain/src/ops/alt_block/block.rs b/storage/blockchain/src/ops/alt_block/block.rs index 171cdd7b5..a429eab84 100644 --- a/storage/blockchain/src/ops/alt_block/block.rs +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -1,19 +1,14 @@ use crate::ops::alt_block::{ add_alt_transaction_blob, check_add_alt_chain_info, get_alt_chain_history_ranges, - get_alt_transaction_blob, + get_alt_transaction, }; use crate::ops::block::{get_block_extended_header_from_height, get_block_info}; use crate::tables::{Tables, TablesMut}; -use crate::types::{ - AltBlockHeight, AltTransactionInfo, BlockHash, BlockHeight, CompactAltBlockInfo, -}; +use crate::types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo}; use bytemuck::TransparentWrapper; -use cuprate_database::{RuntimeError, StorableVec}; +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; -use cuprate_types::{ - AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork, - VerifiedTransactionInformation, -}; +use cuprate_types::{AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork}; use monero_serai::block::{Block, BlockHeader}; pub fn add_alt_block( @@ -54,8 +49,8 @@ pub fn add_alt_block( )?; assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len()); - for (tx, tx_hash) in alt_block.txs.iter().zip(&alt_block.block.transactions) { - add_alt_transaction_blob(tx_hash, StorableVec::wrap_ref(tx), tables)?; + for tx in alt_block.txs.iter() { + add_alt_transaction_blob(tx, tables)?; } Ok(()) @@ -74,8 +69,8 @@ pub fn get_alt_block( let txs = block .transactions .iter() - .map(|tx_hash| get_alt_transaction_blob(tx_hash, tables)) - .collect()?; + .map(|tx_hash| get_alt_transaction(tx_hash, tables)) + .collect::>()?; Ok(AltBlockInformation { block, diff --git a/storage/blockchain/src/ops/alt_block/chain.rs b/storage/blockchain/src/ops/alt_block/chain.rs index 4259d4dc9..1162a9cde 100644 --- a/storage/blockchain/src/ops/alt_block/chain.rs +++ b/storage/blockchain/src/ops/alt_block/chain.rs @@ -1,6 +1,6 @@ use crate::tables::{AltChainInfos, TablesMut}; use crate::types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight}; -use cuprate_database::{DatabaseRo, RuntimeError}; +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError}; use cuprate_types::{Chain, ChainId}; use std::cmp::max; @@ -26,6 +26,7 @@ pub fn check_add_alt_chain_info( &AltChainInfo { parent_chain: parent_chain.into(), common_ancestor_height: alt_block_height.height - 1, + chain_height: alt_block_height.height, }, ) } diff --git a/storage/blockchain/src/ops/alt_block/mod.rs b/storage/blockchain/src/ops/alt_block/mod.rs index 8b2d1f172..72e0933ef 100644 --- a/storage/blockchain/src/ops/alt_block/mod.rs +++ b/storage/blockchain/src/ops/alt_block/mod.rs @@ -5,3 +5,20 @@ mod tx; pub use block::*; pub use chain::*; pub use tx::*; + +pub fn flush_alt_blocks<'a, E: cuprate_database::EnvInner<'a>>( + env_inner: &E, + tx_rw: &mut E::Rw<'_>, +) -> Result<(), cuprate_database::RuntimeError> { + use crate::tables::{ + AltBlockBlobs, AltBlockHeights, AltBlocksInfo, AltChainInfos, AltTransactionBlobs, + AltTransactionInfos, + }; + + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw)?; + env_inner.clear_db::(tx_rw) +} diff --git a/storage/blockchain/src/ops/alt_block/tx.rs b/storage/blockchain/src/ops/alt_block/tx.rs index aad4dc3dd..a49c72ae7 100644 --- a/storage/blockchain/src/ops/alt_block/tx.rs +++ b/storage/blockchain/src/ops/alt_block/tx.rs @@ -1,35 +1,57 @@ use crate::tables::{Tables, TablesMut}; use crate::types::{AltTransactionInfo, TxHash}; use bytemuck::TransparentWrapper; -use cuprate_database::{RuntimeError, StorableVec}; +use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; use cuprate_types::VerifiedTransactionInformation; +use monero_serai::transaction::Transaction; pub fn add_alt_transaction_blob( - tx_hash: &TxHash, - tx_block: &StorableVec, + tx: &VerifiedTransactionInformation, tables: &mut impl TablesMut, ) -> Result<(), RuntimeError> { - if tables.tx_ids().get(&tx_hash).is_ok() || tables.alt_transaction_blobs().get(&tx_hash).is_ok() + tables.alt_transaction_infos_mut().put( + &tx.tx_hash, + &AltTransactionInfo { + tx_weight: tx.tx_weight, + fee: tx.fee, + tx_hash: tx.tx_hash, + }, + )?; + + if tables.tx_ids().get(&tx.tx_hash).is_ok() + || tables.alt_transaction_blobs().get(&tx.tx_hash).is_ok() { return Ok(()); } - tables.alt_transaction_blobs_mut().put(&tx_hash, tx_block) + tables + .alt_transaction_blobs_mut() + .put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob)) } -pub fn get_alt_transaction_blob( +pub fn get_alt_transaction( tx_hash: &TxHash, tables: &impl Tables, -) -> Result, RuntimeError> { - match tables.alt_transaction_blobs().get(tx_hash) { - Ok(blob) => Ok(blob.0), +) -> Result { + let tx_info = tables.alt_transaction_infos().get(tx_hash)?; + + let tx_blob = match tables.alt_transaction_blobs().get(tx_hash) { + Ok(blob) => blob.0, Err(RuntimeError::KeyNotFound) => { let tx_id = tables.tx_ids().get(tx_hash)?; let blob = tables.tx_blobs().get(&tx_id)?; - Ok(blob.0) + blob.0 } Err(e) => return Err(e), - } + }; + + Ok(VerifiedTransactionInformation { + tx: Transaction::read(&mut tx_blob.as_slice()).unwrap(), + tx_blob, + tx_weight: tx_info.tx_weight, + fee: tx_info.fee, + tx_hash: tx_info.tx_hash, + }) } diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 2e110fedf..5cb3b4bcc 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -8,8 +8,13 @@ use cuprate_database::{ RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, }; use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; -use cuprate_types::{ExtendedBlockHeader, HardFork, VerifiedBlockInformation}; +use cuprate_types::{ + AltBlockInformation, ChainId, ExtendedBlockHeader, HardFork, VerifiedBlockInformation, + VerifiedTransactionInformation, +}; +use crate::free::tx_fee; +use crate::ops::alt_block; use crate::{ ops::{ blockchain::{chain_height, cumulative_generated_coins}, @@ -106,9 +111,8 @@ pub fn add_block( cumulative_rct_outs, timestamp: block.block.header.timestamp, block_hash: block.block_hash, - // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` - weight: block.weight as u64, - long_term_weight: block.long_term_weight as u64, + weight: block.weight, + long_term_weight: block.long_term_weight, }, )?; @@ -135,17 +139,15 @@ pub fn add_block( /// will be returned if there are no blocks left. // no inline, too big pub fn pop_block( + move_to_alt_chain: Option, tables: &mut impl TablesMut, ) -> Result<(BlockHeight, BlockHash, Block), RuntimeError> { //------------------------------------------------------ Block Info // Remove block data from tables. - let (block_height, block_hash) = { - let (block_height, block_info) = tables.block_infos_mut().pop_last()?; - (block_height, block_info.block_hash) - }; + let (block_height, block_info) = tables.block_infos_mut().pop_last()?; // Block heights. - tables.block_heights_mut().delete(&block_hash)?; + tables.block_heights_mut().delete(&block_info.block_hash)?; // Block blobs. // We deserialize the block blob into a `Block`, such @@ -154,12 +156,42 @@ pub fn pop_block( let block = Block::read(&mut block_blob.as_slice())?; //------------------------------------------------------ Transaction / Outputs / Key Images + let mut txs = Vec::with_capacity(block.transactions.len()); + remove_tx(&block.miner_transaction.hash(), tables)?; for tx_hash in &block.transactions { - remove_tx(tx_hash, tables)?; + let (_, tx) = remove_tx(tx_hash, tables)?; + + if move_to_alt_chain.is_some() { + txs.push(VerifiedTransactionInformation { + tx_weight: tx.weight(), + tx_blob: tx.serialize(), + tx_hash: tx.hash(), + fee: tx_fee(&tx), + tx, + }) + } + } + + if let Some(chain_id) = move_to_alt_chain { + alt_block::add_alt_block( + &AltBlockInformation { + block: block.clone(), + block_blob, + txs, + block_hash: block_info.block_hash, + pow_hash: [255; 32], + height: block_height, + weight: block_info.weight, + long_term_weight: block_info.long_term_weight, + cumulative_difficulty: 0, + chain_id, + }, + tables, + )?; } - Ok((block_height, block_hash, block)) + Ok((block_height, block_info.block_hash, block)) } //---------------------------------------------------------------------------------------------------- `get_block_extended_header_*` @@ -205,8 +237,8 @@ pub fn get_block_extended_header_from_height( .expect("Stored block must have a valid hard-fork"), vote: block_header.hardfork_signal, timestamp: block_header.timestamp, - block_weight: block_info.weight as usize, - long_term_weight: block_info.long_term_weight as usize, + block_weight: block_info.weight, + long_term_weight: block_info.long_term_weight, }) } @@ -412,7 +444,8 @@ mod test { for block_hash in block_hashes.into_iter().rev() { println!("pop_block(): block_hash: {}", hex::encode(block_hash)); - let (_popped_height, popped_hash, _popped_block) = pop_block(&mut tables).unwrap(); + let (_popped_height, popped_hash, _popped_block) = + pop_block(None, &mut tables).unwrap(); assert_eq!(block_hash, popped_hash); diff --git a/storage/blockchain/src/service/free.rs b/storage/blockchain/src/service/free.rs index e748bbbe4..aa8238f95 100644 --- a/storage/blockchain/src/service/free.rs +++ b/storage/blockchain/src/service/free.rs @@ -3,13 +3,13 @@ //---------------------------------------------------------------------------------------------------- Import use std::sync::Arc; -use cuprate_database::{ConcreteEnv, InitError}; - use crate::service::{init_read_service, init_write_service}; use crate::{ config::Config, service::types::{BlockchainReadHandle, BlockchainWriteHandle}, }; +use cuprate_database::{ConcreteEnv, InitError}; +use cuprate_types::{AltBlockInformation, VerifiedBlockInformation}; //---------------------------------------------------------------------------------------------------- Init #[cold] @@ -81,6 +81,39 @@ pub(super) const fn compact_history_genesis_not_included INITIAL_BLOCKS && !(top_block_height - INITIAL_BLOCKS + 2).is_power_of_two() } +//---------------------------------------------------------------------------------------------------- Compact history +pub(super) fn map_valid_alt_block_to_verified_block( + alt_block: AltBlockInformation, +) -> VerifiedBlockInformation { + let total_fees = alt_block.txs.iter().map(|tx| tx.fee).sum::(); + let total_miner_output = alt_block + .block + .miner_transaction + .prefix() + .outputs + .iter() + .map(|out| out.amount.unwrap_or(0)) + .sum::(); + + VerifiedBlockInformation { + block: alt_block.block, + block_blob: alt_block.block_blob, + txs: alt_block + .txs + .into_iter() + .map(TryInto::try_into) + .collect::>() + .unwrap(), + block_hash: alt_block.block_hash, + pow_hash: alt_block.pow_hash, + height: alt_block.height, + generated_coins: total_miner_output - total_fees, + weight: alt_block.weight, + long_term_weight: alt_block.long_term_weight, + cumulative_difficulty: alt_block.cumulative_difficulty, + } +} + //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 067ba7f18..95124d41a 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -1,18 +1,20 @@ //! Database writer thread definitions and logic. - //---------------------------------------------------------------------------------------------------- Import use std::sync::Arc; -use cuprate_database::{ConcreteEnv, Env, EnvInner, RuntimeError, TxRw}; +use cuprate_database::{ConcreteEnv, DatabaseRo, DatabaseRw, Env, EnvInner, RuntimeError, TxRw}; use cuprate_database_service::DatabaseWriteHandle; use cuprate_types::{ blockchain::{BlockchainResponse, BlockchainWriteRequest}, - AltBlockInformation, VerifiedBlockInformation, + AltBlockInformation, Chain, ChainId, VerifiedBlockInformation, }; +use crate::service::free::map_valid_alt_block_to_verified_block; +use crate::types::AltBlockHeight; use crate::{ service::types::{BlockchainWriteHandle, ResponseResult}, - tables::OpenTables, + tables::{OpenTables, Tables, TablesMut}, + types::AltChainInfo, }; //---------------------------------------------------------------------------------------------------- init_write_service @@ -30,8 +32,10 @@ fn handle_blockchain_request( match req { BlockchainWriteRequest::WriteBlock(block) => write_block(env, block), BlockchainWriteRequest::WriteAltBlock(alt_block) => write_alt_block(env, alt_block), - BlockchainWriteRequest::StartReorg(_) => todo!(), - BlockchainWriteRequest::ReverseReorg(_) => todo!(), + BlockchainWriteRequest::PopBlocks(numb_blocks) => pop_blocks(env, *numb_blocks), + BlockchainWriteRequest::ReverseReorg(old_main_chain_id) => { + reverse_reorg(env, *old_main_chain_id) + } BlockchainWriteRequest::FlushAltBlocks => flush_alt_blocks(env), } } @@ -97,6 +101,108 @@ fn write_alt_block(env: &ConcreteEnv, block: &AltBlockInformation) -> ResponseRe } } +/// [`BlockchainWriteRequest::PopBlocks`]. +fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult { + let env_inner = env.env_inner(); + let mut tx_rw = env_inner.tx_rw()?; + + let result = { + crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?; + + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + + let old_main_chain_id = ChainId(rand::random()); + + let mut last_block_height = 0; + for _ in 0..numb_blocks { + (last_block_height, _, _) = + crate::ops::block::pop_block(Some(old_main_chain_id), &mut tables_mut)?; + } + + tables_mut.alt_chain_infos_mut().put( + &old_main_chain_id.into(), + &AltChainInfo { + parent_chain: Chain::Main.into(), + common_ancestor_height: last_block_height - 1, + chain_height: last_block_height + numb_blocks, + }, + )?; + + Ok(old_main_chain_id) + }; + + match result { + Ok(old_main_chain_id) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::PopBlocks(old_main_chain_id)) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + +/// [`BlockchainWriteRequest::ReverseReorg`]. +fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_rw = env_inner.tx_rw()?; + + let result = { + let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; + + let chain_info = tables_mut.alt_chain_infos().get(&chain_id.into())?; + assert_eq!(Chain::from(chain_info.parent_chain), Chain::Main); + + let tob_block_height = + crate::ops::blockchain::top_block_height(tables_mut.block_heights())?; + + for _ in chain_info.common_ancestor_height..tob_block_height { + crate::ops::block::pop_block(None, &mut tables_mut)?; + } + + // Rust borrow rules requires us to collect into a Vec first before looping over the Vec. + let alt_blocks = (chain_info.common_ancestor_height..chain_info.chain_height) + .map(|height| { + crate::ops::alt_block::get_alt_block( + &AltBlockHeight { + chain_id: chain_id.into(), + height, + }, + &tables_mut, + ) + }) + .collect::>(); + + for res_alt_block in alt_blocks { + let alt_block = res_alt_block?; + + let verified_block = map_valid_alt_block_to_verified_block(alt_block); + + crate::ops::block::add_block(&verified_block, &mut tables_mut)?; + } + + Ok(()) + }; + + match result { + Ok(()) => { + TxRw::commit(tx_rw)?; + Ok(BlockchainResponse::Ok) + } + Err(e) => { + // INVARIANT: ensure database atomicity by aborting + // the transaction on `add_block()` failures. + TxRw::abort(tx_rw) + .expect("could not maintain database atomicity by aborting write transaction"); + Err(e) + } + } +} + /// [`BlockchainWriteRequest::FlushAltBlocks`]. #[inline] fn flush_alt_blocks(env: &ConcreteEnv) -> ResponseResult { diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index deb957ea7..fa568ae6b 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -145,6 +145,9 @@ cuprate_database::define_tables! { 19 => AltTransactionBlobs, TxHash => TxBlob, + + 20 => AltTransactionInfos, + TxHash => AltTransactionInfo, } //---------------------------------------------------------------------------------------------------- Tests diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index 88ece10b0..14917249f 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -189,7 +189,7 @@ pub struct BlockInfo { /// The adjusted block size, in bytes. /// /// See [`block_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#blocks-weight). - pub weight: u64, + pub weight: usize, /// Least-significant 64 bits of the 128-bit cumulative difficulty. pub cumulative_difficulty_low: u64, /// Most-significant 64 bits of the 128-bit cumulative difficulty. @@ -201,7 +201,7 @@ pub struct BlockInfo { /// The long term block weight, based on the median weight of the preceding `100_000` blocks. /// /// See [`long_term_weight`](https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#long-term-block-weight). - pub long_term_weight: u64, + pub long_term_weight: usize, } //---------------------------------------------------------------------------------------------------- OutputFlags @@ -381,6 +381,7 @@ impl Key for RawChainId {} pub struct AltChainInfo { pub parent_chain: RawChain, pub common_ancestor_height: usize, + pub chain_height: usize, } //---------------------------------------------------------------------------------------------------- AltBlockHeight diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 33c3e8bd4..c2a5517da 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -116,20 +116,17 @@ pub enum BlockchainWriteRequest { /// /// Input is the alternative block. WriteAltBlock(AltBlockInformation), - /// A request to start the re-org process. + /// A request to pop some blocks from the top of the main chain /// - /// The inner value is the [`ChainId`] of the alt-chain we want to re-org to. + /// Input is the amount of blocks to pop. /// - /// This will: - /// - pop blocks from the main chain - /// - retrieve all alt-blocks in this alt-chain - /// - flush all other alt blocks - StartReorg(ChainId), + /// This request flush all alt-chains from the cache before adding the popped blocks to the alt cache. + PopBlocks(usize), /// A request to reverse the re-org process. /// /// The inner value is the [`ChainId`] of the old main chain. /// - /// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::StartReorg`]. + /// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::PopBlocks`]. ReverseReorg(ChainId), /// A request to flush all alternative blocks. FlushAltBlocks, @@ -227,13 +224,10 @@ pub enum BlockchainResponse { /// - [`BlockchainWriteRequest::ReverseReorg`] /// - [`BlockchainWriteRequest::FlushAltBlocks`] Ok, - /// The response for [`BlockchainWriteRequest::StartReorg`]. - StartReorg { - /// The [`ChainId`] of the old main chain blocks that were popped. - old_main_chain_id: ChainId, - /// The next alt chain blocks. - alt_chain: Vec, - }, + /// The response for [`BlockchainWriteRequest::PopBlocks`]. + /// + /// The inner value is the alt-chain ID for the old main chain blocks. + PopBlocks(ChainId), } //---------------------------------------------------------------------------------------------------- Tests diff --git a/types/src/types.rs b/types/src/types.rs index cc4543e67..c6e83d093 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -79,6 +79,7 @@ pub struct VerifiedBlockInformation { /// [`Block::hash`]. pub block_hash: [u8; 32], /// The block's proof-of-work hash. + // TODO: make this an option. pub pow_hash: [u8; 32], /// The block's height. pub height: usize, @@ -120,7 +121,7 @@ pub struct AltBlockInformation { /// [`Block::serialize`]. pub block_blob: Vec, /// All the transactions in the block, excluding the [`Block::miner_transaction`]. - pub txs: Vec>, + pub txs: Vec, /// The block's hash. /// /// [`Block::hash`]. From 21e4b3a20a56323961f65e100ca826f0677a41e5 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 6 Sep 2024 00:26:36 +0100 Subject: [PATCH 14/46] commit Cargo.lock --- Cargo.lock | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5d64902a..e277e3f7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -160,7 +166,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.3", "object", "rustc-demangle", ] @@ -1080,12 +1086,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1239,9 +1245,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -1735,6 +1741,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -2397,9 +2412,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -2958,9 +2973,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ "base64", "flate2", @@ -3083,9 +3098,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ "rustls-pki-types", ] From 123aedd6a986f7884efdc922eca046c86b7169ae Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 6 Sep 2024 02:39:40 +0100 Subject: [PATCH 15/46] add test --- storage/blockchain/src/ops/alt_block/block.rs | 100 +++++++++++++++++- storage/blockchain/src/ops/alt_block/chain.rs | 2 +- storage/blockchain/src/ops/block.rs | 2 +- storage/blockchain/src/ops/mod.rs | 2 +- storage/blockchain/src/service/mod.rs | 2 +- storage/blockchain/src/service/tests.rs | 2 +- storage/blockchain/src/tests.rs | 16 +++ 7 files changed, 120 insertions(+), 6 deletions(-) diff --git a/storage/blockchain/src/ops/alt_block/block.rs b/storage/blockchain/src/ops/alt_block/block.rs index a429eab84..83d04ba74 100644 --- a/storage/blockchain/src/ops/alt_block/block.rs +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -171,7 +171,7 @@ pub fn get_alt_block_extended_header_from_height( let block_header = BlockHeader::read(&mut block_blob.as_slice())?; Ok(ExtendedBlockHeader { - version: HardFork::from_version(0).expect("Block in DB must have correct version"), + version: HardFork::from_version(block_header.hardfork_version).expect("Block in DB must have correct version"), vote: block_header.hardfork_version, timestamp: block_header.timestamp, cumulative_difficulty: combine_low_high_bits_to_u128( @@ -182,3 +182,101 @@ pub fn get_alt_block_extended_header_from_height( long_term_weight: block_info.long_term_weight, }) } + +#[cfg(test)] +mod tests { + use std::num::NonZero; + use cuprate_database::{Env, EnvInner, TxRw}; + use cuprate_test_utils::data::{BLOCK_V1_TX2, BLOCK_V9_TX3, BLOCK_V16_TX0}; + use cuprate_types::ChainId; + use crate::ops::alt_block::{add_alt_block, flush_alt_blocks, get_alt_block, get_alt_extended_headers_in_range}; + use crate::ops::block::{add_block, pop_block}; + use crate::tables::OpenTables; + use crate::tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env}; + use crate::types::AltBlockHeight; + + #[test] + fn all_alt_blocks() { + let (env, _tmp) = tmp_concrete_env(); + let env_inner = env.env_inner(); + assert_all_tables_are_empty(&env); + + let chain_id = ChainId(NonZero::new(1).unwrap()).into(); + + // Add initial block. + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut initial_block = BLOCK_V1_TX2.clone(); + initial_block.height = 0; + + add_block(&initial_block, &mut tables).unwrap(); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + let alt_blocks = [ + map_verified_block_to_alt(BLOCK_V9_TX3.clone(), chain_id), + map_verified_block_to_alt(BLOCK_V16_TX0.clone(), chain_id), + ]; + + // Add alt-blocks + { + let tx_rw = env_inner.tx_rw().unwrap(); + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + + let mut prev_hash = BLOCK_V1_TX2.block_hash; + for (i, mut alt_block) in alt_blocks.into_iter().enumerate() { + let height = i + 1; + + alt_block.height = height; + alt_block.block.header.previous = prev_hash; + alt_block.block_blob = alt_block.block.serialize(); + + add_alt_block(&alt_block, &mut tables).unwrap(); + + let alt_height = AltBlockHeight { + chain_id: chain_id.into(), + height, + }; + + let alt_block_2 = get_alt_block(&alt_height, &tables).unwrap(); + assert_eq!(alt_block.block, alt_block_2.block); + + let headers = get_alt_extended_headers_in_range(0..(height + 1), chain_id, &tables).unwrap(); + assert_eq!(headers.len(), height); + + let last_header = headers.last().unwrap(); + assert_eq!(last_header.timestamp, alt_block.block.header.timestamp); + assert_eq!(last_header.block_weight, alt_block.weight); + assert_eq!(last_header.long_term_weight, alt_block.long_term_weight); + assert_eq!(last_header.cumulative_difficulty, alt_block.cumulative_difficulty); + assert_eq!(last_header.version.as_u8(), alt_block.block.header.hardfork_version); + assert_eq!(last_header.vote, alt_block.block.header.hardfork_signal); + + prev_hash = alt_block.block_hash; + } + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + + { + let mut tx_rw = env_inner.tx_rw().unwrap(); + + flush_alt_blocks(&env_inner, &mut tx_rw).unwrap(); + + let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap(); + pop_block(None, &mut tables).unwrap(); + + drop(tables); + TxRw::commit(tx_rw).unwrap(); + } + + assert_all_tables_are_empty(&env); + } + +} diff --git a/storage/blockchain/src/ops/alt_block/chain.rs b/storage/blockchain/src/ops/alt_block/chain.rs index 1162a9cde..166a294a5 100644 --- a/storage/blockchain/src/ops/alt_block/chain.rs +++ b/storage/blockchain/src/ops/alt_block/chain.rs @@ -45,7 +45,7 @@ pub fn get_alt_chain_history_ranges( let start_height = max(range.start, chain_info.common_ancestor_height + 1); - ranges.push((chain_info.parent_chain.into(), start_height..i)); + ranges.push((Chain::Alt(current_chain_id.into()), start_height..i)); i = chain_info.common_ancestor_height; match chain_info.parent_chain.into() { diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 229d35e1d..b0997f760 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -180,7 +180,7 @@ pub fn pop_block( block_blob, txs, block_hash: block_info.block_hash, - pow_hash: [255; 32], + pow_hash: [0; 32], height: block_height, weight: block_info.weight, long_term_weight: block_info.long_term_weight, diff --git a/storage/blockchain/src/ops/mod.rs b/storage/blockchain/src/ops/mod.rs index 8a8f0f158..285aa2440 100644 --- a/storage/blockchain/src/ops/mod.rs +++ b/storage/blockchain/src/ops/mod.rs @@ -94,7 +94,7 @@ //! // Read the data, assert it is correct. //! let tx_rw = env_inner.tx_rw()?; //! let mut tables = env_inner.open_tables_mut(&tx_rw)?; -//! let (height, hash, serai_block) = pop_block(&mut tables)?; +//! let (height, hash, serai_block) = pop_block(None, &mut tables)?; //! //! assert_eq!(height, 0); //! assert_eq!(serai_block, block.block); diff --git a/storage/blockchain/src/service/mod.rs b/storage/blockchain/src/service/mod.rs index c774ee493..aa322d06e 100644 --- a/storage/blockchain/src/service/mod.rs +++ b/storage/blockchain/src/service/mod.rs @@ -98,7 +98,7 @@ //! //! // Block write was OK. //! let response = response_channel.await?; -//! assert_eq!(response, BlockchainResponse::WriteBlockOk); +//! assert_eq!(response, BlockchainResponse::Ok); //! //! // Now, let's try getting the block hash //! // of the block we just wrote. diff --git a/storage/blockchain/src/service/tests.rs b/storage/blockchain/src/service/tests.rs index b68b5444c..78cb6944a 100644 --- a/storage/blockchain/src/service/tests.rs +++ b/storage/blockchain/src/service/tests.rs @@ -84,7 +84,7 @@ async fn test_template( let request = BlockchainWriteRequest::WriteBlock(block); let response_channel = writer.call(request); let response = response_channel.await.unwrap(); - assert_eq!(response, BlockchainResponse::WriteBlockOk); + assert_eq!(response, BlockchainResponse::Ok); } //----------------------------------------------------------------------- Reset the transaction diff --git a/storage/blockchain/src/tests.rs b/storage/blockchain/src/tests.rs index 65527e102..602391273 100644 --- a/storage/blockchain/src/tests.rs +++ b/storage/blockchain/src/tests.rs @@ -10,6 +10,7 @@ use std::{borrow::Cow, fmt::Debug}; use pretty_assertions::assert_eq; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner}; +use cuprate_types::{AltBlockInformation, ChainId, VerifiedBlockInformation}; use crate::{ config::ConfigBuilder, @@ -88,3 +89,18 @@ pub(crate) fn assert_all_tables_are_empty(env: &ConcreteEnv) { assert!(tables.all_tables_empty().unwrap()); assert_eq!(crate::ops::tx::get_num_tx(tables.tx_ids()).unwrap(), 0); } + +pub(crate) fn map_verified_block_to_alt(verified_block: VerifiedBlockInformation, chain_id: ChainId) -> AltBlockInformation { + AltBlockInformation { + block: verified_block.block, + block_blob: verified_block.block_blob, + txs: verified_block.txs, + block_hash: verified_block.block_hash, + pow_hash: verified_block.pow_hash, + height: verified_block.height, + weight: verified_block.weight, + long_term_weight: verified_block.long_term_weight, + cumulative_difficulty: verified_block.cumulative_difficulty, + chain_id, + } +} \ No newline at end of file From ba5c5ac45d958f43382df21dd12eeca520a7c870 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 7 Sep 2024 02:02:19 +0100 Subject: [PATCH 16/46] more docs + cleanup + alt blocks request --- storage/blockchain/src/ops/alt_block/block.rs | 164 +++++++++++------- storage/blockchain/src/ops/alt_block/chain.rs | 47 ++++- storage/blockchain/src/ops/alt_block/mod.rs | 59 ++++++- storage/blockchain/src/ops/alt_block/tx.rs | 21 ++- storage/blockchain/src/ops/block.rs | 23 +-- storage/blockchain/src/ops/macros.rs | 21 +++ storage/blockchain/src/service/read.rs | 98 +++++++++-- storage/blockchain/src/service/write.rs | 19 +- storage/blockchain/src/tests.rs | 7 +- types/src/blockchain.rs | 20 ++- 10 files changed, 365 insertions(+), 114 deletions(-) diff --git a/storage/blockchain/src/ops/alt_block/block.rs b/storage/blockchain/src/ops/alt_block/block.rs index 83d04ba74..07878d55b 100644 --- a/storage/blockchain/src/ops/alt_block/block.rs +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -1,16 +1,35 @@ -use crate::ops::alt_block::{ - add_alt_transaction_blob, check_add_alt_chain_info, get_alt_chain_history_ranges, - get_alt_transaction, -}; -use crate::ops::block::{get_block_extended_header_from_height, get_block_info}; -use crate::tables::{Tables, TablesMut}; -use crate::types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo}; use bytemuck::TransparentWrapper; +use monero_serai::block::{Block, BlockHeader}; + use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; use cuprate_types::{AltBlockInformation, Chain, ChainId, ExtendedBlockHeader, HardFork}; -use monero_serai::block::{Block, BlockHeader}; +use crate::{ + ops::{ + alt_block::{add_alt_transaction_blob, get_alt_transaction, update_alt_chain_info}, + block::get_block_info, + macros::doc_error, + }, + tables::{Tables, TablesMut}, + types::{AltBlockHeight, BlockHash, BlockHeight, CompactAltBlockInfo}, +}; + +/// Add a [`AltBlockInformation`] to the database. +/// +/// This extracts all the data from the input block and +/// maps/adds them to the appropriate database tables. +/// +#[doc = doc_error!()] +/// +/// # Panics +/// This function will panic if: +/// - `block.height` is == `0` +/// +/// # Already exists +/// This function will operate normally even if `block` already +/// exists, i.e., this function will not return `Err` even if you +/// call this function infinitely with the same block. pub fn add_alt_block( alt_block: &AltBlockInformation, tables: &mut impl TablesMut, @@ -24,7 +43,7 @@ pub fn add_alt_block( .alt_block_heights_mut() .put(&alt_block.block_hash, &alt_block_height)?; - check_add_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?; + update_alt_chain_info(&alt_block_height, &alt_block.block.header.previous, tables)?; let (cumulative_difficulty_low, cumulative_difficulty_high) = split_u128_into_low_high_bits(alt_block.cumulative_difficulty); @@ -49,13 +68,18 @@ pub fn add_alt_block( )?; assert_eq!(alt_block.txs.len(), alt_block.block.transactions.len()); - for tx in alt_block.txs.iter() { + for tx in &alt_block.txs { add_alt_transaction_blob(tx, tables)?; } Ok(()) } +/// Retrieves an [`AltBlockInformation`] from the database. +/// +/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others +/// even if they are technically part of this chain. +#[doc = doc_error!()] pub fn get_alt_block( alt_block_height: &AltBlockHeight, tables: &impl Tables, @@ -89,13 +113,21 @@ pub fn get_alt_block( }) } +/// Retrieves the hash of the block at the given `block_height` on the alt chain with +/// the given [`ChainId`]. +/// +/// This function will get blocks from the whole chain, for example if you were to ask for height +/// `0` with any [`ChainId`] (as long that chain actually exists) you will get the main chain genesis. +/// +#[doc = doc_error!()] pub fn get_alt_block_hash( block_height: &BlockHeight, alt_chain: ChainId, - tables: &mut impl Tables, + tables: &impl Tables, ) -> Result { let alt_chains = tables.alt_chain_infos(); + // First find what [`ChainId`] this block would be stored under. let original_chain = { let mut chain = alt_chain.into(); loop { @@ -115,9 +147,10 @@ pub fn get_alt_block_hash( } }; + // Get the block hash. match original_chain { Chain::Main => { - get_block_info(&block_height, tables.block_infos()).map(|info| info.block_hash) + get_block_info(block_height, tables.block_infos()).map(|info| info.block_hash) } Chain::Alt(chain_id) => tables .alt_blocks_info() @@ -129,37 +162,12 @@ pub fn get_alt_block_hash( } } -pub fn get_alt_extended_headers_in_range( - range: std::ops::Range, - alt_chain: ChainId, - tables: &impl Tables, -) -> Result, RuntimeError> { - // TODO: this function does not use rayon, however it probably should. - - let alt_chains = tables.alt_chain_infos(); - let ranges = get_alt_chain_history_ranges(range, alt_chain, alt_chains)?; - - let res = ranges - .into_iter() - .rev() - .map(|(chain, range)| { - range.into_iter().map(move |height| match chain { - Chain::Main => get_block_extended_header_from_height(&height, tables), - Chain::Alt(chain_id) => get_alt_block_extended_header_from_height( - &AltBlockHeight { - chain_id: chain_id.into(), - height, - }, - tables, - ), - }) - }) - .flatten() - .collect::>()?; - - Ok(res) -} - +/// Retrieves the [`ExtendedBlockHeader`] of the alt-block with an exact [`AltBlockHeight`]. +/// +/// This function will look at only the blocks with the given [`AltBlockHeight::chain_id`], no others +/// even if they are technically part of this chain. +/// +#[doc = doc_error!()] pub fn get_alt_block_extended_header_from_height( height: &AltBlockHeight, table: &impl Tables, @@ -171,7 +179,8 @@ pub fn get_alt_block_extended_header_from_height( let block_header = BlockHeader::read(&mut block_blob.as_slice())?; Ok(ExtendedBlockHeader { - version: HardFork::from_version(block_header.hardfork_version).expect("Block in DB must have correct version"), + version: HardFork::from_version(block_header.hardfork_version) + .expect("Block in DB must have correct version"), vote: block_header.hardfork_version, timestamp: block_header.timestamp, cumulative_difficulty: combine_low_high_bits_to_u128( @@ -186,22 +195,33 @@ pub fn get_alt_block_extended_header_from_height( #[cfg(test)] mod tests { use std::num::NonZero; + use cuprate_database::{Env, EnvInner, TxRw}; - use cuprate_test_utils::data::{BLOCK_V1_TX2, BLOCK_V9_TX3, BLOCK_V16_TX0}; - use cuprate_types::ChainId; - use crate::ops::alt_block::{add_alt_block, flush_alt_blocks, get_alt_block, get_alt_extended_headers_in_range}; - use crate::ops::block::{add_block, pop_block}; - use crate::tables::OpenTables; - use crate::tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env}; - use crate::types::AltBlockHeight; + use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3}; + use cuprate_types::{Chain, ChainId}; + + use crate::{ + ops::{ + alt_block::{ + add_alt_block, flush_alt_blocks, get_alt_block, + get_alt_block_extended_header_from_height, get_alt_block_hash, + get_alt_chain_history_ranges, + }, + block::{add_block, pop_block}, + }, + tables::{OpenTables, Tables}, + tests::{assert_all_tables_are_empty, map_verified_block_to_alt, tmp_concrete_env}, + types::AltBlockHeight, + }; + #[allow(clippy::range_plus_one)] #[test] fn all_alt_blocks() { let (env, _tmp) = tmp_concrete_env(); let env_inner = env.env_inner(); assert_all_tables_are_empty(&env); - let chain_id = ChainId(NonZero::new(1).unwrap()).into(); + let chain_id = ChainId(NonZero::new(1).unwrap()); // Add initial block. { @@ -245,25 +265,44 @@ mod tests { let alt_block_2 = get_alt_block(&alt_height, &tables).unwrap(); assert_eq!(alt_block.block, alt_block_2.block); - let headers = get_alt_extended_headers_in_range(0..(height + 1), chain_id, &tables).unwrap(); - assert_eq!(headers.len(), height); + let headers = get_alt_chain_history_ranges( + 0..(height + 1), + chain_id, + tables.alt_chain_infos(), + ) + .unwrap(); - let last_header = headers.last().unwrap(); - assert_eq!(last_header.timestamp, alt_block.block.header.timestamp); - assert_eq!(last_header.block_weight, alt_block.weight); - assert_eq!(last_header.long_term_weight, alt_block.long_term_weight); - assert_eq!(last_header.cumulative_difficulty, alt_block.cumulative_difficulty); - assert_eq!(last_header.version.as_u8(), alt_block.block.header.hardfork_version); - assert_eq!(last_header.vote, alt_block.block.header.hardfork_signal); + assert_eq!(headers.len(), 2); + assert_eq!(headers[1], (Chain::Main, 0..1)); + assert_eq!(headers[0], (Chain::Alt(chain_id), 1..(height + 1))); prev_hash = alt_block.block_hash; + + let header = + get_alt_block_extended_header_from_height(&alt_height, &tables).unwrap(); + + assert_eq!(header.timestamp, alt_block.block.header.timestamp); + assert_eq!(header.block_weight, alt_block.weight); + assert_eq!(header.long_term_weight, alt_block.long_term_weight); + assert_eq!( + header.cumulative_difficulty, + alt_block.cumulative_difficulty + ); + assert_eq!( + header.version.as_u8(), + alt_block.block.header.hardfork_version + ); + assert_eq!(header.vote, alt_block.block.header.hardfork_signal); + + let block_hash = get_alt_block_hash(&height, chain_id, &tables).unwrap(); + + assert_eq!(block_hash, alt_block.block_hash); } drop(tables); TxRw::commit(tx_rw).unwrap(); } - { let mut tx_rw = env_inner.tx_rw().unwrap(); @@ -278,5 +317,4 @@ mod tests { assert_all_tables_are_empty(&env); } - } diff --git a/storage/blockchain/src/ops/alt_block/chain.rs b/storage/blockchain/src/ops/alt_block/chain.rs index 166a294a5..3e27d7d4b 100644 --- a/storage/blockchain/src/ops/alt_block/chain.rs +++ b/storage/blockchain/src/ops/alt_block/chain.rs @@ -1,20 +1,43 @@ -use crate::tables::{AltChainInfos, TablesMut}; -use crate::types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight}; +use std::cmp::max; + use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError}; use cuprate_types::{Chain, ChainId}; -use std::cmp::max; -pub fn check_add_alt_chain_info( +use crate::{ + ops::macros::{doc_add_alt_block_inner_invariant, doc_error}, + tables::{AltChainInfos, TablesMut}, + types::{AltBlockHeight, AltChainInfo, BlockHash, BlockHeight}, +}; + +/// Updates the [`AltChainInfo`] with information on a new alt-block. +/// +#[doc = doc_add_alt_block_inner_invariant!()] +#[doc = doc_error!()] +/// +/// # Panics +/// +/// This will panic if [`AltBlockHeight::height`] == `0`. +pub fn update_alt_chain_info( alt_block_height: &AltBlockHeight, prev_hash: &BlockHash, tables: &mut impl TablesMut, ) -> Result<(), RuntimeError> { - match tables.alt_chain_infos().get(&alt_block_height.chain_id) { - Ok(_) => return Ok(()), + // try update the info if one exists for this chain. + let update = tables + .alt_chain_infos_mut() + .update(&alt_block_height.chain_id, |mut info| { + info.chain_height = alt_block_height.height + 1; + Some(info) + }); + + match update { + Ok(()) => return Ok(()), Err(RuntimeError::KeyNotFound) => (), Err(e) => return Err(e), } + // If one doesn't already exist add it. + let parent_chain = match tables.alt_block_heights().get(prev_hash) { Ok(alt_parent_height) => Chain::Alt(alt_parent_height.chain_id.into()), Err(RuntimeError::KeyNotFound) => Chain::Main, @@ -25,12 +48,18 @@ pub fn check_add_alt_chain_info( &alt_block_height.chain_id, &AltChainInfo { parent_chain: parent_chain.into(), - common_ancestor_height: alt_block_height.height - 1, - chain_height: alt_block_height.height, + common_ancestor_height: alt_block_height.height.checked_sub(1).unwrap(), + chain_height: alt_block_height.height + 1, }, ) } +/// Get the height history of an alt-chain in reverse chronological order. +/// +/// Height history is a list of height ranges with the corresponding [`Chain`] they are stored under. +/// For example if your range goes from height `0` the last entry in the list will be [`Chain::Main`] +/// upto the height where the first split occurs. +#[doc = doc_error!()] pub fn get_alt_chain_history_ranges( range: std::ops::Range, alt_chain: ChainId, @@ -46,7 +75,7 @@ pub fn get_alt_chain_history_ranges( let start_height = max(range.start, chain_info.common_ancestor_height + 1); ranges.push((Chain::Alt(current_chain_id.into()), start_height..i)); - i = chain_info.common_ancestor_height; + i = chain_info.common_ancestor_height + 1; match chain_info.parent_chain.into() { Chain::Main => { diff --git a/storage/blockchain/src/ops/alt_block/mod.rs b/storage/blockchain/src/ops/alt_block/mod.rs index 72e0933ef..36e4768ec 100644 --- a/storage/blockchain/src/ops/alt_block/mod.rs +++ b/storage/blockchain/src/ops/alt_block/mod.rs @@ -1,11 +1,62 @@ -mod block; -mod chain; -mod tx; - +//! Alternative Block/Chain Ops +//! +//! Alternative chains are chains that potentially have more proof-of-work than the main-chain +//! which we are tracking to potentially re-org to. +//! +//! Cuprate uses an ID system for alt-chains. When a split is made from the main-chain we generate +//! a random [`ChainID`](cuprate_types::ChainId) and assign it to the chain: +//! +//! ```text +//! | +//! | +//! | split +//! |------------- +//! | | +//! | | +//! \|/ \|/ +//! main-chain ChainID(X) +//! ``` +//! +//! In that example if we were to receive an alt-block which immediately follows the top block of `ChainID(X)` +//! then that block will also be stored under `ChainID(X)`. However if it follows from another block from `ChainID(X)` +//! we will split into a chain with a different ID. +//! +//! ```text +//! | +//! | +//! | split +//! |------------- +//! | | split +//! | |-------------| +//! | | | +//! | | | +//! | | | +//! \|/ \|/ \|/ +//! main-chain ChainID(X) ChainID(Z) +//! ``` +//! +//! As you can see if we wanted to get all the alt-blocks in `ChainID(Z)` that now includes some blocks from `ChainID(X)` as well. +//! [`get_alt_chain_history_ranges`] covers this and is the method to get the ranges of heights needed from each [`ChainID`](cuprate_types::ChainId) +//! to get all the alt-blocks in a given [`ChainID`](cuprate_types::ChainId). +//! +//! Although this should be kept in mind as a possibility because Cuprate's block downloader will only track a single chain it is +//! unlikely that we will be tracking [`ChainID`](cuprate_types::ChainId) that don't immediately connect to the main-chain. +//! +//! ## Why not use block's previous field? +//! +//! Although that would be easier, it makes getting a range of block extremely slow, as we have to build the weight cache to verify +//! blocks, roughly 100,000 block headers needed, this cost was seen as too high. pub use block::*; pub use chain::*; pub use tx::*; +mod block; +mod chain; +mod tx; + +/// Flush all alt-block data from all the alt-block tables. +/// +/// This function completely empties the alt block tables. pub fn flush_alt_blocks<'a, E: cuprate_database::EnvInner<'a>>( env_inner: &E, tx_rw: &mut E::Rw<'_>, diff --git a/storage/blockchain/src/ops/alt_block/tx.rs b/storage/blockchain/src/ops/alt_block/tx.rs index a49c72ae7..5671e4b45 100644 --- a/storage/blockchain/src/ops/alt_block/tx.rs +++ b/storage/blockchain/src/ops/alt_block/tx.rs @@ -1,10 +1,22 @@ -use crate::tables::{Tables, TablesMut}; -use crate::types::{AltTransactionInfo, TxHash}; use bytemuck::TransparentWrapper; +use monero_serai::transaction::Transaction; + use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; use cuprate_types::VerifiedTransactionInformation; -use monero_serai::transaction::Transaction; +use crate::ops::macros::{doc_add_alt_block_inner_invariant, doc_error}; +use crate::tables::{Tables, TablesMut}; +use crate::types::{AltTransactionInfo, TxHash}; + +/// Adds a [`VerifiedTransactionInformation`] form an alt-block to the DB, if +/// that transaction is not already in the DB. +/// +/// If the transaction is in the main-chain this function will still fill in the +/// [`AltTransactionInfos`](crate::tables::AltTransactionInfos) table, as that +/// table holds data which we don't keep around for main-chain txs. +/// +#[doc = doc_add_alt_block_inner_invariant!()] +#[doc = doc_error!()] pub fn add_alt_transaction_blob( tx: &VerifiedTransactionInformation, tables: &mut impl TablesMut, @@ -29,6 +41,9 @@ pub fn add_alt_transaction_blob( .put(&tx.tx_hash, StorableVec::wrap_ref(&tx.tx_blob)) } +/// Retrieve a [`VerifiedTransactionInformation`] from the database. +/// +#[doc = doc_error!()] pub fn get_alt_transaction( tx_hash: &TxHash, tables: &impl Tables, diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index b0997f760..45bab41cf 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -38,11 +38,6 @@ use crate::{ /// This function will panic if: /// - `block.height > u32::MAX` (not normally possible) /// - `block.height` is not != [`chain_height`] -/// -/// # Already exists -/// This function will operate normally even if `block` already -/// exists, i.e., this function will not return `Err` even if you -/// call this function infinitely with the same block. // no inline, too big. pub fn add_block( block: &VerifiedBlockInformation, @@ -133,6 +128,9 @@ pub fn add_block( /// Remove the top/latest block from the database. /// /// The removed block's data is returned. +/// +/// If a [`ChainId`] is specified the popped block will be added to the alt block tables under +/// that [`ChainId`]. Otherwise, the block will be completely removed from the DB. #[doc = doc_error!()] /// /// In `pop_block()`'s case, [`RuntimeError::KeyNotFound`] @@ -169,7 +167,7 @@ pub fn pop_block( tx_hash: tx.hash(), fee: tx_fee(&tx), tx, - }) + }); } } @@ -180,11 +178,16 @@ pub fn pop_block( block_blob, txs, block_hash: block_info.block_hash, + // We know the PoW is valid for this block so just set it so it will always verify as + // valid. pow_hash: [0; 32], height: block_height, weight: block_info.weight, long_term_weight: block_info.long_term_weight, - cumulative_difficulty: 0, + cumulative_difficulty: combine_low_high_bits_to_u128( + block_info.cumulative_difficulty_low, + block_info.cumulative_difficulty_high, + ), chain_id, }, tables, @@ -229,8 +232,6 @@ pub fn get_block_extended_header_from_height( block_info.cumulative_difficulty_high, ); - // INVARIANT: #[cfg] @ lib.rs asserts `usize == u64` - #[allow(clippy::cast_possible_truncation)] Ok(ExtendedBlockHeader { cumulative_difficulty, version: HardFork::from_version(block_header.hardfork_version) @@ -302,14 +303,14 @@ mod test { use cuprate_database::{Env, EnvInner, TxRw}; use cuprate_test_utils::data::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3}; - use super::*; - use crate::{ ops::tx::{get_tx, tx_exists}, tables::OpenTables, tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen}, }; + use super::*; + /// Tests all above block functions. /// /// Note that this doesn't test the correctness of values added, as the diff --git a/storage/blockchain/src/ops/macros.rs b/storage/blockchain/src/ops/macros.rs index b7cdba474..e547c7f24 100644 --- a/storage/blockchain/src/ops/macros.rs +++ b/storage/blockchain/src/ops/macros.rs @@ -31,3 +31,24 @@ When calling this function, ensure that either: }; } pub(super) use doc_add_block_inner_invariant; + +// This is pretty much the same as [`doc_add_block_inner_invariant`], it's not worth the effort to reduce +// the duplication. +/// Generate `# Invariant` documentation for internal alt block `fn`'s +/// that should be called directly with caution. +macro_rules! doc_add_alt_block_inner_invariant { + () => { + r#"# ⚠️ Invariant ⚠️ +This function mainly exists to be used internally by the parent function [`crate::ops::alt_block::add_alt_block`]. + +`add_alt_block()` makes sure all data related to the input is mutated, while +this function _does not_, it specifically mutates _particular_ tables. + +This is usually undesired - although this function is still available to call directly. + +When calling this function, ensure that either: +1. This effect (incomplete database mutation) is what is desired, or that... +2. ...the other tables will also be mutated to a correct state"# + }; +} +pub(super) use doc_add_alt_block_inner_invariant; diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 70da01b3e..416e61372 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -1,28 +1,32 @@ //! Database reader thread-pool definitions and logic. -//---------------------------------------------------------------------------------------------------- Import use std::{ collections::{HashMap, HashSet}, sync::Arc, }; +//---------------------------------------------------------------------------------------------------- Import use rayon::{ iter::{IntoParallelIterator, ParallelIterator}, + prelude::*, ThreadPool, }; use thread_local::ThreadLocal; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; -use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThreads}; +use cuprate_database_service::{DatabaseReadService, init_thread_pool, ReaderThreads}; use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, - Chain, ExtendedBlockHeader, OutputOnChain, + Chain, ChainId, ExtendedBlockHeader, OutputOnChain, }; use crate::{ ops::{ - alt_block::{get_alt_block_hash, get_alt_extended_headers_in_range}, + alt_block::{ + get_alt_block_extended_header_from_height, get_alt_block_hash, + get_alt_chain_history_ranges, + }, block::{ block_exists, get_block_extended_header_from_height, get_block_height, get_block_info, }, @@ -35,8 +39,11 @@ use crate::{ types::{BlockchainReadHandle, ResponseResult}, }, tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables}, - types::{Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId}, + types::{ + AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId, + }, }; +use crate::ops::alt_block::get_alt_block; //---------------------------------------------------------------------------------------------------- init_read_service /// Initialize the [`BlockchainReadHandle`] thread-pool backed by [`rayon`]. @@ -100,6 +107,7 @@ fn map_request( R::KeyImagesSpent(set) => key_images_spent(env, set), R::CompactChainHistory => compact_chain_history(env), R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids), + R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id), } /* SOMEDAY: post-request handling, run some code for each request? */ @@ -200,7 +208,7 @@ fn block_hash(env: &ConcreteEnv, block_height: BlockHeight, chain: Chain) -> Res let block_hash = match chain { Chain::Main => get_block_info(&block_height, &table_block_infos)?.block_hash, Chain::Alt(chain) => { - get_alt_block_hash(&block_height, chain, &mut env_inner.open_tables(&tx_ro)?)? + get_alt_block_hash(&block_height, chain, &env_inner.open_tables(&tx_ro)?)? } }; @@ -283,12 +291,36 @@ fn block_extended_header_in_range( }) .collect::, RuntimeError>>()?, Chain::Alt(chain_id) => { - let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; - get_alt_extended_headers_in_range( - range, - chain_id, - get_tables!(env_inner, tx_ro, tables)?.as_ref(), - )? + let ranges = { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + let alt_chains = tables.alt_chain_infos(); + + get_alt_chain_history_ranges(range, chain_id, alt_chains)? + }; + + ranges + .par_iter() + .rev() + .map(|(chain, range)| { + range.clone().into_par_iter().map(|height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + match *chain { + Chain::Main => get_block_extended_header_from_height(&height, tables), + Chain::Alt(chain_id) => get_alt_block_extended_header_from_height( + &AltBlockHeight { + chain_id: chain_id.into(), + height, + }, + tables, + ), + } + }) + }) + .flatten() + .collect::, _>>()? } }; @@ -524,3 +556,45 @@ fn find_first_unknown(env: &ConcreteEnv, block_ids: &[BlockHash]) -> ResponseRes BlockchainResponse::FindFirstUnknown(Some((idx, last_known_height + 1))) }) } + +/// [`BlockchainReadRequest::AltBlocksInChain`] +fn alt_blocks_in_chain(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + // Get the history of this alt-chain. + let history = { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + get_alt_chain_history_ranges(0..usize::MAX, chain_id, tables.alt_chain_infos())? + }; + + // Get all the blocks until we join the main-chain. + let blocks = history + .par_iter() + .rev() + .skip(1) + .flat_map(|(chain_id, range)| { + let Chain::Alt(chain_id) = chain_id else { + panic!("Should not have main chain blocks here we skipped last range"); + }; + + range.clone().into_par_iter().map(|height| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + get_alt_block( + &AltBlockHeight { + chain_id: (*chain_id).into(), + height, + }, + tables, + ) + }) + }) + .collect::>()?; + + Ok(BlockchainResponse::AltBlocksInChain(blocks)) +} diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 95124d41a..849a3030f 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -9,12 +9,13 @@ use cuprate_types::{ AltBlockInformation, Chain, ChainId, VerifiedBlockInformation, }; -use crate::service::free::map_valid_alt_block_to_verified_block; -use crate::types::AltBlockHeight; use crate::{ - service::types::{BlockchainWriteHandle, ResponseResult}, + service::{ + free::map_valid_alt_block_to_verified_block, + types::{BlockchainWriteHandle, ResponseResult}, + }, tables::{OpenTables, Tables, TablesMut}, - types::AltChainInfo, + types::{AltBlockHeight, AltChainInfo}, }; //---------------------------------------------------------------------------------------------------- init_write_service @@ -106,19 +107,23 @@ fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult { let env_inner = env.env_inner(); let mut tx_rw = env_inner.tx_rw()?; + // TODO: try blocks let result = { + // flush all the current alt blocks as they may reference blocks to be popped. crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?; let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; - + // generate a `ChainId` for the popped blocks. let old_main_chain_id = ChainId(rand::random()); + // pop the blocks let mut last_block_height = 0; for _ in 0..numb_blocks { (last_block_height, _, _) = crate::ops::block::pop_block(Some(old_main_chain_id), &mut tables_mut)?; } + // Update the alt_chain_info with the correct information. tables_mut.alt_chain_infos_mut().put( &old_main_chain_id.into(), &AltChainInfo { @@ -155,11 +160,14 @@ fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; let chain_info = tables_mut.alt_chain_infos().get(&chain_id.into())?; + // Although this doesn't guarantee the chain was popped from the main-chain, it's an easy + // thing for us to check. assert_eq!(Chain::from(chain_info.parent_chain), Chain::Main); let tob_block_height = crate::ops::blockchain::top_block_height(tables_mut.block_heights())?; + // pop any blocks that were added as part of a re-org. for _ in chain_info.common_ancestor_height..tob_block_height { crate::ops::block::pop_block(None, &mut tables_mut)?; } @@ -177,6 +185,7 @@ fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { }) .collect::>(); + // Add the old main chain blocks back to the main chain. for res_alt_block in alt_blocks { let alt_block = res_alt_block?; diff --git a/storage/blockchain/src/tests.rs b/storage/blockchain/src/tests.rs index 602391273..d57a37154 100644 --- a/storage/blockchain/src/tests.rs +++ b/storage/blockchain/src/tests.rs @@ -90,7 +90,10 @@ pub(crate) fn assert_all_tables_are_empty(env: &ConcreteEnv) { assert_eq!(crate::ops::tx::get_num_tx(tables.tx_ids()).unwrap(), 0); } -pub(crate) fn map_verified_block_to_alt(verified_block: VerifiedBlockInformation, chain_id: ChainId) -> AltBlockInformation { +pub(crate) fn map_verified_block_to_alt( + verified_block: VerifiedBlockInformation, + chain_id: ChainId, +) -> AltBlockInformation { AltBlockInformation { block: verified_block.block, block_blob: verified_block.block_blob, @@ -103,4 +106,4 @@ pub(crate) fn map_verified_block_to_alt(verified_block: VerifiedBlockInformation cumulative_difficulty: verified_block.cumulative_difficulty, chain_id, } -} \ No newline at end of file +} diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index c2a5517da..9f79c3a39 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -3,14 +3,15 @@ //! Tests that assert particular requests lead to particular //! responses are also tested in Cuprate's blockchain database crate. -//---------------------------------------------------------------------------------------------------- Import -use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; -use crate::{AltBlockInformation, ChainId}; use std::{ collections::{HashMap, HashSet}, ops::Range, }; +use crate::{AltBlockInformation, ChainId}; +//---------------------------------------------------------------------------------------------------- Import +use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; + //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. /// @@ -92,12 +93,14 @@ pub enum BlockchainReadRequest { CompactChainHistory, /// A request to find the first unknown block ID in a list of block IDs. - //// + /// /// # Invariant /// The [`Vec`] containing the block IDs must be sorted in chronological block /// order, or else the returned response is unspecified and meaningless, /// as this request performs a binary search. FindFirstUnknown(Vec<[u8; 32]>), + /// A request for all alt blocks in the chain with the given [`ChainId`]. + AltBlocksInChain(ChainId), } //---------------------------------------------------------------------------------------------------- WriteRequest @@ -120,12 +123,14 @@ pub enum BlockchainWriteRequest { /// /// Input is the amount of blocks to pop. /// - /// This request flush all alt-chains from the cache before adding the popped blocks to the alt cache. + /// This request flushes all alt-chains from the cache before adding the popped blocks to the + /// alt cache. PopBlocks(usize), /// A request to reverse the re-org process. /// /// The inner value is the [`ChainId`] of the old main chain. /// + /// # Invariant /// It is invalid to call this with a [`ChainId`] that was not returned from [`BlockchainWriteRequest::PopBlocks`]. ReverseReorg(ChainId), /// A request to flush all alternative blocks. @@ -215,6 +220,11 @@ pub enum BlockchainResponse { /// This will be [`None`] if all blocks were known. FindFirstUnknown(Option<(usize, usize)>), + /// The response for [`BlockchainReadRequest::AltBlocksInChain`]. + /// + /// Contains all the alt blocks in the alt-chain in chronological order. + AltBlocksInChain(Vec), + //------------------------------------------------------ Writes /// A generic Ok response to indicate a request was successfully handled. /// From f92375f6a61d72fa802e4a75ca7cba3f8d968363 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 7 Sep 2024 02:07:23 +0100 Subject: [PATCH 17/46] clippy + fmt --- storage/blockchain/src/service/read.rs | 4 ++-- storage/blockchain/src/types.rs | 16 +++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 416e61372..835f2d6a6 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -14,13 +14,14 @@ use rayon::{ use thread_local::ThreadLocal; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; -use cuprate_database_service::{DatabaseReadService, init_thread_pool, ReaderThreads}; +use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThreads}; use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, }; +use crate::ops::alt_block::get_alt_block; use crate::{ ops::{ alt_block::{ @@ -43,7 +44,6 @@ use crate::{ AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId, }, }; -use crate::ops::alt_block::get_alt_block; //---------------------------------------------------------------------------------------------------- init_read_service /// Initialize the [`BlockchainReadHandle`] thread-pool backed by [`rayon`]. diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index 14917249f..13c449941 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -44,12 +44,12 @@ use std::num::NonZero; use bytemuck::{Pod, Zeroable}; - #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use cuprate_database::{Key, StorableVec}; use cuprate_types::{Chain, ChainId}; + //---------------------------------------------------------------------------------------------------- Aliases // These type aliases exist as many Monero-related types are the exact same. // For clarity, they're given type aliases as to not confuse them. @@ -334,17 +334,15 @@ pub struct RawChain(u64); impl From for RawChain { fn from(value: Chain) -> Self { match value { - Chain::Main => RawChain(0), - Chain::Alt(chain_id) => RawChain(chain_id.0.get()), + Chain::Main => Self(0), + Chain::Alt(chain_id) => Self(chain_id.0.get()), } } } impl From for Chain { fn from(value: RawChain) -> Self { - NonZero::new(value.0) - .map(|id| Chain::Alt(ChainId(id))) - .unwrap_or(Chain::Main) + NonZero::new(value.0).map_or(Self::Main, |id| Self::Alt(ChainId(id))) } } @@ -352,7 +350,7 @@ impl From for RawChain { fn from(value: RawChainId) -> Self { assert_ne!(value.0, 0); - RawChain(value.0) + Self(value.0) } } @@ -363,13 +361,13 @@ pub struct RawChainId(u64); impl From for RawChainId { fn from(value: ChainId) -> Self { - RawChainId(value.0.get()) + Self(value.0.get()) } } impl From for ChainId { fn from(value: RawChainId) -> Self { - ChainId(NonZero::new(value.0).expect("RawChainId mut not have a value of `0`")) + Self(NonZero::new(value.0).expect("RawChainId mut not have a value of `0`")) } } From a864f934be9f2e42cbda24d66dfb83ee9ff84f91 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 7 Sep 2024 02:45:45 +0100 Subject: [PATCH 18/46] document types --- storage/blockchain/src/types.rs | 164 ++++++++++++++++++++++++++++++-- types/src/blockchain.rs | 4 - 2 files changed, 157 insertions(+), 11 deletions(-) diff --git a/storage/blockchain/src/types.rs b/storage/blockchain/src/types.rs index 13c449941..e96656951 100644 --- a/storage/blockchain/src/types.rs +++ b/storage/blockchain/src/types.rs @@ -327,6 +327,29 @@ pub struct RctOutput { // TODO: local_index? //---------------------------------------------------------------------------------------------------- RawChain +/// [`Chain`] in a format which can be stored in the DB. +/// +/// Implements [`Into`] and [`From`] for [`Chain`]. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::Chain; +/// +/// // Assert Storable is correct. +/// let a: RawChain = Chain::Main.into(); +/// let b = Storable::as_bytes(&a); +/// let c: RawChain = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 8); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(transparent)] pub struct RawChain(u64); @@ -348,6 +371,7 @@ impl From for Chain { impl From for RawChain { fn from(value: RawChainId) -> Self { + // A [`ChainID`] with an inner value of `0` is invalid. assert_ne!(value.0, 0); Self(value.0) @@ -355,6 +379,29 @@ impl From for RawChain { } //---------------------------------------------------------------------------------------------------- RawChainId +/// [`ChainId`] in a format which can be stored in the DB. +/// +/// Implements [`Into`] and [`From`] for [`ChainId`]. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::ChainId; +/// +/// // Assert Storable is correct. +/// let a: RawChainId = ChainId(10.try_into().unwrap()).into(); +/// let b = Storable::as_bytes(&a); +/// let c: RawChainId = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 8); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(transparent)] pub struct RawChainId(u64); @@ -374,31 +421,112 @@ impl From for ChainId { impl Key for RawChainId {} //---------------------------------------------------------------------------------------------------- AltChainInfo +/// Information on an alternative chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::Chain; +/// +/// // Assert Storable is correct. +/// let a: AltChainInfo = AltChainInfo { +/// parent_chain: Chain::Main.into(), +/// common_ancestor_height: 0, +/// chain_height: 1, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: AltChainInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 24); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct AltChainInfo { + /// The chain this alt chain forks from. pub parent_chain: RawChain, + /// The height of the first block we share with the parent chain. pub common_ancestor_height: usize, + /// The chain height of the blocks in this alt chain. pub chain_height: usize, } //---------------------------------------------------------------------------------------------------- AltBlockHeight +/// Represents the height of a block on an alt-chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// use cuprate_types::ChainId; +/// +/// // Assert Storable is correct. +/// let a: AltBlockHeight = AltBlockHeight { +/// chain_id: ChainId(1.try_into().unwrap()).into(), +/// height: 1, +/// }; +/// let b = Storable::as_bytes(&a); +/// let c: AltBlockHeight = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 16); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct AltBlockHeight { + /// The [`ChainId`] of the chain this alt block is on, in raw form. pub chain_id: RawChainId, + /// The height of this alt-block. pub height: usize, } impl Key for AltBlockHeight {} //---------------------------------------------------------------------------------------------------- CompactAltBlockInfo +/// Represents information on an alt-chain. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// +/// // Assert Storable is correct. +/// let a: CompactAltBlockInfo = CompactAltBlockInfo { +/// block_hash: [1; 32], +/// pow_hash: [2; 32], +/// height: 10, +/// weight: 20, +/// long_term_weight: 30, +/// cumulative_difficulty_low: 40, +/// cumulative_difficulty_high: 50, +/// }; +/// +/// let b = Storable::as_bytes(&a); +/// let c: CompactAltBlockInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 104); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct CompactAltBlockInfo { /// The block's hash. - /// - /// [`Block::hash`]. pub block_hash: [u8; 32], /// The block's proof-of-work hash. pub pow_hash: [u8; 32], @@ -408,24 +536,46 @@ pub struct CompactAltBlockInfo { pub weight: usize, /// The long term block weight, which is the weight factored in with previous block weights. pub long_term_weight: usize, - /// The cumulative difficulty of all blocks up until and including this block. + /// The low 64 bits of the cumulative difficulty. pub cumulative_difficulty_low: u64, + /// The high 64 bits of the cumulative difficulty. pub cumulative_difficulty_high: u64, } //---------------------------------------------------------------------------------------------------- AltTransactionInfo +/// Represents information on an alt transaction. +/// +/// ```rust +/// # use std::borrow::*; +/// # use cuprate_blockchain::{*, types::*}; +/// use cuprate_database::Storable; +/// +/// // Assert Storable is correct. +/// let a: AltTransactionInfo = AltTransactionInfo { +/// tx_weight: 1, +/// fee: 6, +/// tx_hash: [6; 32], +/// }; +/// +/// let b = Storable::as_bytes(&a); +/// let c: AltTransactionInfo = Storable::from_bytes(b); +/// assert_eq!(a, c); +/// ``` +/// +/// # Size & Alignment +/// ```rust +/// # use cuprate_blockchain::types::*; +/// assert_eq!(size_of::(), 48); +/// assert_eq!(align_of::(), 8); +/// ``` #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Pod, Zeroable)] #[repr(C)] pub struct AltTransactionInfo { /// The transaction's weight. - /// - /// [`Transaction::weight`]. pub tx_weight: usize, /// The transaction's total fees. pub fee: u64, /// The transaction's hash. - /// - /// [`Transaction::hash`]. pub tx_hash: [u8; 32], } diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 9f79c3a39..6c7ecf37b 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -105,10 +105,6 @@ pub enum BlockchainReadRequest { //---------------------------------------------------------------------------------------------------- WriteRequest /// A write request to the blockchain database. -/// -/// There is currently only 1 write request to the database, -/// as such, the only valid [`BlockchainResponse`] to this request is -/// the proper response for a [`BlockchainResponse::WriteBlockOk`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BlockchainWriteRequest { /// Request that a block be written to the database. From 6119972fe81ff9969a64cc07cbcb2f8bd7fc9220 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 8 Sep 2024 02:08:20 +0100 Subject: [PATCH 19/46] move tx_fee to helper --- helper/Cargo.toml | 4 ++- helper/src/lib.rs | 2 ++ helper/src/tx_utils.rs | 34 ++++++++++++++++++++++++ storage/blockchain/src/free.rs | 32 ----------------------- storage/blockchain/src/ops/block.rs | 8 +++--- test-utils/Cargo.toml | 2 +- test-utils/src/data/statics.rs | 40 ++++------------------------- 7 files changed, 50 insertions(+), 72 deletions(-) create mode 100644 helper/src/tx_utils.rs diff --git a/helper/Cargo.toml b/helper/Cargo.toml index c74e40fd4..4bd17ca89 100644 --- a/helper/Cargo.toml +++ b/helper/Cargo.toml @@ -9,8 +9,9 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/consensus" [features] +# TODO: I don't think this is a good idea # All features on by default. -default = ["std", "atomic", "asynch", "cast", "fs", "num", "map", "time", "thread", "constants"] +default = ["std", "atomic", "asynch", "cast", "fs", "num", "map", "time", "thread", "constants", "tx-utils"] std = [] atomic = ["dep:crossbeam"] asynch = ["dep:futures", "dep:rayon"] @@ -21,6 +22,7 @@ num = [] map = ["cast", "dep:monero-serai"] time = ["dep:chrono", "std"] thread = ["std", "dep:target_os_lib"] +tx-utils = ["dep:monero-serai"] [dependencies] crossbeam = { workspace = true, optional = true } diff --git a/helper/src/lib.rs b/helper/src/lib.rs index de0d95558..e82ec827d 100644 --- a/helper/src/lib.rs +++ b/helper/src/lib.rs @@ -31,6 +31,8 @@ pub mod thread; #[cfg(feature = "time")] pub mod time; +#[cfg(feature = "tx-utils")] +pub mod tx_utils; //---------------------------------------------------------------------------------------------------- Private Usage //---------------------------------------------------------------------------------------------------- diff --git a/helper/src/tx_utils.rs b/helper/src/tx_utils.rs new file mode 100644 index 000000000..aeccf32b2 --- /dev/null +++ b/helper/src/tx_utils.rs @@ -0,0 +1,34 @@ +//! Utils for working with [`Transaction`] + +use monero_serai::transaction::{Input, Transaction}; + +/// Calculates the fee of the [`Transaction`]. +/// +/// # Panics +/// This will panic if the inputs overflow or the transaction outputs too much, so should only +/// be used on known to be valid txs. +pub fn tx_fee(tx: &Transaction) -> u64 { + let mut fee = 0_u64; + + match &tx { + Transaction::V1 { prefix, .. } => { + for input in &prefix.inputs { + match input { + Input::Gen(_) => return 0, + Input::ToKey { amount, .. } => { + fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); + } + } + } + + for output in &prefix.outputs { + fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); + } + } + Transaction::V2 { proofs, .. } => { + fee = proofs.as_ref().unwrap().base.fee; + } + }; + + fee +} diff --git a/storage/blockchain/src/free.rs b/storage/blockchain/src/free.rs index 20d56226d..8288e65f7 100644 --- a/storage/blockchain/src/free.rs +++ b/storage/blockchain/src/free.rs @@ -1,6 +1,5 @@ //! General free functions (related to the database). -use monero_serai::transaction::{Input, Transaction}; //---------------------------------------------------------------------------------------------------- Import use cuprate_database::{ConcreteEnv, Env, EnvInner, InitError, RuntimeError, TxRw}; @@ -62,37 +61,6 @@ pub fn open(config: Config) -> Result { Ok(env) } -//---------------------------------------------------------------------------------------------------- Tx Fee -/// Calculates the fee of the [`Transaction`]. -/// -/// # Panics -/// This will panic if the inputs overflow or the transaction outputs too much. -pub(crate) fn tx_fee(tx: &Transaction) -> u64 { - let mut fee = 0_u64; - - match &tx { - Transaction::V1 { prefix, .. } => { - for input in &prefix.inputs { - match input { - Input::Gen(_) => return 0, - Input::ToKey { amount, .. } => { - fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); - } - } - } - - for output in &prefix.outputs { - fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); - } - } - Transaction::V2 { proofs, .. } => { - fee = proofs.as_ref().unwrap().base.fee; - } - }; - - fee -} - //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 45bab41cf..295869257 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -7,16 +7,18 @@ use monero_serai::block::{Block, BlockHeader}; use cuprate_database::{ RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, }; -use cuprate_helper::map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}; +use cuprate_helper::{ + map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}, + tx_utils::tx_fee, +}; use cuprate_types::{ AltBlockInformation, ChainId, ExtendedBlockHeader, HardFork, VerifiedBlockInformation, VerifiedTransactionInformation, }; -use crate::free::tx_fee; -use crate::ops::alt_block; use crate::{ ops::{ + alt_block, blockchain::{chain_height, cumulative_generated_coins}, macros::doc_error, output::get_rct_num_outputs, diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index a96a9cfcd..9c64bd81b 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Boog900", "hinto-janai"] [dependencies] cuprate-types = { path = "../types" } -cuprate-helper = { path = "../helper", features = ["map"] } +cuprate-helper = { path = "../helper", features = ["map", "tx-utils"] } cuprate-wire = { path = "../net/wire" } cuprate-p2p-core = { path = "../p2p/p2p-core", features = ["borsh"] } diff --git a/test-utils/src/data/statics.rs b/test-utils/src/data/statics.rs index 8b98171a2..a45cc13fa 100644 --- a/test-utils/src/data/statics.rs +++ b/test-utils/src/data/statics.rs @@ -8,12 +8,12 @@ //---------------------------------------------------------------------------------------------------- Import use std::sync::LazyLock; -use cuprate_helper::map::combine_low_high_bits_to_u128; -use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; use hex_literal::hex; -use monero_serai::transaction::Input; use monero_serai::{block::Block, transaction::Transaction}; +use cuprate_helper::{map::combine_low_high_bits_to_u128, tx_utils::tx_fee}; +use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; + use crate::data::constants::{ BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D, TX_9E3F73, TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440, @@ -110,36 +110,6 @@ fn to_tx_verification_data(tx_blob: impl AsRef<[u8]>) -> VerifiedTransactionInfo } } -/// Calculates the fee of the [`Transaction`]. -/// -/// # Panics -/// This will panic if the inputs overflow or the transaction outputs too much. -pub fn tx_fee(tx: &Transaction) -> u64 { - let mut fee = 0_u64; - - match &tx { - Transaction::V1 { prefix, .. } => { - for input in &prefix.inputs { - match input { - Input::Gen(_) => return 0, - Input::ToKey { amount, .. } => { - fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); - } - } - } - - for output in &prefix.outputs { - fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); - } - } - Transaction::V2 { proofs, .. } => { - fee = proofs.as_ref().unwrap().base.fee; - } - }; - - fee -} - //---------------------------------------------------------------------------------------------------- Blocks /// Generate a `static LazyLock`. /// @@ -311,12 +281,12 @@ transaction_verification_data! { //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod tests { - use super::*; - use pretty_assertions::assert_eq; use crate::rpc::client::HttpRpcClient; + use super::*; + /// Assert the defined blocks are the same compared to ones received from a local RPC call. #[ignore] // FIXME: doesn't work in CI, we need a real unrestricted node #[tokio::test] From b211210fa24c220dac1c46824430e1a1accaaebf Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 8 Sep 2024 15:34:30 +0100 Subject: [PATCH 20/46] more doc updates --- storage/blockchain/src/ops/alt_block/block.rs | 7 ++----- storage/blockchain/src/ops/alt_block/mod.rs | 6 +++--- storage/blockchain/src/ops/alt_block/tx.rs | 8 +++++--- storage/blockchain/src/service/free.rs | 14 ++++++++++---- storage/blockchain/src/service/read.rs | 7 +++---- storage/blockchain/src/service/write.rs | 17 +++++++++-------- storage/blockchain/src/tables.rs | 16 ++++++++++++++++ test-utils/src/data/mod.rs | 6 ++---- test-utils/src/rpc/client.rs | 13 ++++++------- 9 files changed, 56 insertions(+), 38 deletions(-) diff --git a/storage/blockchain/src/ops/alt_block/block.rs b/storage/blockchain/src/ops/alt_block/block.rs index 07878d55b..5bc052b74 100644 --- a/storage/blockchain/src/ops/alt_block/block.rs +++ b/storage/blockchain/src/ops/alt_block/block.rs @@ -24,12 +24,9 @@ use crate::{ /// /// # Panics /// This function will panic if: -/// - `block.height` is == `0` +/// - `alt_block.height` is == `0` +/// - `alt_block.txs.len()` != `alt_block.block.transactions.len()` /// -/// # Already exists -/// This function will operate normally even if `block` already -/// exists, i.e., this function will not return `Err` even if you -/// call this function infinitely with the same block. pub fn add_alt_block( alt_block: &AltBlockInformation, tables: &mut impl TablesMut, diff --git a/storage/blockchain/src/ops/alt_block/mod.rs b/storage/blockchain/src/ops/alt_block/mod.rs index 36e4768ec..96c1cecff 100644 --- a/storage/blockchain/src/ops/alt_block/mod.rs +++ b/storage/blockchain/src/ops/alt_block/mod.rs @@ -18,8 +18,8 @@ //! ``` //! //! In that example if we were to receive an alt-block which immediately follows the top block of `ChainID(X)` -//! then that block will also be stored under `ChainID(X)`. However if it follows from another block from `ChainID(X)` -//! we will split into a chain with a different ID. +//! then that block will also be stored under `ChainID(X)`. However, if it follows from another block from `ChainID(X)` +//! we will split into a chain with a different ID: //! //! ```text //! | @@ -39,7 +39,7 @@ //! [`get_alt_chain_history_ranges`] covers this and is the method to get the ranges of heights needed from each [`ChainID`](cuprate_types::ChainId) //! to get all the alt-blocks in a given [`ChainID`](cuprate_types::ChainId). //! -//! Although this should be kept in mind as a possibility because Cuprate's block downloader will only track a single chain it is +//! Although this should be kept in mind as a possibility, because Cuprate's block downloader will only track a single chain it is //! unlikely that we will be tracking [`ChainID`](cuprate_types::ChainId) that don't immediately connect to the main-chain. //! //! ## Why not use block's previous field? diff --git a/storage/blockchain/src/ops/alt_block/tx.rs b/storage/blockchain/src/ops/alt_block/tx.rs index 5671e4b45..aa2e82f40 100644 --- a/storage/blockchain/src/ops/alt_block/tx.rs +++ b/storage/blockchain/src/ops/alt_block/tx.rs @@ -4,9 +4,11 @@ use monero_serai::transaction::Transaction; use cuprate_database::{DatabaseRo, DatabaseRw, RuntimeError, StorableVec}; use cuprate_types::VerifiedTransactionInformation; -use crate::ops::macros::{doc_add_alt_block_inner_invariant, doc_error}; -use crate::tables::{Tables, TablesMut}; -use crate::types::{AltTransactionInfo, TxHash}; +use crate::{ + ops::macros::{doc_add_alt_block_inner_invariant, doc_error}, + tables::{Tables, TablesMut}, + types::{AltTransactionInfo, TxHash}, +}; /// Adds a [`VerifiedTransactionInformation`] form an alt-block to the DB, if /// that transaction is not already in the DB. diff --git a/storage/blockchain/src/service/free.rs b/storage/blockchain/src/service/free.rs index 7a4e6ce6d..d12844f35 100644 --- a/storage/blockchain/src/service/free.rs +++ b/storage/blockchain/src/service/free.rs @@ -3,13 +3,14 @@ //---------------------------------------------------------------------------------------------------- Import use std::sync::Arc; -use crate::service::{init_read_service, init_write_service}; +use cuprate_database::{ConcreteEnv, InitError}; +use cuprate_types::{AltBlockInformation, VerifiedBlockInformation}; + use crate::{ config::Config, service::types::{BlockchainReadHandle, BlockchainWriteHandle}, }; -use cuprate_database::{ConcreteEnv, InitError}; -use cuprate_types::{AltBlockInformation, VerifiedBlockInformation}; +use crate::service::{init_read_service, init_write_service}; //---------------------------------------------------------------------------------------------------- Init #[cold] @@ -81,7 +82,12 @@ pub(super) const fn compact_history_genesis_not_included INITIAL_BLOCKS && !(top_block_height - INITIAL_BLOCKS + 2).is_power_of_two() } -//---------------------------------------------------------------------------------------------------- Compact history +//---------------------------------------------------------------------------------------------------- Map Block +/// Maps [`AltBlockInformation`] to [`VerifiedBlockInformation`] +/// +/// # Panics +/// This will panic if the block is invalid, so should only be used on blocks that have been popped from +/// the main-chain. pub(super) fn map_valid_alt_block_to_verified_block( alt_block: AltBlockInformation, ) -> VerifiedBlockInformation { diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 835f2d6a6..3d0379e05 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -1,11 +1,11 @@ //! Database reader thread-pool definitions and logic. +//---------------------------------------------------------------------------------------------------- Import use std::{ collections::{HashMap, HashSet}, sync::Arc, }; -//---------------------------------------------------------------------------------------------------- Import use rayon::{ iter::{IntoParallelIterator, ParallelIterator}, prelude::*, @@ -14,18 +14,17 @@ use rayon::{ use thread_local::ThreadLocal; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; -use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThreads}; +use cuprate_database_service::{DatabaseReadService, init_thread_pool, ReaderThreads}; use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, Chain, ChainId, ExtendedBlockHeader, OutputOnChain, }; -use crate::ops::alt_block::get_alt_block; use crate::{ ops::{ alt_block::{ - get_alt_block_extended_header_from_height, get_alt_block_hash, + get_alt_block, get_alt_block_extended_header_from_height, get_alt_block_hash, get_alt_chain_history_ranges, }, block::{ diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 849a3030f..723843f53 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use cuprate_database::{ConcreteEnv, DatabaseRo, DatabaseRw, Env, EnvInner, RuntimeError, TxRw}; use cuprate_database_service::DatabaseWriteHandle; use cuprate_types::{ - blockchain::{BlockchainResponse, BlockchainWriteRequest}, - AltBlockInformation, Chain, ChainId, VerifiedBlockInformation, + AltBlockInformation, + blockchain::{BlockchainResponse, BlockchainWriteRequest}, Chain, ChainId, VerifiedBlockInformation, }; use crate::{ @@ -107,8 +107,8 @@ fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult { let env_inner = env.env_inner(); let mut tx_rw = env_inner.tx_rw()?; - // TODO: try blocks - let result = { + // TODO: turn this function into a try block once stable. + let mut result = || { // flush all the current alt blocks as they may reference blocks to be popped. crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw)?; @@ -136,7 +136,7 @@ fn pop_blocks(env: &ConcreteEnv, numb_blocks: usize) -> ResponseResult { Ok(old_main_chain_id) }; - match result { + match result() { Ok(old_main_chain_id) => { TxRw::commit(tx_rw)?; Ok(BlockchainResponse::PopBlocks(old_main_chain_id)) @@ -156,7 +156,8 @@ fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { let env_inner = env.env_inner(); let tx_rw = env_inner.tx_rw()?; - let result = { + // TODO: turn this function into a try block once stable. + let result = || { let mut tables_mut = env_inner.open_tables_mut(&tx_rw)?; let chain_info = tables_mut.alt_chain_infos().get(&chain_id.into())?; @@ -197,7 +198,7 @@ fn reverse_reorg(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { Ok(()) }; - match result { + match result() { Ok(()) => { TxRw::commit(tx_rw)?; Ok(BlockchainResponse::Ok) @@ -218,7 +219,7 @@ fn flush_alt_blocks(env: &ConcreteEnv) -> ResponseResult { let env_inner = env.env_inner(); let mut tx_rw = env_inner.tx_rw()?; - let result = { crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw) }; + let result = crate::ops::alt_block::flush_alt_blocks(&env_inner, &mut tx_rw); match result { Ok(()) => { diff --git a/storage/blockchain/src/tables.rs b/storage/blockchain/src/tables.rs index fa568ae6b..74d2e591e 100644 --- a/storage/blockchain/src/tables.rs +++ b/storage/blockchain/src/tables.rs @@ -131,21 +131,37 @@ cuprate_database::define_tables! { 14 => TxUnlockTime, TxId => UnlockTime, + /// Information on alt-chains. 15 => AltChainInfos, RawChainId => AltChainInfo, + /// Alt-block heights. + /// + /// Contains the height of all alt-blocks. 16 => AltBlockHeights, BlockHash => AltBlockHeight, + /// Alt-block information. + /// + /// Contains information on all alt-blocks. 17 => AltBlocksInfo, AltBlockHeight => CompactAltBlockInfo, + /// Alt-block blobs. + /// + /// Contains the raw bytes of all alt-blocks. 18 => AltBlockBlobs, AltBlockHeight => BlockBlob, + /// Alt-Block transactions blobs. + /// + /// Contains the raw bytes of alt transactions, if those transactions are not in the main-chain. 19 => AltTransactionBlobs, TxHash => TxBlob, + /// Alt-Block transactions information. + /// + /// Contains information on all alt transactions, even if they are in the main-chain. 20 => AltTransactionInfos, TxHash => AltTransactionInfo, } diff --git a/test-utils/src/data/mod.rs b/test-utils/src/data/mod.rs index b9d42fb86..3be409fe1 100644 --- a/test-utils/src/data/mod.rs +++ b/test-utils/src/data/mod.rs @@ -25,13 +25,11 @@ //! let tx: VerifiedTransactionInformation = TX_V1_SIG0.clone(); //! ``` -mod constants; pub use constants::{ BLOCK_43BD1F, BLOCK_5ECB7E, BLOCK_BBD604, BLOCK_F91043, TX_2180A8, TX_3BC7FF, TX_84D48D, TX_9E3F73, TX_B6B439, TX_D7FEBD, TX_E2D393, TX_E57440, }; +pub use statics::{BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3}; +mod constants; mod statics; -pub use statics::{ - tx_fee, BLOCK_V16_TX0, BLOCK_V1_TX2, BLOCK_V9_TX3, TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3, -}; diff --git a/test-utils/src/rpc/client.rs b/test-utils/src/rpc/client.rs index fbe6fb9e4..3711334b7 100644 --- a/test-utils/src/rpc/client.rs +++ b/test-utils/src/rpc/client.rs @@ -1,18 +1,16 @@ //! HTTP RPC client. //---------------------------------------------------------------------------------------------------- Use -use serde::Deserialize; -use serde_json::json; -use tokio::task::spawn_blocking; - use monero_rpc::Rpc; use monero_serai::block::Block; use monero_simple_request_rpc::SimpleRequestRpc; +use serde::Deserialize; +use serde_json::json; +use tokio::task::spawn_blocking; +use cuprate_helper::tx_utils::tx_fee; use cuprate_types::{VerifiedBlockInformation, VerifiedTransactionInformation}; -use crate::data::tx_fee; - //---------------------------------------------------------------------------------------------------- Constants /// The default URL used for Monero RPC connections. pub const LOCALHOST_RPC_URL: &str = "http://127.0.0.1:18081"; @@ -184,9 +182,10 @@ impl HttpRpcClient { //---------------------------------------------------------------------------------------------------- TESTS #[cfg(test)] mod tests { - use super::*; use hex_literal::hex; + use super::*; + /// Assert the default address is localhost. #[tokio::test] async fn localhost() { From 68807e7563a86ac189b005797a687dd513a3b10d Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 8 Sep 2024 15:37:01 +0100 Subject: [PATCH 21/46] fmt --- storage/blockchain/src/service/free.rs | 6 ++++-- storage/blockchain/src/service/read.rs | 2 +- storage/blockchain/src/service/write.rs | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/storage/blockchain/src/service/free.rs b/storage/blockchain/src/service/free.rs index d12844f35..d8a878c93 100644 --- a/storage/blockchain/src/service/free.rs +++ b/storage/blockchain/src/service/free.rs @@ -8,9 +8,11 @@ use cuprate_types::{AltBlockInformation, VerifiedBlockInformation}; use crate::{ config::Config, - service::types::{BlockchainReadHandle, BlockchainWriteHandle}, + service::{ + init_read_service, init_write_service, + types::{BlockchainReadHandle, BlockchainWriteHandle}, + }, }; -use crate::service::{init_read_service, init_write_service}; //---------------------------------------------------------------------------------------------------- Init #[cold] diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 3d0379e05..73b2d2205 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -14,7 +14,7 @@ use rayon::{ use thread_local::ThreadLocal; use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; -use cuprate_database_service::{DatabaseReadService, init_thread_pool, ReaderThreads}; +use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThreads}; use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, diff --git a/storage/blockchain/src/service/write.rs b/storage/blockchain/src/service/write.rs index 723843f53..4038d94a5 100644 --- a/storage/blockchain/src/service/write.rs +++ b/storage/blockchain/src/service/write.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use cuprate_database::{ConcreteEnv, DatabaseRo, DatabaseRw, Env, EnvInner, RuntimeError, TxRw}; use cuprate_database_service::DatabaseWriteHandle; use cuprate_types::{ - AltBlockInformation, - blockchain::{BlockchainResponse, BlockchainWriteRequest}, Chain, ChainId, VerifiedBlockInformation, + blockchain::{BlockchainResponse, BlockchainWriteRequest}, + AltBlockInformation, Chain, ChainId, VerifiedBlockInformation, }; use crate::{ From c03065bcd5569a631cc28687667445bfd0e5e151 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 8 Sep 2024 18:42:25 +0100 Subject: [PATCH 22/46] fix imports --- types/src/blockchain.rs | 10 ++++++---- types/src/types.rs | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index 6c7ecf37b..f246e59a5 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -2,15 +2,17 @@ //! //! Tests that assert particular requests lead to particular //! responses are also tested in Cuprate's blockchain database crate. - +//! +//---------------------------------------------------------------------------------------------------- Import use std::{ collections::{HashMap, HashSet}, ops::Range, }; -use crate::{AltBlockInformation, ChainId}; -//---------------------------------------------------------------------------------------------------- Import -use crate::types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}; +use crate::{ + types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}, + AltBlockInformation, ChainId, +}; //---------------------------------------------------------------------------------------------------- ReadRequest /// A read request to the blockchain database. diff --git a/types/src/types.rs b/types/src/types.rs index c6e83d093..a60ce6c60 100644 --- a/types/src/types.rs +++ b/types/src/types.rs @@ -1,7 +1,8 @@ //! Various shared data types in Cuprate. -use std::num::NonZero; //---------------------------------------------------------------------------------------------------- Import +use std::num::NonZero; + use curve25519_dalek::edwards::EdwardsPoint; use monero_serai::{ block::Block, From 1831fa62747eec6a6a09b4f39c8af67e02892ae2 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Mon, 9 Sep 2024 01:14:00 +0100 Subject: [PATCH 23/46] remove config files --- .idea/vcs.xml | 6 - .idea/workspace.xml | 637 -------------------------------------------- 2 files changed, 643 deletions(-) delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfb..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 8ddf4f0ee..000000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,637 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - "associatedIndex": 4 -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1722869508802 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From da78cbdb7aaabf659508811a557f9eb2afed65db Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Mon, 9 Sep 2024 17:04:45 +0100 Subject: [PATCH 24/46] fix merge errors --- Cargo.lock | 41 +++++++++++++++++++++++------------- binaries/cuprated/Cargo.toml | 2 +- consensus/src/lib.rs | 2 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bb4612af..7ccfc6be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,7 +1010,7 @@ dependencies = [ [[package]] name = "dalek-ff-group" version = "0.4.1" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "crypto-bigint", "curve25519-dalek", @@ -1165,7 +1165,7 @@ dependencies = [ [[package]] name = "flexible-transcript" version = "0.3.2" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "blake2", "digest", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "monero-address" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-io", @@ -1845,7 +1845,7 @@ dependencies = [ [[package]] name = "monero-borromean" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1858,7 +1858,7 @@ dependencies = [ [[package]] name = "monero-bulletproofs" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1873,7 +1873,7 @@ dependencies = [ [[package]] name = "monero-clsag" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "dalek-ff-group", @@ -1893,7 +1893,7 @@ dependencies = [ [[package]] name = "monero-generators" version = "0.4.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "dalek-ff-group", @@ -1907,7 +1907,7 @@ dependencies = [ [[package]] name = "monero-io" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "std-shims", @@ -1916,7 +1916,7 @@ dependencies = [ [[package]] name = "monero-mlsag" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1930,7 +1930,7 @@ dependencies = [ [[package]] name = "monero-primitives" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "monero-generators", @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "monero-rpc" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "async-trait", "curve25519-dalek", @@ -1960,7 +1960,7 @@ dependencies = [ [[package]] name = "monero-serai" version = "0.1.4-alpha" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "curve25519-dalek", "hex-literal", @@ -1978,7 +1978,7 @@ dependencies = [ [[package]] name = "monero-simple-request-rpc" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "async-trait", "digest_auth", @@ -2646,6 +2646,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2658,7 +2667,7 @@ dependencies = [ [[package]] name = "simple-request" version = "0.1.0" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "http-body-util", "hyper", @@ -2714,7 +2723,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "std-shims" version = "0.1.1" -source = "git+https://github.com/Cuprate/serai.git?rev=d5205ce#d5205ce2319e09414eb91d12cf38e83a08165f79" +source = "git+https://github.com/Cuprate/serai.git?rev=50686e8#50686e84022edbd0065d2af655ea4aa5faf486b8" dependencies = [ "hashbrown", "spin", @@ -3015,6 +3024,8 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "sharded-slab", + "thread_local", "tracing-core", ] diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index 0c70a31da..e74dd0438 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -69,7 +69,7 @@ tokio-util = { workspace = true } tokio-stream = { workspace = true } tokio = { workspace = true } tower = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["std", "fmt"] } tracing = { workspace = true } #[lints] diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 18de7dc4f..e97523671 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -28,7 +28,7 @@ pub use transactions::{TxVerifierService, VerifyTxRequest, VerifyTxResponse}; // re-export. pub use cuprate_consensus_rules::genesis::generate_genesis_block; -pub use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +pub use cuprate_types::{blockchain::{BlockchainReadRequest, BlockchainResponse}, HardFork}; /// An Error returned from one of the consensus services. #[derive(Debug, thiserror::Error)] From d4e0e301333026bc599f52bc031b9594a04ac67d Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Mon, 9 Sep 2024 20:13:22 +0100 Subject: [PATCH 25/46] fix generated coins --- Cargo.lock | 37 +++++++++++++++++++++++++++++ binaries/cuprated/Cargo.toml | 2 +- consensus/src/lib.rs | 5 +++- storage/blockchain/src/ops/block.rs | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0035f074a..3300a9783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1989,6 +1989,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2036,6 +2046,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "page_size" version = "0.6.0" @@ -3017,6 +3033,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", ] [[package]] @@ -3025,9 +3053,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "nu-ansi-term", "sharded-slab", + "smallvec", "thread_local", "tracing-core", + "tracing-log", ] [[package]] @@ -3101,6 +3132,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index e74dd0438..125961b4d 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -69,7 +69,7 @@ tokio-util = { workspace = true } tokio-stream = { workspace = true } tokio = { workspace = true } tower = { workspace = true } -tracing-subscriber = { workspace = true, features = ["std", "fmt"] } +tracing-subscriber = { workspace = true, features = ["std", "fmt", "default"] } tracing = { workspace = true } #[lints] diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index e97523671..3753fcc9e 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -28,7 +28,10 @@ pub use transactions::{TxVerifierService, VerifyTxRequest, VerifyTxResponse}; // re-export. pub use cuprate_consensus_rules::genesis::generate_genesis_block; -pub use cuprate_types::{blockchain::{BlockchainReadRequest, BlockchainResponse}, HardFork}; +pub use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse}, + HardFork, +}; /// An Error returned from one of the consensus services. #[derive(Debug, thiserror::Error)] diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 295869257..8e0465202 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -93,7 +93,7 @@ pub fn add_block( let cumulative_generated_coins = cumulative_generated_coins(&block.height.saturating_sub(1), tables.block_infos())? - + block.generated_coins; + .saturating_add(block.generated_coins); let (cumulative_difficulty_low, cumulative_difficulty_high) = split_u128_into_low_high_bits(block.cumulative_difficulty); From 915633fe70a356f0452be675e4f3fce37ba2d6d7 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 12 Sep 2024 02:24:07 +0100 Subject: [PATCH 26/46] handle more p2p requests + alt blocks --- Cargo.lock | 1 + binaries/cuprated/Cargo.toml | 2 +- binaries/cuprated/src/blockchain/manager.rs | 90 +++++++++- .../src/blockchain/manager/batch_handler.rs | 105 ++++------- .../src/blockchain/manager/handler.rs | 163 ++++++++++++++++++ binaries/cuprated/src/main.rs | 4 +- binaries/cuprated/src/p2p/request_handler.rs | 114 ++++++++++-- consensus/src/block.rs | 14 ++ p2p/address-book/src/lib.rs | 11 +- p2p/p2p/src/constants.rs | 7 +- p2p/p2p/src/lib.rs | 2 +- storage/blockchain/Cargo.toml | 1 + storage/blockchain/src/ops/block.rs | 45 ++++- storage/blockchain/src/service/read.rs | 109 +++++++++++- types/src/blockchain.rs | 42 ++++- 15 files changed, 601 insertions(+), 109 deletions(-) create mode 100644 binaries/cuprated/src/blockchain/manager/handler.rs diff --git a/Cargo.lock b/Cargo.lock index 3300a9783..e6b617587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,7 @@ version = "0.0.0" dependencies = [ "bitflags 2.5.0", "bytemuck", + "bytes", "cuprate-database", "cuprate-database-service", "cuprate-helper", diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index 125961b4d..43daabfb3 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -23,7 +23,7 @@ cuprate-p2p-core = { path = "../../p2p/p2p-core" } cuprate-dandelion-tower = { path = "../../p2p/dandelion-tower" } cuprate-async-buffer = { path = "../../p2p/async-buffer" } cuprate-address-book = { path = "../../p2p/address-book" } -cuprate-blockchain = { path = "../../storage/blockchain" } +cuprate-blockchain = { path = "../../storage/blockchain", features = ["service"] } cuprate-database-service = { path = "../../storage/service" } cuprate-txpool = { path = "../../storage/txpool" } cuprate-database = { path = "../../storage/database" } diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index e13c9073d..eaf8669f1 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,17 +1,27 @@ mod batch_handler; +mod handler; -use crate::blockchain::manager::batch_handler::handle_incoming_block_batch; use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::{BlockChainContextService, BlockVerifierService, TxVerifierService}; +use cuprate_consensus::context::RawBlockChainContext; +use cuprate_consensus::{ + BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, + BlockVerifierService, ExtendedConsensusError, TxVerifierService, VerifyBlockRequest, + VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, +}; use cuprate_p2p::block_downloader::BlockBatch; +use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +use cuprate_types::Chain; use futures::StreamExt; use tokio::sync::mpsc::Receiver; +use tower::{Service, ServiceExt}; +use tracing::error; pub struct BlockchainManager { blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, blockchain_context_service: BlockChainContextService, + cached_blockchain_context: RawBlockChainContext, block_verifier_service: BlockVerifierService< BlockChainContextService, TxVerifierService, @@ -34,20 +44,86 @@ impl BlockchainManager { blockchain_write_handle, blockchain_read_handle, blockchain_context_service, + cached_blockchain_context: todo!(), block_verifier_service, } } + async fn handle_incoming_main_chain_batch( + &mut self, + batch: BlockBatch, + ) -> Result<(), anyhow::Error> { + let VerifyBlockResponse::MainChainBatchPrepped(prepped) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { + blocks: batch.blocks, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + for (block, txs) in prepped { + let VerifyBlockResponse::MainChain(verified_block) = block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainPrepped { block, txs }) + .await + .unwrap() + else { + panic!("Incorrect response!"); + }; + + blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::Update(NewBlockData { + block_hash: verified_block.block_hash, + height: verified_block.height, + timestamp: verified_block.block.header.timestamp, + weight: verified_block.weight, + long_term_weight: verified_block.long_term_weight, + generated_coins: verified_block.generated_coins, + vote: HardFork::from_vote(verified_block.block.header.hardfork_signal), + cumulative_difficulty: verified_block.cumulative_difficulty, + })) + .await + .expect("TODO"); + + blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::WriteBlock(verified_block)) + .await + .expect("TODO"); + } + } + + async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { + let (first_block, _) = batch + .blocks + .first() + .expect("Block batch should not be empty"); + + if first_block.header.previous == self.cached_blockchain_context.top_hash { + todo!("Main chain") + } else { + todo!("Alt chain") + } + } + pub async fn run(mut self, mut batch_rx: Receiver) { loop { tokio::select! { Some(batch) = batch_rx.recv() => { - handle_incoming_block_batch( + self.handle_incoming_block_batch( batch, - &mut self.block_verifier_service, - &mut self.blockchain_context_service, - &mut self.blockchain_read_handle, - &mut self.blockchain_write_handle ).await; } else => { diff --git a/binaries/cuprated/src/blockchain/manager/batch_handler.rs b/binaries/cuprated/src/blockchain/manager/batch_handler.rs index c4a3d6e58..ea08af42a 100644 --- a/binaries/cuprated/src/blockchain/manager/batch_handler.rs +++ b/binaries/cuprated/src/blockchain/manager/batch_handler.rs @@ -3,6 +3,7 @@ use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::context::NewBlockData; +use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_consensus::{ BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, BlockVerifierService, BlockchainReadRequest, BlockchainResponse, ExtendedConsensusError, @@ -11,82 +12,17 @@ use cuprate_consensus::{ use cuprate_p2p::block_downloader::BlockBatch; use cuprate_types::blockchain::BlockchainWriteRequest; use cuprate_types::{Chain, HardFork}; +use rayon::prelude::*; use tower::{Service, ServiceExt}; use tracing::{debug, error, info}; -pub async fn handle_incoming_block_batch( - batch: BlockBatch, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_read_handle: &mut BlockchainReadHandle, - blockchain_write_handle: &mut BlockchainWriteHandle, -) where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - let (first_block, _) = batch - .blocks - .first() - .expect("Block batch should not be empty"); - - handle_incoming_block_batch_main_chain( - batch, - block_verifier_service, - blockchain_context_service, - blockchain_write_handle, - ) - .await; - - // TODO: alt block to the DB - /* - match blockchain_read_handle - .oneshot(BlockchainReadRequest::FindBlock( - first_block.header.previous, - )) - .await - { - Err(_) | Ok(BlockchainResponse::FindBlock(None)) => { - // The block downloader shouldn't be downloading orphan blocks - error!("Failed to find parent block for first block in batch."); - return; - } - Ok(BlockchainResponse::FindBlock(Some((chain, _)))) => match chain { - Chain::Main => { - handle_incoming_block_batch_main_chain( - batch, - block_verifier_service, - blockchain_context_service, - blockchain_write_handle, - ) - .await; - } - Chain::Alt(_) => todo!(), - }, - - Ok(_) => panic!("Blockchain service returned incorrect response"), - } - - */ -} - async fn handle_incoming_block_batch_main_chain( batch: BlockBatch, block_verifier_service: &mut BlockVerifierService, blockchain_context_service: &mut C, blockchain_write_handle: &mut BlockchainWriteHandle, -) where +) -> Result<(), anyhow::Error> +where C: Service< BlockChainContextRequest, Response = BlockChainContextResponse, @@ -114,8 +50,7 @@ async fn handle_incoming_block_batch_main_chain( .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { blocks: batch.blocks, }) - .await - .unwrap() + .await? else { panic!("Incorrect response!"); }; @@ -126,8 +61,7 @@ async fn handle_incoming_block_batch_main_chain( .await .expect("TODO") .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await - .unwrap() + .await? else { panic!("Incorrect response!"); }; @@ -158,3 +92,30 @@ async fn handle_incoming_block_batch_main_chain( .expect("TODO"); } } + +async fn handle_incoming_block_batch_alt_chain( + batch: BlockBatch, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_write_handle: &mut BlockchainWriteHandle, +) -> Result<(), anyhow::Error> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + for (block, txs) in batch.blocks { + alt_block_info.cumulative_difficulty + } +} diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs new file mode 100644 index 000000000..221acb817 --- /dev/null +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -0,0 +1,163 @@ +use crate::blockchain::types::ConsensusBlockchainReadHandle; +use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::transactions::new_tx_verification_data; +use cuprate_consensus::{ + BlockChainContextRequest, BlockChainContextResponse, BlockVerifierService, + ExtendedConsensusError, VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, + VerifyTxResponse, +}; +use cuprate_p2p::block_downloader::BlockBatch; +use cuprate_types::blockchain::{ + BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, +}; +use cuprate_types::AltBlockInformation; +use monero_serai::block::Block; +use monero_serai::transaction::Transaction; +use rayon::prelude::*; +use tower::{Service, ServiceExt}; + +async fn handle_incoming_alt_block( + block: Block, + txs: Vec, + current_cumulative_difficulty: u128, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_write_handle: &mut BlockchainWriteHandle, +) -> Result<(), anyhow::Error> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + let prepared_txs = txs + .into_par_iter() + .map(|tx| { + let tx = new_tx_verification_data(tx)?; + (tx.tx_hash, tx) + }) + .collect::>()?; + + let VerifyBlockResponse::AltChain(alt_block_info) = block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::AltChain { + block, + prepared_txs, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + if alt_block_info.cumulative_difficulty > current_cumulative_difficulty { + todo!("do re-org"); + } + + blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::WriteAltBlock(alt_block_info))?; + + Ok(()) +} + +async fn try_do_reorg( + top_alt_block: AltBlockInformation, + chain_height: usize, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_write_handle: &mut BlockchainWriteHandle, + blockchain_read_handle: &mut BlockchainReadHandle, +) -> Result<(), anyhow::Error> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + let BlockchainResponse::AltBlocksInChain(mut alt_blocks) = blockchain_read_handle + .ready() + .await + .expect("TODO") + .call(BlockchainReadRequest::AltBlocksInChain( + top_alt_block.chain_id, + )) + .await? + else { + panic!("Incorrect response!"); + }; + + alt_blocks.push(top_alt_block); + + let split_height = alt_blocks[0].height; + + let BlockchainResponse::PopBlocks(old_main_chain_id) = blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::PopBlocks( + chain_height - split_height + 1, + )) + .await? + else { + panic!("Incorrect response!"); + }; + + todo!() +} + +async fn verify_add_alt_blocks_to_main_chain( + alt_blocks: Vec, + block_verifier_service: &mut BlockVerifierService, + blockchain_context_service: &mut C, + blockchain_write_handle: &mut BlockchainWriteHandle, +) -> Result<(), anyhow::Error> +where + C: Service< + BlockChainContextRequest, + Response = BlockChainContextResponse, + Error = tower::BoxError, + > + Clone + + Send + + 'static, + C::Future: Send + 'static, + + TxV: Service + + Clone + + Send + + 'static, + TxV::Future: Send + 'static, +{ + let VerifyBlockResponse::AltChain(alt_block_info) = block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainPrepped { block, txs }) + .await? + else { + panic!("Incorrect response!"); + }; +} diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 62a7056a4..0e706b933 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -42,7 +42,9 @@ fn main() { .unwrap(); let net = cuprate_p2p::initialize_network( - p2p::request_handler::P2pProtocolRequestHandler, + p2p::request_handler::P2pProtocolRequestHandler { + blockchain_read_handle: bc_read_handle.clone(), + }, p2p::core_sync_svc::CoreSyncService(context_svc.clone()), config.clearnet_config(), ) diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index 76554e929..0a4412fa3 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -1,12 +1,23 @@ +use bytes::Bytes; use cuprate_p2p_core::{ProtocolRequest, ProtocolResponse}; use futures::future::BoxFuture; use futures::FutureExt; use std::task::{Context, Poll}; -use tower::Service; +use tower::{Service, ServiceExt}; use tracing::trace; +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_helper::cast::usize_to_u64; +use cuprate_helper::map::split_u128_into_low_high_bits; +use cuprate_p2p::constants::{MAX_BLOCKCHAIN_SUPPLEMENT_LEN, MAX_BLOCK_BATCH_LEN}; +use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +use cuprate_types::BlockCompleteEntry; +use cuprate_wire::protocol::{ChainRequest, ChainResponse, GetObjectsRequest, GetObjectsResponse}; + #[derive(Clone)] -pub struct P2pProtocolRequestHandler; +pub struct P2pProtocolRequestHandler { + pub(crate) blockchain_read_handle: BlockchainReadHandle, +} impl Service for P2pProtocolRequestHandler { type Response = ProtocolResponse; @@ -19,15 +30,98 @@ impl Service for P2pProtocolRequestHandler { fn call(&mut self, req: ProtocolRequest) -> Self::Future { match req { - ProtocolRequest::GetObjects(_) => trace!("TODO: GetObjects"), - ProtocolRequest::GetChain(_) => trace!("TODO: GetChain"), - ProtocolRequest::FluffyMissingTxs(_) => trace!("TODO: FluffyMissingTxs"), - ProtocolRequest::GetTxPoolCompliment(_) => trace!("TODO: GetTxPoolCompliment"), - ProtocolRequest::NewBlock(_) => trace!("TODO: NewBlock"), - ProtocolRequest::NewFluffyBlock(_) => trace!("TODO: NewFluffyBlock"), - ProtocolRequest::NewTransactions(_) => trace!("TODO: NewTransactions"), + ProtocolRequest::GetObjects(req) => { + get_objects(self.blockchain_read_handle.clone(), req).boxed() + } + ProtocolRequest::GetChain(req) => { + get_chain(self.blockchain_read_handle.clone(), req).boxed() + } + ProtocolRequest::FluffyMissingTxs(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + ProtocolRequest::GetTxPoolCompliment(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + ProtocolRequest::NewBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + ProtocolRequest::NewFluffyBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + ProtocolRequest::NewTransactions(_) => async { Ok(ProtocolResponse::NA) }.boxed(), } + } +} + +async fn get_objects( + blockchain_read_handle: BlockchainReadHandle, + req: GetObjectsRequest, +) -> Result { + if req.blocks.is_empty() { + Err("No blocks requested in a GetObjectsRequest")?; + } + + if req.blocks.len() > MAX_BLOCK_BATCH_LEN { + Err("Too many blocks requested in a GetObjectsRequest")?; + } + + let block_ids: Vec<[u8; 32]> = (&req.blocks).into(); + // de-allocate the backing [`Bytes`] + drop(req); + + let res = blockchain_read_handle + .oneshot(BlockchainReadRequest::BlockCompleteEntries(block_ids)) + .await?; - async { Ok(ProtocolResponse::NA) }.boxed() + let BlockchainResponse::BlockCompleteEntries { + blocks, + missed_ids, + current_blockchain_height, + } = res + else { + panic!("Blockchain service returned wrong response!"); + }; + + Ok(ProtocolResponse::GetObjects(GetObjectsResponse { + blocks, + missed_ids: missed_ids.into(), + current_blockchain_height: usize_to_u64(current_blockchain_height), + })) +} + +async fn get_chain( + blockchain_read_handle: BlockchainReadHandle, + req: ChainRequest, +) -> Result { + if req.block_ids.is_empty() { + Err("No block hashes sent in a `ChainRequest`")?; + } + + if req.block_ids.len() > MAX_BLOCKCHAIN_SUPPLEMENT_LEN { + Err("Too many block hashes in a `ChainRequest`")?; } + + let block_ids: Vec<[u8; 32]> = (&req.block_ids).into(); + // de-allocate the backing [`Bytes`] + drop(req); + + let res = blockchain_read_handle + .oneshot(BlockchainReadRequest::NextMissingChainEntry(block_ids)) + .await?; + + let BlockchainResponse::NextMissingChainEntry { + next_entry, + first_missing_block, + start_height, + chain_height, + cumulative_difficulty, + } = res + else { + panic!("Blockchain service returned wrong response!"); + }; + + let (cumulative_difficulty_low64, cumulative_difficulty_top64) = + split_u128_into_low_high_bits(cumulative_difficulty); + + Ok(ProtocolResponse::GetChain(ChainResponse { + start_height: usize_to_u64(start_height), + total_height: usize_to_u64(chain_height), + cumulative_difficulty_low64, + cumulative_difficulty_top64, + m_block_ids: next_entry.into(), + m_block_weights: vec![], + first_block: first_missing_block.map_or(Bytes::new(), Bytes::from), + })) } diff --git a/consensus/src/block.rs b/consensus/src/block.rs index b33b58577..727b072b1 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -8,6 +8,7 @@ use std::{ }; use futures::FutureExt; +use monero_serai::generators::H; use monero_serai::{ block::Block, transaction::{Input, Transaction}, @@ -183,6 +184,19 @@ impl PreparedBlock { block: block.block, }) } + + pub fn new_alt_block(block: AltBlockInformation) -> Result { + Ok(PreparedBlock { + block_blob: block.block_blob, + hf_vote: HardFork::from_version(block.block.header.hardfork_version) + .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?, + hf_version: HardFork::from_vote(block.block.header.hardfork_signal), + block_hash: block.block_hash, + pow_hash: block.pow_hash, + miner_tx_weight: block.block.miner_transaction.weight(), + block: block.block, + }) + } } /// A request to verify a block. diff --git a/p2p/address-book/src/lib.rs b/p2p/address-book/src/lib.rs index c09034851..8c531e75c 100644 --- a/p2p/address-book/src/lib.rs +++ b/p2p/address-book/src/lib.rs @@ -88,14 +88,19 @@ mod sealed { /// An internal trait for the address book for a [`NetworkZone`] that adds the requirement of [`borsh`] traits /// onto the network address. - pub trait BorshNetworkZone: NetworkZone { - type BorshAddr: NetZoneAddress + borsh::BorshDeserialize + borsh::BorshSerialize; + pub trait BorshNetworkZone: + NetworkZone< + Addr: NetZoneAddress + + borsh::BorshDeserialize + + borsh::BorshSerialize, + > + { } impl BorshNetworkZone for T where T::Addr: borsh::BorshDeserialize + borsh::BorshSerialize, + ::BanID: borsh::BorshDeserialize + borsh::BorshSerialize, { - type BorshAddr = T::Addr; } } diff --git a/p2p/p2p/src/constants.rs b/p2p/p2p/src/constants.rs index 44dba917c..4e6daa732 100644 --- a/p2p/p2p/src/constants.rs +++ b/p2p/p2p/src/constants.rs @@ -46,7 +46,12 @@ pub(crate) const INITIAL_CHAIN_REQUESTS_TO_SEND: usize = 3; /// The enforced maximum amount of blocks to request in a batch. /// /// Requesting more than this will cause the peer to disconnect and potentially lead to bans. -pub(crate) const MAX_BLOCK_BATCH_LEN: usize = 100; +pub const MAX_BLOCK_BATCH_LEN: usize = 100; + +/// The enforced maximum amount of block hashes in a blockchain supplement request. +/// +/// Requesting more than this might cause the peer to disconnect and potentially lead to bans. +pub const MAX_BLOCKCHAIN_SUPPLEMENT_LEN: usize = 250; /// The timeout that the block downloader will use for requests. pub(crate) const BLOCK_DOWNLOADER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index 6f95948fc..5355df266 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -26,7 +26,7 @@ mod broadcast; mod client_pool; pub mod config; pub mod connection_maintainer; -mod constants; +pub mod constants; mod inbound_server; mod sync_states; diff --git a/storage/blockchain/Cargo.toml b/storage/blockchain/Cargo.toml index 46b8414d6..594521acf 100644 --- a/storage/blockchain/Cargo.toml +++ b/storage/blockchain/Cargo.toml @@ -38,6 +38,7 @@ serde = { workspace = true, optional = true } tower = { workspace = true } thread_local = { workspace = true, optional = true } rayon = { workspace = true, optional = true } +bytes = "1.6.0" [dev-dependencies] cuprate-helper = { path = "../../helper", features = ["thread", "cast"] } diff --git a/storage/blockchain/src/ops/block.rs b/storage/blockchain/src/ops/block.rs index 8e0465202..010234953 100644 --- a/storage/blockchain/src/ops/block.rs +++ b/storage/blockchain/src/ops/block.rs @@ -2,20 +2,23 @@ //---------------------------------------------------------------------------------------------------- Import use bytemuck::TransparentWrapper; -use monero_serai::block::{Block, BlockHeader}; +use bytes::Bytes; use cuprate_database::{ - RuntimeError, StorableVec, {DatabaseRo, DatabaseRw}, + RuntimeError, StorableVec, {DatabaseIter, DatabaseRo, DatabaseRw}, }; +use cuprate_helper::cast::usize_to_u64; use cuprate_helper::{ map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits}, tx_utils::tx_fee, }; use cuprate_types::{ - AltBlockInformation, ChainId, ExtendedBlockHeader, HardFork, VerifiedBlockInformation, - VerifiedTransactionInformation, + AltBlockInformation, BlockCompleteEntry, ChainId, ExtendedBlockHeader, HardFork, + TransactionBlobs, VerifiedBlockInformation, VerifiedTransactionInformation, }; +use monero_serai::block::{Block, BlockHeader}; +use crate::tables::TablesIter; use crate::{ ops::{ alt_block, @@ -256,6 +259,40 @@ pub fn get_block_extended_header_top( Ok((header, height)) } +//---------------------------------------------------------------------------------------------------- `get_block_complete_entry` + +pub fn get_block_complete_entry( + block_hash: &BlockHash, + tables: &impl TablesIter, +) -> Result { + let height = tables.block_heights().get(block_hash)?; + + let block_blob = tables.block_blobs().get(&height)?.0; + + let block = Block::read(&mut block_blob.as_slice()).expect("Valid block failed to be read"); + + let txs = if let Some(first_tx) = block.transactions.first() { + let first_tx_idx = tables.tx_ids().get(first_tx)?; + let end_tx_idx = first_tx_idx + usize_to_u64(block.transactions.len()); + + let tx_blobs = tables.tx_blobs_iter().get_range(first_tx_idx..end_tx_idx)?; + + tx_blobs + .map(|res| Ok(Bytes::from(res?.0))) + .collect::>()? + } else { + vec![] + }; + + Ok(BlockCompleteEntry { + block: Bytes::from(block_blob), + txs: TransactionBlobs::Normal(txs), + pruned: false, + // This is only needed when pruned. + block_weight: 0, + }) +} + //---------------------------------------------------------------------------------------------------- Misc /// Retrieve a [`BlockInfo`] via its [`BlockHeight`]. #[doc = doc_error!()] diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 73b2d2205..07fa8253c 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -1,19 +1,20 @@ //! Database reader thread-pool definitions and logic. //---------------------------------------------------------------------------------------------------- Import -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; - use rayon::{ - iter::{IntoParallelIterator, ParallelIterator}, + iter::{Either, IntoParallelIterator, ParallelIterator}, prelude::*, ThreadPool, }; +use std::cmp::min; +use std::ops::Index; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use thread_local::ThreadLocal; -use cuprate_database::{ConcreteEnv, DatabaseRo, Env, EnvInner, RuntimeError}; +use cuprate_database::{ConcreteEnv, DatabaseIter, DatabaseRo, Env, EnvInner, RuntimeError}; use cuprate_database_service::{init_thread_pool, DatabaseReadService, ReaderThreads}; use cuprate_helper::map::combine_low_high_bits_to_u128; use cuprate_types::{ @@ -21,6 +22,8 @@ use cuprate_types::{ Chain, ChainId, ExtendedBlockHeader, OutputOnChain, }; +use crate::ops::block::get_block_complete_entry; +use crate::tables::{BlockBlobs, TxIds}; use crate::{ ops::{ alt_block::{ @@ -92,6 +95,7 @@ fn map_request( /* SOMEDAY: pre-request handling, run some code for each request? */ match request { + R::BlockCompleteEntries(blocks) => block_complete_entries(env, blocks), R::BlockExtendedHeader(block) => block_extended_header(env, block), R::BlockHash(block, chain) => block_hash(env, block, chain), R::FindBlock(block_hash) => find_block(env, block_hash), @@ -106,6 +110,7 @@ fn map_request( R::KeyImagesSpent(set) => key_images_spent(env, set), R::CompactChainHistory => compact_chain_history(env), R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids), + R::NextMissingChainEntry(block_hashes) => next_missing_chain_entry(env, block_hashes), R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id), } @@ -180,9 +185,41 @@ macro_rules! get_tables { // FIXME: implement multi-transaction read atomicity. // . -// TODO: The overhead of parallelism may be too much for every request, perfomace test to find optimal +// TODO: The overhead of parallelism may be too much for every request, performance test to find optimal // amount of parallelism. +/// [`BlockchainReadRequest::BlockCompleteEntries`]. +fn block_complete_entries(env: &ConcreteEnv, block_hashes: Vec) -> ResponseResult { + // Prepare tx/tables in `ThreadLocal`. + let env_inner = env.env_inner(); + let tx_ro = thread_local(env); + let tables = thread_local(env); + + let (missed_ids, blocks) = block_hashes + .into_par_iter() + .map(|block_hash| { + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + match get_block_complete_entry(&block_hash, tables) { + Err(RuntimeError::KeyNotFound) => Ok(Either::Left(block_hash)), + res => res.map(Either::Right), + } + }) + .collect::>()?; + + let tx_ro = tx_ro.get_or_try(|| env_inner.tx_ro())?; + let tables = get_tables!(env_inner, tx_ro, tables)?.as_ref(); + + let chain_height = crate::ops::blockchain::chain_height(tables.block_heights())?; + + Ok(BlockchainResponse::BlockCompleteEntries { + blocks, + missed_ids, + current_blockchain_height: chain_height, + }) +} + /// [`BlockchainReadRequest::BlockExtendedHeader`]. #[inline] fn block_extended_header(env: &ConcreteEnv, block_height: BlockHeight) -> ResponseResult { @@ -556,6 +593,62 @@ fn find_first_unknown(env: &ConcreteEnv, block_ids: &[BlockHash]) -> ResponseRes }) } +/// [`BlockchainReadRequest::NextMissingChainEntry`] +fn next_missing_chain_entry(env: &ConcreteEnv, block_hashes: Vec<[u8; 32]>) -> ResponseResult { + let env_inner = env.env_inner(); + let tx_ro = env_inner.tx_ro()?; + + let table_block_heights = env_inner.open_db_ro::(&tx_ro)?; + let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; + + let (top_block_height, top_block_info) = table_block_infos.last()?; + + let mut start_height = 0; + + for block_hash in block_hashes { + match table_block_heights.get(&block_hash) { + Ok(height) => { + start_height = height; + break; + } + Err(RuntimeError::KeyNotFound) => continue, + Err(e) => return Err(e), + } + } + + let table_block_infos = env_inner.open_db_ro::(&tx_ro)?; + + const DEFAULT_CHAIN_ENTRY_SIZE: usize = 10_000; + + let end_height = min( + start_height + DEFAULT_CHAIN_ENTRY_SIZE, + top_block_height + 1, + ); + + let block_hashes: Vec<_> = table_block_infos + .get_range(start_height..end_height)? + .map(|block_info| Ok(block_info?.block_hash)) + .collect::>()?; + + let first_missing_block = if block_hashes.len() > 1 { + let table_block_blobs = env_inner.open_db_ro::(&tx_ro)?; + Some(table_block_blobs.get(&(start_height + 1))?.0) + } else { + None + }; + + Ok(BlockchainResponse::NextMissingChainEntry { + next_entry: block_hashes, + first_missing_block, + start_height, + chain_height: top_block_height + 1, + cumulative_difficulty: combine_low_high_bits_to_u128( + top_block_info.cumulative_difficulty_low, + top_block_info.cumulative_difficulty_high, + ), + }) +} + /// [`BlockchainReadRequest::AltBlocksInChain`] fn alt_blocks_in_chain(env: &ConcreteEnv, chain_id: ChainId) -> ResponseResult { // Prepare tx/tables in `ThreadLocal`. diff --git a/types/src/blockchain.rs b/types/src/blockchain.rs index f246e59a5..1595ec603 100644 --- a/types/src/blockchain.rs +++ b/types/src/blockchain.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ types::{Chain, ExtendedBlockHeader, OutputOnChain, VerifiedBlockInformation}, - AltBlockInformation, ChainId, + AltBlockInformation, BlockCompleteEntry, ChainId, }; //---------------------------------------------------------------------------------------------------- ReadRequest @@ -25,6 +25,11 @@ use crate::{ /// See `Response` for the expected responses per `Request`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BlockchainReadRequest { + /// Request for [`BlockCompleteEntry`]s. + /// + /// The input is the hashes of the blocks wanted. + BlockCompleteEntries(Vec<[u8; 32]>), + /// Request a block's extended header. /// /// The input is the block's height. @@ -101,6 +106,13 @@ pub enum BlockchainReadRequest { /// order, or else the returned response is unspecified and meaningless, /// as this request performs a binary search. FindFirstUnknown(Vec<[u8; 32]>), + + /// A request for the next missing chain entry. + /// + /// The input is a list of block hashes in reverse chronological order that do not necessarily + /// directly follow each other. + NextMissingChainEntry(Vec<[u8; 32]>), + /// A request for all alt blocks in the chain with the given [`ChainId`]. AltBlocksInChain(ChainId), } @@ -145,6 +157,16 @@ pub enum BlockchainWriteRequest { #[derive(Debug, Clone, PartialEq, Eq)] pub enum BlockchainResponse { //------------------------------------------------------ Reads + /// Response to [`BlockchainReadRequest::BlockCompleteEntries`] + BlockCompleteEntries { + /// The blocks requested that we had. + blocks: Vec, + /// The hashes of the blocks we did not have. + missed_ids: Vec<[u8; 32]>, + /// The current height of our blockchain. + current_blockchain_height: usize, + }, + /// Response to [`BlockchainReadRequest::BlockExtendedHeader`]. /// /// Inner value is the extended headed of the requested block. @@ -218,6 +240,24 @@ pub enum BlockchainResponse { /// This will be [`None`] if all blocks were known. FindFirstUnknown(Option<(usize, usize)>), + /// The response for [`BlockchainReadRequest::NextMissingChainEntry`] + NextMissingChainEntry { + /// A list of block hashes that should be next from the requested chain. + /// + /// The first block hash will overlap with one of the blocks in the request. + next_entry: Vec<[u8; 32]>, + /// The block blob of the second block in `next_entry`. + /// + /// If there is only 1 block in `next_entry` then this will be [`None`]. + first_missing_block: Option>, + /// The height of the first block in `next_entry`. + start_height: usize, + /// The current height of our chain. + chain_height: usize, + /// The cumulative difficulty of our chain. + cumulative_difficulty: u128, + }, + /// The response for [`BlockchainReadRequest::AltBlocksInChain`]. /// /// Contains all the alt blocks in the alt-chain in chronological order. From 01a3065cc837a23ae76c31a9b756d5a3ea3bb706 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 12 Sep 2024 22:17:44 +0100 Subject: [PATCH 27/46] clean up handler code --- binaries/cuprated/src/blockchain.rs | 6 +- binaries/cuprated/src/blockchain/manager.rs | 115 ++--- .../src/blockchain/manager/batch_handler.rs | 121 ----- .../src/blockchain/manager/handler.rs | 421 ++++++++++++------ binaries/cuprated/src/main.rs | 3 +- binaries/cuprated/src/signals.rs | 3 + p2p/address-book/src/lib.rs | 12 +- p2p_state.bin | Bin 162918 -> 172430 bytes storage/blockchain/src/service/read.rs | 2 +- 9 files changed, 333 insertions(+), 350 deletions(-) delete mode 100644 binaries/cuprated/src/blockchain/manager/batch_handler.rs create mode 100644 binaries/cuprated/src/signals.rs diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index e1f5765bf..eb23224ec 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -1,6 +1,8 @@ //! Blockchain //! //! Will contain the chain manager and syncer. + +use futures::FutureExt; use tokio::sync::mpsc; use tower::{Service, ServiceExt}; @@ -95,7 +97,7 @@ pub async fn init_consensus( } /// Initializes the blockchain manager task and syncer. -pub fn init_blockchain_manager( +pub async fn init_blockchain_manager( clearnet_interface: NetworkInterface, blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, @@ -118,7 +120,7 @@ pub fn init_blockchain_manager( blockchain_read_handle, blockchain_context_service, block_verifier_service, - ); + ).await; tokio::spawn(manager.run(batch_rx)); } diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index eaf8669f1..e1e78d46c 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,6 +1,6 @@ -mod batch_handler; mod handler; +use std::collections::HashMap; use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::context::RawBlockChainContext; @@ -11,12 +11,20 @@ use cuprate_consensus::{ }; use cuprate_p2p::block_downloader::BlockBatch; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use cuprate_types::Chain; +use cuprate_types::{Chain, TransactionVerificationData}; use futures::StreamExt; -use tokio::sync::mpsc::Receiver; +use monero_serai::block::Block; +use tokio::sync::mpsc; +use tokio::sync::{Notify, oneshot}; use tower::{Service, ServiceExt}; use tracing::error; +pub struct IncomingBlock { + block: Block, + prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, + response_tx: oneshot::Sender>, +} + pub struct BlockchainManager { blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, @@ -27,105 +35,58 @@ pub struct BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, + + // TODO: stop_current_block_downloader: Notify, } impl BlockchainManager { - pub const fn new( + pub async fn new( blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, - blockchain_context_service: BlockChainContextService, + mut blockchain_context_service: BlockChainContextService, block_verifier_service: BlockVerifierService< BlockChainContextService, TxVerifierService, ConsensusBlockchainReadHandle, >, ) -> Self { - Self { - blockchain_write_handle, - blockchain_read_handle, - blockchain_context_service, - cached_blockchain_context: todo!(), - block_verifier_service, - } - } - - async fn handle_incoming_main_chain_batch( - &mut self, - batch: BlockBatch, - ) -> Result<(), anyhow::Error> { - let VerifyBlockResponse::MainChainBatchPrepped(prepped) = self - .block_verifier_service + let BlockChainContextResponse::Context(blockchain_context) = blockchain_context_service .ready() .await .expect("TODO") - .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { - blocks: batch.blocks, - }) - .await? - else { - panic!("Incorrect response!"); + .call(BlockChainContextRequest::GetContext) + .await + .expect("TODO") else { + panic!("Blockchain context service returned wrong response!"); }; - for (block, txs) in prepped { - let VerifyBlockResponse::MainChain(verified_block) = block_verifier_service - .ready() - .await - .expect("TODO") - .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await - .unwrap() - else { - panic!("Incorrect response!"); - }; - - blockchain_context_service - .ready() - .await - .expect("TODO") - .call(BlockChainContextRequest::Update(NewBlockData { - block_hash: verified_block.block_hash, - height: verified_block.height, - timestamp: verified_block.block.header.timestamp, - weight: verified_block.weight, - long_term_weight: verified_block.long_term_weight, - generated_coins: verified_block.generated_coins, - vote: HardFork::from_vote(verified_block.block.header.hardfork_signal), - cumulative_difficulty: verified_block.cumulative_difficulty, - })) - .await - .expect("TODO"); - - blockchain_write_handle - .ready() - .await - .expect("TODO") - .call(BlockchainWriteRequest::WriteBlock(verified_block)) - .await - .expect("TODO"); - } - } - - async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { - let (first_block, _) = batch - .blocks - .first() - .expect("Block batch should not be empty"); - - if first_block.header.previous == self.cached_blockchain_context.top_hash { - todo!("Main chain") - } else { - todo!("Alt chain") + Self { + blockchain_write_handle, + blockchain_read_handle, + blockchain_context_service, + cached_blockchain_context: blockchain_context.unchecked_blockchain_context().clone(), + block_verifier_service, } } - pub async fn run(mut self, mut batch_rx: Receiver) { + pub async fn run(mut self, mut block_batch_rx: mpsc::Receiver, mut block_single_rx: mpsc::Receiver) { loop { tokio::select! { - Some(batch) = batch_rx.recv() => { + Some(batch) = block_batch_rx.recv() => { self.handle_incoming_block_batch( batch, ).await; } + Some(incoming_block) = block_single_rx.recv() => { + let IncomingBlock { + block, + prepped_txs, + response_tx + } = incoming_block; + + let res = self.handle_incoming_block(block, prepped_txs).await; + let _ = response_tx.send(res); + } else => { todo!("TODO: exit the BC manager") } diff --git a/binaries/cuprated/src/blockchain/manager/batch_handler.rs b/binaries/cuprated/src/blockchain/manager/batch_handler.rs deleted file mode 100644 index ea08af42a..000000000 --- a/binaries/cuprated/src/blockchain/manager/batch_handler.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Block batch handling functions. - -use crate::blockchain::types::ConsensusBlockchainReadHandle; -use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::context::NewBlockData; -use cuprate_consensus::transactions::new_tx_verification_data; -use cuprate_consensus::{ - BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, - BlockVerifierService, BlockchainReadRequest, BlockchainResponse, ExtendedConsensusError, - VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, -}; -use cuprate_p2p::block_downloader::BlockBatch; -use cuprate_types::blockchain::BlockchainWriteRequest; -use cuprate_types::{Chain, HardFork}; -use rayon::prelude::*; -use tower::{Service, ServiceExt}; -use tracing::{debug, error, info}; - -async fn handle_incoming_block_batch_main_chain( - batch: BlockBatch, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_write_handle: &mut BlockchainWriteHandle, -) -> Result<(), anyhow::Error> -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - info!( - "Handling batch to main chain height: {}", - batch.blocks.first().unwrap().0.number().unwrap() - ); - - let VerifyBlockResponse::MainChainBatchPrepped(prepped) = block_verifier_service - .ready() - .await - .expect("TODO") - .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { - blocks: batch.blocks, - }) - .await? - else { - panic!("Incorrect response!"); - }; - - for (block, txs) in prepped { - let VerifyBlockResponse::MainChain(verified_block) = block_verifier_service - .ready() - .await - .expect("TODO") - .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await? - else { - panic!("Incorrect response!"); - }; - - blockchain_context_service - .ready() - .await - .expect("TODO") - .call(BlockChainContextRequest::Update(NewBlockData { - block_hash: verified_block.block_hash, - height: verified_block.height, - timestamp: verified_block.block.header.timestamp, - weight: verified_block.weight, - long_term_weight: verified_block.long_term_weight, - generated_coins: verified_block.generated_coins, - vote: HardFork::from_vote(verified_block.block.header.hardfork_signal), - cumulative_difficulty: verified_block.cumulative_difficulty, - })) - .await - .expect("TODO"); - - blockchain_write_handle - .ready() - .await - .expect("TODO") - .call(BlockchainWriteRequest::WriteBlock(verified_block)) - .await - .expect("TODO"); - } -} - -async fn handle_incoming_block_batch_alt_chain( - batch: BlockBatch, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_write_handle: &mut BlockchainWriteHandle, -) -> Result<(), anyhow::Error> -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - for (block, txs) in batch.blocks { - alt_block_info.cumulative_difficulty - } -} diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 221acb817..6dcffef79 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,5 +1,8 @@ use crate::blockchain::types::ConsensusBlockchainReadHandle; +use crate::signals::REORG_LOCK; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::block::PreparedBlock; +use cuprate_consensus::context::NewBlockData; use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_consensus::{ BlockChainContextRequest, BlockChainContextResponse, BlockVerifierService, @@ -10,154 +13,292 @@ use cuprate_p2p::block_downloader::BlockBatch; use cuprate_types::blockchain::{ BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, }; -use cuprate_types::AltBlockInformation; +use cuprate_types::{ + AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, +}; +use futures::{TryFutureExt, TryStreamExt}; use monero_serai::block::Block; use monero_serai::transaction::Transaction; use rayon::prelude::*; +use std::collections::HashMap; +use std::sync::Arc; use tower::{Service, ServiceExt}; +use tracing::info; + +impl super::BlockchainManager { + pub async fn handle_incoming_block( + &mut self, + block: Block, + prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, + ) -> Result<(), anyhow::Error> { + if block.header.previous != self.cached_blockchain_context.top_hash { + self.handle_incoming_alt_block(block, prepared_txs).await?; + + return Ok(()); + } -async fn handle_incoming_alt_block( - block: Block, - txs: Vec, - current_cumulative_difficulty: u128, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_write_handle: &mut BlockchainWriteHandle, -) -> Result<(), anyhow::Error> -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - let prepared_txs = txs - .into_par_iter() - .map(|tx| { - let tx = new_tx_verification_data(tx)?; - (tx.tx_hash, tx) - }) - .collect::>()?; - - let VerifyBlockResponse::AltChain(alt_block_info) = block_verifier_service - .ready() - .await - .expect("TODO") - .call(VerifyBlockRequest::AltChain { - block, - prepared_txs, - }) - .await? - else { - panic!("Incorrect response!"); - }; - - if alt_block_info.cumulative_difficulty > current_cumulative_difficulty { - todo!("do re-org"); + let VerifyBlockResponse::MainChain(verified_block) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChain { + block, + prepared_txs, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + self.add_valid_block_to_main_chain(verified_block).await; + + Ok(()) } - blockchain_write_handle - .ready() - .await - .expect("TODO") - .call(BlockchainWriteRequest::WriteAltBlock(alt_block_info))?; + pub async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { + let (first_block, _) = batch + .blocks + .first() + .expect("Block batch should not be empty"); - Ok(()) -} + if first_block.header.previous == self.cached_blockchain_context.top_hash { + self.handle_incoming_block_batch_main_chain(batch).await.expect("TODO"); + } else { + self.handle_incoming_block_batch_alt_chain(batch).await.expect("TODO"); + } + } -async fn try_do_reorg( - top_alt_block: AltBlockInformation, - chain_height: usize, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_write_handle: &mut BlockchainWriteHandle, - blockchain_read_handle: &mut BlockchainReadHandle, -) -> Result<(), anyhow::Error> -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - let BlockchainResponse::AltBlocksInChain(mut alt_blocks) = blockchain_read_handle - .ready() - .await - .expect("TODO") - .call(BlockchainReadRequest::AltBlocksInChain( - top_alt_block.chain_id, - )) - .await? - else { - panic!("Incorrect response!"); - }; - - alt_blocks.push(top_alt_block); - - let split_height = alt_blocks[0].height; - - let BlockchainResponse::PopBlocks(old_main_chain_id) = blockchain_write_handle - .ready() - .await - .expect("TODO") - .call(BlockchainWriteRequest::PopBlocks( - chain_height - split_height + 1, - )) - .await? - else { - panic!("Incorrect response!"); - }; - - todo!() -} + async fn handle_incoming_block_batch_main_chain( + &mut self, + batch: BlockBatch, + ) -> Result<(), anyhow::Error> { + info!( + "Handling batch to main chain height: {}", + batch.blocks.first().unwrap().0.number().unwrap() + ); + + let VerifyBlockResponse::MainChainBatchPrepped(prepped) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { + blocks: batch.blocks, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + for (block, txs) in prepped { + let VerifyBlockResponse::MainChain(verified_block) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainPrepped { block, txs }) + .await? + else { + panic!("Incorrect response!"); + }; + + self.add_valid_block_to_main_chain(verified_block).await; + } + + Ok(()) + } + + async fn handle_incoming_block_batch_alt_chain( + &mut self, + batch: BlockBatch, + ) -> Result<(), anyhow::Error> { + for (block, txs) in batch.blocks { + let txs = txs + .into_par_iter() + .map(|tx| { + let tx = new_tx_verification_data(tx)?; + Ok((tx.tx_hash, tx)) + }) + .collect::>()?; + + self.handle_incoming_alt_block(block, txs).await?; + } + + Ok(()) + } + + pub async fn handle_incoming_alt_block( + &mut self, + block: Block, + prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, + ) -> Result<(), anyhow::Error> { + let VerifyBlockResponse::AltChain(alt_block_info) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::AltChain { + block, + prepared_txs, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + // TODO: check in consensus crate if alt block already exists. + + if alt_block_info.cumulative_difficulty + > self.cached_blockchain_context.cumulative_difficulty + { + self.try_do_reorg(alt_block_info).await?; + // TODO: ban the peer if the reorg failed. + + return Ok(()); + } + + self.blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::WriteAltBlock(alt_block_info)) + .await?; -async fn verify_add_alt_blocks_to_main_chain( - alt_blocks: Vec, - block_verifier_service: &mut BlockVerifierService, - blockchain_context_service: &mut C, - blockchain_write_handle: &mut BlockchainWriteHandle, -) -> Result<(), anyhow::Error> -where - C: Service< - BlockChainContextRequest, - Response = BlockChainContextResponse, - Error = tower::BoxError, - > + Clone - + Send - + 'static, - C::Future: Send + 'static, - - TxV: Service - + Clone - + Send - + 'static, - TxV::Future: Send + 'static, -{ - let VerifyBlockResponse::AltChain(alt_block_info) = block_verifier_service - .ready() - .await - .expect("TODO") - .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await? - else { - panic!("Incorrect response!"); - }; + Ok(()) + } + + async fn try_do_reorg( + &mut self, + top_alt_block: AltBlockInformation, + ) -> Result<(), anyhow::Error> { + let _guard = REORG_LOCK.write().await; + + let BlockchainResponse::AltBlocksInChain(mut alt_blocks) = self + .blockchain_read_handle + .ready() + .await + .expect("TODO") + .call(BlockchainReadRequest::AltBlocksInChain( + top_alt_block.chain_id, + )) + .await? + else { + panic!("Incorrect response!"); + }; + + alt_blocks.push(top_alt_block); + + let split_height = alt_blocks[0].height; + let current_main_chain_height = self.cached_blockchain_context.chain_height; + + let BlockchainResponse::PopBlocks(old_main_chain_id) = self + .blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::PopBlocks( + current_main_chain_height - split_height + 1, + )) + .await + .expect("TODO") + else { + panic!("Incorrect response!"); + }; + + self.blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::PopBlocks { + numb_blocks: current_main_chain_height - split_height + 1, + }) + .await + .expect("TODO"); + + let reorg_res = self.verify_add_alt_blocks_to_main_chain(alt_blocks).await; + + match reorg_res { + Ok(()) => Ok(()), + Err(e) => { + todo!("Reverse reorg") + } + } + } + + async fn verify_add_alt_blocks_to_main_chain( + &mut self, + alt_blocks: Vec, + ) -> Result<(), anyhow::Error> { + for mut alt_block in alt_blocks { + let prepped_txs = alt_block + .txs + .drain(..) + .map(|tx| Ok(Arc::new(tx.try_into()?))) + .collect::>()?; + + let prepped_block = PreparedBlock::new_alt_block(alt_block)?; + + let VerifyBlockResponse::MainChain(verified_block) = self + .block_verifier_service + .ready() + .await + .expect("TODO") + .call(VerifyBlockRequest::MainChainPrepped { + block: prepped_block, + txs: prepped_txs, + }) + .await? + else { + panic!("Incorrect response!"); + }; + + self.add_valid_block_to_main_chain(verified_block).await; + } + + Ok(()) + } + + pub async fn add_valid_block_to_main_chain( + &mut self, + verified_block: VerifiedBlockInformation, + ) { + self.blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::Update(NewBlockData { + block_hash: verified_block.block_hash, + height: verified_block.height, + timestamp: verified_block.block.header.timestamp, + weight: verified_block.weight, + long_term_weight: verified_block.long_term_weight, + generated_coins: verified_block.generated_coins, + vote: HardFork::from_vote(verified_block.block.header.hardfork_signal), + cumulative_difficulty: verified_block.cumulative_difficulty, + })) + .await + .expect("TODO"); + + self.blockchain_write_handle + .ready() + .await + .expect("TODO") + .call(BlockchainWriteRequest::WriteBlock(verified_block)) + .await + .expect("TODO"); + + let BlockChainContextResponse::Context(blockchain_context) = self + .blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::GetContext) + .await + .expect("TODO") else { + panic!("Incorrect response!"); + }; + + self.cached_blockchain_context = blockchain_context.unchecked_blockchain_context().clone(); + } } diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 0e706b933..0a885363f 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -18,6 +18,7 @@ mod blockchain; mod config; mod p2p; mod rpc; +mod signals; mod txpool; use blockchain::check_add_genesis; @@ -58,7 +59,7 @@ fn main() { context_svc, block_verifier, config.block_downloader_config(), - ); + ).await; // TODO: this can be removed as long as the main thread does not exit, so when command handling // is added diff --git a/binaries/cuprated/src/signals.rs b/binaries/cuprated/src/signals.rs new file mode 100644 index 000000000..cafd8cdbb --- /dev/null +++ b/binaries/cuprated/src/signals.rs @@ -0,0 +1,3 @@ +use tokio::sync::RwLock; + +pub static REORG_LOCK: RwLock<()> = RwLock::const_new(()); diff --git a/p2p/address-book/src/lib.rs b/p2p/address-book/src/lib.rs index 8c531e75c..ae35a1bb4 100644 --- a/p2p/address-book/src/lib.rs +++ b/p2p/address-book/src/lib.rs @@ -88,19 +88,15 @@ mod sealed { /// An internal trait for the address book for a [`NetworkZone`] that adds the requirement of [`borsh`] traits /// onto the network address. - pub trait BorshNetworkZone: - NetworkZone< - Addr: NetZoneAddress - + borsh::BorshDeserialize - + borsh::BorshSerialize, - > - { + pub trait BorshNetworkZone: NetworkZone { + type BorshAddr: NetZoneAddress + borsh::BorshDeserialize + borsh::BorshSerialize; } impl BorshNetworkZone for T where T::Addr: borsh::BorshDeserialize + borsh::BorshSerialize, - ::BanID: borsh::BorshDeserialize + borsh::BorshSerialize, { + type BorshAddr = T::Addr; } + } diff --git a/p2p_state.bin b/p2p_state.bin index bf2d673dd8c3bca6473f8a64e5c96f5145cd2331..281771cac6fd217c1d42942fc5489e431caa63a0 100644 GIT binary patch delta 72123 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 AKmY&$ delta 62066 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 CT8b0^ diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index 07fa8253c..e502c9f1d 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -628,7 +628,7 @@ fn next_missing_chain_entry(env: &ConcreteEnv, block_hashes: Vec<[u8; 32]>) -> R let block_hashes: Vec<_> = table_block_infos .get_range(start_height..end_height)? .map(|block_info| Ok(block_info?.block_hash)) - .collect::>()?; + .collect::>()?; let first_missing_block = if block_hashes.len() > 1 { let table_block_blobs = env_inner.open_db_ro::(&tx_ro)?; From 6ec5bc37a94ae236a31fe29fea79c971ddae6ebc Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 13 Sep 2024 00:03:03 +0100 Subject: [PATCH 28/46] add function for incoming blocks --- binaries/cuprated/src/blockchain.rs | 4 +- binaries/cuprated/src/blockchain/free.rs | 93 ++++++++++++++++++ binaries/cuprated/src/blockchain/manager.rs | 20 ++-- .../src/blockchain/manager/handler.rs | 11 ++- binaries/cuprated/src/main.rs | 3 +- p2p/address-book/src/lib.rs | 1 - p2p_state.bin | Bin 172430 -> 172852 bytes 7 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 binaries/cuprated/src/blockchain/free.rs diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index eb23224ec..46cc6a43c 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -16,6 +16,7 @@ use cuprate_types::{ VerifiedBlockInformation, }; +mod free; mod manager; mod syncer; mod types; @@ -120,7 +121,8 @@ pub async fn init_blockchain_manager( blockchain_read_handle, blockchain_context_service, block_verifier_service, - ).await; + ) + .await; tokio::spawn(manager.run(batch_rx)); } diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs new file mode 100644 index 000000000..becdf3079 --- /dev/null +++ b/binaries/cuprated/src/blockchain/free.rs @@ -0,0 +1,93 @@ +use crate::blockchain::manager::IncomingBlock; +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_consensus::transactions::new_tx_verification_data; +use cuprate_helper::cast::usize_to_u64; +use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; +use cuprate_types::Chain; +use monero_serai::block::Block; +use monero_serai::transaction::Transaction; +use rayon::prelude::*; +use std::collections::HashMap; +use std::sync::OnceLock; +use tokio::sync::{mpsc, oneshot}; +use tower::{Service, ServiceExt}; + +static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); + +#[derive(thiserror::Error)] +pub enum IncomingBlockError { + #[error("Unknown transactions in block.")] + UnknownTransactions(Vec), + #[error("The block has an unknown parent.")] + Orphan, + #[error(transparent)] + InvalidBlock(anyhow::Error), +} + +pub async fn handle_incoming_block( + block: Block, + given_txs: Vec, + blockchain_read_handle: &mut BlockchainReadHandle, +) -> Result<(), IncomingBlockError> { + if !block_exists(block.header.previous, blockchain_read_handle).expect("TODO") { + return Err(IncomingBlockError::Orphan); + } + + let block_hash = block.hash(); + + if block_exists(block_hash, blockchain_read_handle) + .await + .expect("TODO") + { + return Ok(()); + } + + let prepped_txs = given_txs + .into_par_iter() + .map(|tx| { + let tx = new_tx_verification_data(tx)?; + Ok((tx.tx_hash, tx)) + }) + .collect::>() + .map_err(IncomingBlockError::InvalidBlock)?; + + // TODO: Get transactions from the tx pool first. + if given_txs.len() != block.transactions.len() { + return Err(IncomingBlockError::UnknownTransactions( + (0..usize_to_u64(block.transactions.len())).collect(), + )); + } + + let Some(incoming_block_tx) = INCOMING_BLOCK_TX.get() else { + return Ok(()); + }; + + let (response_tx, response_rx) = oneshot::channel(); + + incoming_block_tx + .send(IncomingBlock { + block, + prepped_txs, + response_tx, + }) + .await + .expect("TODO: don't actually panic here"); + + response_rx.await.map_err(IncomingBlockError::InvalidBlock) +} + +async fn block_exists( + block_hash: [u8; 32], + blockchain_read_handle: &mut BlockchainReadHandle, +) -> Result { + let BlockchainResponse::FindBlock(chain) = blockchain_read_handle + .ready() + .await? + .call(BlockchainReadRequest::FindBlock(block_hash)) + .await? + else { + panic!("Invalid blockchain response!"); + }; + + Ok(chain.is_some()) +} diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index e1e78d46c..69de33993 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,6 +1,5 @@ mod handler; -use std::collections::HashMap; use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::context::RawBlockChainContext; @@ -14,15 +13,16 @@ use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::{Chain, TransactionVerificationData}; use futures::StreamExt; use monero_serai::block::Block; +use std::collections::HashMap; use tokio::sync::mpsc; -use tokio::sync::{Notify, oneshot}; +use tokio::sync::{oneshot, Notify}; use tower::{Service, ServiceExt}; use tracing::error; pub struct IncomingBlock { - block: Block, - prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, - response_tx: oneshot::Sender>, + pub block: Block, + pub prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, + pub response_tx: oneshot::Sender>, } pub struct BlockchainManager { @@ -35,7 +35,6 @@ pub struct BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, - // TODO: stop_current_block_downloader: Notify, } @@ -56,7 +55,8 @@ impl BlockchainManager { .expect("TODO") .call(BlockChainContextRequest::GetContext) .await - .expect("TODO") else { + .expect("TODO") + else { panic!("Blockchain context service returned wrong response!"); }; @@ -69,7 +69,11 @@ impl BlockchainManager { } } - pub async fn run(mut self, mut block_batch_rx: mpsc::Receiver, mut block_single_rx: mpsc::Receiver) { + pub async fn run( + mut self, + mut block_batch_rx: mpsc::Receiver, + mut block_single_rx: mpsc::Receiver, + ) { loop { tokio::select! { Some(batch) = block_batch_rx.recv() => { diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 6dcffef79..f9f6ce807 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -63,9 +63,13 @@ impl super::BlockchainManager { .expect("Block batch should not be empty"); if first_block.header.previous == self.cached_blockchain_context.top_hash { - self.handle_incoming_block_batch_main_chain(batch).await.expect("TODO"); + self.handle_incoming_block_batch_main_chain(batch) + .await + .expect("TODO"); } else { - self.handle_incoming_block_batch_alt_chain(batch).await.expect("TODO"); + self.handle_incoming_block_batch_alt_chain(batch) + .await + .expect("TODO"); } } @@ -295,7 +299,8 @@ impl super::BlockchainManager { .expect("TODO") .call(BlockChainContextRequest::GetContext) .await - .expect("TODO") else { + .expect("TODO") + else { panic!("Incorrect response!"); }; diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 0a885363f..2c26c118a 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -59,7 +59,8 @@ fn main() { context_svc, block_verifier, config.block_downloader_config(), - ).await; + ) + .await; // TODO: this can be removed as long as the main thread does not exit, so when command handling // is added diff --git a/p2p/address-book/src/lib.rs b/p2p/address-book/src/lib.rs index ae35a1bb4..c09034851 100644 --- a/p2p/address-book/src/lib.rs +++ b/p2p/address-book/src/lib.rs @@ -98,5 +98,4 @@ mod sealed { { type BorshAddr = T::Addr; } - } diff --git a/p2p_state.bin b/p2p_state.bin index 281771cac6fd217c1d42942fc5489e431caa63a0..fc17e050f72079bd7b70ecf81a388975afba2996 100644 GIT binary patch delta 4664 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 DW~I!Y From 90beed15905ac1d45e237bf87bcdbe40eb871ab7 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Fri, 13 Sep 2024 03:17:30 +0100 Subject: [PATCH 29/46] add docs to handler functions --- binaries/cuprated/src/blockchain/manager.rs | 4 +- .../src/blockchain/manager/handler.rs | 218 +++++++++++++----- binaries/cuprated/src/blockchain/syncer.rs | 23 +- p2p/p2p/src/constants.rs | 6 +- p2p_state.bin | Bin 172852 -> 172906 bytes 5 files changed, 186 insertions(+), 65 deletions(-) diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 69de33993..ae5a1d3d8 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -22,7 +22,7 @@ use tracing::error; pub struct IncomingBlock { pub block: Block, pub prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, - pub response_tx: oneshot::Sender>, + pub response_tx: oneshot::Sender>, } pub struct BlockchainManager { @@ -35,7 +35,7 @@ pub struct BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, - // TODO: stop_current_block_downloader: Notify, + stop_current_block_downloader: Notify, } impl BlockchainManager { diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index f9f6ce807..1bdae16c2 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,40 +1,45 @@ -use crate::blockchain::types::ConsensusBlockchainReadHandle; -use crate::signals::REORG_LOCK; +use std::{collections::HashMap, sync::Arc}; + +use futures::{TryFutureExt, TryStreamExt}; +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; +use tower::{Service, ServiceExt}; +use tracing::info; + use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::block::PreparedBlock; -use cuprate_consensus::context::NewBlockData; -use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_consensus::{ + block::PreparedBlock, context::NewBlockData, transactions::new_tx_verification_data, BlockChainContextRequest, BlockChainContextResponse, BlockVerifierService, ExtendedConsensusError, VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, }; -use cuprate_p2p::block_downloader::BlockBatch; -use cuprate_types::blockchain::{ - BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest, -}; +use cuprate_p2p::{block_downloader::BlockBatch, constants::LONG_BAN}; use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest}, AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; -use futures::{TryFutureExt, TryStreamExt}; -use monero_serai::block::Block; -use monero_serai::transaction::Transaction; -use rayon::prelude::*; -use std::collections::HashMap; -use std::sync::Arc; -use tower::{Service, ServiceExt}; -use tracing::info; + +use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; impl super::BlockchainManager { + /// Handle an incoming [`Block`]. + /// + /// This function will route to [`Self::handle_incoming_alt_block`] if the block does not follow + /// the top of the main chain. + /// + /// Otherwise, this function will validate and add the block to the main chain. + /// + /// On success returns a [`bool`] indicating if the block was added to the main chain ([`true`]) + /// of an alt-chain ([`false`]). pub async fn handle_incoming_block( &mut self, block: Block, prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, - ) -> Result<(), anyhow::Error> { + ) -> Result { if block.header.previous != self.cached_blockchain_context.top_hash { self.handle_incoming_alt_block(block, prepared_txs).await?; - return Ok(()); + return Ok(false); } let VerifyBlockResponse::MainChain(verified_block) = self @@ -53,9 +58,18 @@ impl super::BlockchainManager { self.add_valid_block_to_main_chain(verified_block).await; - Ok(()) + Ok(true) } + /// Handle an incoming [`BlockBatch`]. + /// + /// This function will route to [`Self::handle_incoming_block_batch_main_chain`] or [`Self::handle_incoming_block_batch_alt_chain`] + /// depending on if the first block in the batch follows from the top of our chain. + /// + /// # Panics + /// + /// This function will panic if the batch is empty or if any internal service returns an unexpected + /// error that we cannot recover from. pub async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { let (first_block, _) = batch .blocks @@ -63,26 +77,36 @@ impl super::BlockchainManager { .expect("Block batch should not be empty"); if first_block.header.previous == self.cached_blockchain_context.top_hash { - self.handle_incoming_block_batch_main_chain(batch) - .await - .expect("TODO"); + self.handle_incoming_block_batch_main_chain(batch).await; } else { - self.handle_incoming_block_batch_alt_chain(batch) - .await - .expect("TODO"); + self.handle_incoming_block_batch_alt_chain(batch).await; } } - async fn handle_incoming_block_batch_main_chain( - &mut self, - batch: BlockBatch, - ) -> Result<(), anyhow::Error> { + /// Handles an incoming [`BlockBatch`] that follows the main chain. + /// + /// This function will handle validating the blocks in the batch and adding them to the blockchain + /// database and context cache. + /// + /// This function will also handle banning the peer and canceling the block downloader if the + /// block is invalid. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. + async fn handle_incoming_block_batch_main_chain(&mut self, batch: BlockBatch) { info!( "Handling batch to main chain height: {}", batch.blocks.first().unwrap().0.number().unwrap() ); - let VerifyBlockResponse::MainChainBatchPrepped(prepped) = self + let ban_cancel_download = || { + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); + }; + + let batch_prep_res = self .block_verifier_service .ready() .await @@ -90,21 +114,33 @@ impl super::BlockchainManager { .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { blocks: batch.blocks, }) - .await? - else { - panic!("Incorrect response!"); + .await; + + let prepped_blocks = match batch_prep_res { + Ok(VerifyBlockResponse::MainChainBatchPrepped(prepped_blocks)) => prepped_blocks, + Err(_) => { + ban_cancel_download(); + return; + } + _ => panic!("Incorrect response!"), }; - for (block, txs) in prepped { - let VerifyBlockResponse::MainChain(verified_block) = self + for (block, txs) in prepped_blocks { + let verify_res = self .block_verifier_service .ready() .await .expect("TODO") .call(VerifyBlockRequest::MainChainPrepped { block, txs }) - .await? - else { - panic!("Incorrect response!"); + .await; + + let VerifyBlockResponse::MainChain(verified_block) = match verify_res { + Ok(VerifyBlockResponse::MainChain(verified_block)) => verified_block, + Err(_) => { + ban_cancel_download(); + return; + } + _ => panic!("Incorrect response!"), }; self.add_valid_block_to_main_chain(verified_block).await; @@ -113,25 +149,60 @@ impl super::BlockchainManager { Ok(()) } - async fn handle_incoming_block_batch_alt_chain( - &mut self, - batch: BlockBatch, - ) -> Result<(), anyhow::Error> { + /// Handles an incoming [`BlockBatch`] that does not follow the main-chain. + /// + /// This function will handle validating the alt-blocks to add them to our cache and reorging the + /// chain if the alt-chain has a higher cumulative difficulty. + /// + /// This function will also handle banning the peer and canceling the block downloader if the + /// alt block is invalid or if a reorg fails. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. + async fn handle_incoming_block_batch_alt_chain(&mut self, batch: BlockBatch) { for (block, txs) in batch.blocks { - let txs = txs - .into_par_iter() - .map(|tx| { - let tx = new_tx_verification_data(tx)?; - Ok((tx.tx_hash, tx)) - }) - .collect::>()?; + // async blocks work as try blocks. + let res = async { + let txs = txs + .into_par_iter() + .map(|tx| { + let tx = new_tx_verification_data(tx)?; + Ok((tx.tx_hash, tx)) + }) + .collect::>()?; + + self.handle_incoming_alt_block(block, txs).await?; + + Ok(()) + } + .await; - self.handle_incoming_alt_block(block, txs).await?; + if let Err(e) = res { + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); + return; + } } - - Ok(()) } + /// Handles an incoming alt [`Block`]. + /// + /// This function will do some pre-validation of the alt block, then if the cumulative difficulty + /// of the alt chain is higher than the main chain it will attempt a reorg otherwise it will add + /// the alt block to the alt block cache. + /// + /// # Errors + /// + /// This will return an [`Err`] if: + /// - The alt block was invalid. + /// - An attempt to reorg the chain failed. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn handle_incoming_alt_block( &mut self, block: Block, @@ -157,8 +228,6 @@ impl super::BlockchainManager { > self.cached_blockchain_context.cumulative_difficulty { self.try_do_reorg(alt_block_info).await?; - // TODO: ban the peer if the reorg failed. - return Ok(()); } @@ -172,6 +241,21 @@ impl super::BlockchainManager { Ok(()) } + /// Attempt a re-org with the given top block of the alt-chain. + /// + /// This function will take a write lock on [`REORG_LOCK`] and then set up the blockchain database + /// and context cache to verify the alt-chain. It will then attempt to verify and add each block + /// in the alt-chain to tha main-chain. Releasing the lock on [`REORG_LOCK`] when finished. + /// + /// # Errors + /// + /// This function will return an [`Err`] if the re-org was unsuccessful, if this happens the chain + /// will be returned back into its state it was at when then function was called. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. async fn try_do_reorg( &mut self, top_alt_block: AltBlockInformation, @@ -230,6 +314,21 @@ impl super::BlockchainManager { } } + /// Verify and add a list of [`AltBlockInformation`]s to the main-chain. + /// + /// This function assumes the first [`AltBlockInformation`] is the next block in the blockchain + /// for the blockchain database and the context cache, or in other words that the blockchain database + /// and context cache has had the top blocks popped to where the alt-chain meets the main-chain. + /// + /// # Errors + /// + /// This function will return an [`Err`] if the alt-blocks were invalid, in this case the re-org should + /// be aborted and the chain should be returned to its previous state. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. async fn verify_add_alt_blocks_to_main_chain( &mut self, alt_blocks: Vec, @@ -263,6 +362,15 @@ impl super::BlockchainManager { Ok(()) } + /// Adds a [`VerifiedBlockInformation`] to the main-chain. + /// + /// This function will update the blockchain database and the context cache, it will also + /// update [`Self::cached_blockchain_context`]. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn add_valid_block_to_main_chain( &mut self, verified_block: VerifiedBlockInformation, diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index dc7381239..fbf4a88f5 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1,7 +1,11 @@ +use std::pin::pin; use std::time::Duration; use futures::StreamExt; -use tokio::{sync::mpsc, time::sleep}; +use tokio::{ + sync::{mpsc, Notify}, + time::sleep, +}; use tower::{Service, ServiceExt}; use tracing::instrument; @@ -27,6 +31,7 @@ pub async fn syncer( our_chain: CN, clearnet_interface: NetworkInterface, incoming_block_batch_tx: mpsc::Sender, + stop_current_block_downloader: Notify, block_downloader_config: BlockDownloaderConfig, ) -> Result<(), SyncerError> where @@ -82,10 +87,18 @@ where let mut block_batch_stream = clearnet_interface.block_downloader(our_chain.clone(), block_downloader_config); - while let Some(batch) = block_batch_stream.next().await { - tracing::debug!("Got batch, len: {}", batch.blocks.len()); - if incoming_block_batch_tx.send(batch).await.is_err() { - return Err(SyncerError::IncomingBlockChannelClosed); + loop { + tokio::select! { + _ = stop_current_block_downloader.notified() => { + tracing::info!("Stopping block downloader"); + break; + } + Some(batch) = block_batch_stream.next() => { + tracing::debug!("Got batch, len: {}", batch.blocks.len()); + if incoming_block_batch_tx.send(batch).await.is_err() { + return Err(SyncerError::IncomingBlockChannelClosed); + } + } } } } diff --git a/p2p/p2p/src/constants.rs b/p2p/p2p/src/constants.rs index 4e6daa732..0dbd188e2 100644 --- a/p2p/p2p/src/constants.rs +++ b/p2p/p2p/src/constants.rs @@ -10,13 +10,13 @@ pub(crate) const MAX_SEED_CONNECTIONS: usize = 3; pub(crate) const OUTBOUND_CONNECTION_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(5); /// The durations of a short ban. -pub(crate) const SHORT_BAN: Duration = Duration::from_secs(60 * 10); +pub const SHORT_BAN: Duration = Duration::from_secs(60 * 10); /// The durations of a medium ban. -pub(crate) const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); +pub const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); /// The durations of a long ban. -pub(crate) const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); +pub const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); /// The default amount of time between inbound diffusion flushes. pub(crate) const DIFFUSION_FLUSH_AVERAGE_SECONDS_INBOUND: Duration = Duration::from_secs(5); diff --git a/p2p_state.bin b/p2p_state.bin index fc17e050f72079bd7b70ecf81a388975afba2996..9faaaff02d13da96127ed10c9c77a8ab0a7b45ff 100644 GIT binary patch delta 4976 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 H2aEm>=pFwT delta 4415 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 From a16381ea61ee25acad3eaae47d8e81e07b6e6333 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 14 Sep 2024 13:49:41 +0100 Subject: [PATCH 30/46] broadcast new blocks + add commands --- binaries/cuprated/src/blockchain.rs | 15 ++++- binaries/cuprated/src/blockchain/free.rs | 32 +++++----- binaries/cuprated/src/blockchain/manager.rs | 32 +++++----- .../src/blockchain/manager/commands.rs | 17 ++++++ .../src/blockchain/manager/handler.rs | 57 +++++++++++++----- binaries/cuprated/src/blockchain/syncer.rs | 3 +- p2p_state.bin | Bin 172906 -> 0 bytes 7 files changed, 105 insertions(+), 51 deletions(-) create mode 100644 binaries/cuprated/src/blockchain/manager/commands.rs delete mode 100644 p2p_state.bin diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 46cc6a43c..f05878ac2 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -3,7 +3,8 @@ //! Will contain the chain manager and syncer. use futures::FutureExt; -use tokio::sync::mpsc; +use std::sync::Arc; +use tokio::sync::{mpsc, Notify}; use tower::{Service, ServiceExt}; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; @@ -26,6 +27,7 @@ use types::{ ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, ConsensusBlockchainReadHandle, }; +use crate::blockchain::free::INCOMING_BLOCK_TX; /// Checks if the genesis block is in the blockchain and adds it if not. pub async fn check_add_genesis( @@ -107,12 +109,17 @@ pub async fn init_blockchain_manager( block_downloader_config: BlockDownloaderConfig, ) { let (batch_tx, batch_rx) = mpsc::channel(1); + let stop_current_block_downloader = Arc::new(Notify::new()); + let (command_tx, command_rx) = mpsc::channel(1); + + INCOMING_BLOCK_TX.set(command_tx).unwrap(); tokio::spawn(syncer::syncer( blockchain_context_service.clone(), ChainService(blockchain_read_handle.clone()), - clearnet_interface, + clearnet_interface.clone(), batch_tx, + stop_current_block_downloader.clone(), block_downloader_config, )); @@ -121,8 +128,10 @@ pub async fn init_blockchain_manager( blockchain_read_handle, blockchain_context_service, block_verifier_service, + stop_current_block_downloader, + clearnet_interface.broadcast_svc(), ) .await; - tokio::spawn(manager.run(batch_rx)); + tokio::spawn(manager.run(batch_rx, command_rx)); } diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs index becdf3079..eb80a31d1 100644 --- a/binaries/cuprated/src/blockchain/free.rs +++ b/binaries/cuprated/src/blockchain/free.rs @@ -1,4 +1,3 @@ -use crate::blockchain::manager::IncomingBlock; use cuprate_blockchain::service::BlockchainReadHandle; use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_helper::cast::usize_to_u64; @@ -11,10 +10,11 @@ use std::collections::HashMap; use std::sync::OnceLock; use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; +use crate::blockchain::manager::commands::BlockchainManagerCommand; -static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); +pub static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); -#[derive(thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum IncomingBlockError { #[error("Unknown transactions in block.")] UnknownTransactions(Vec), @@ -28,8 +28,8 @@ pub async fn handle_incoming_block( block: Block, given_txs: Vec, blockchain_read_handle: &mut BlockchainReadHandle, -) -> Result<(), IncomingBlockError> { - if !block_exists(block.header.previous, blockchain_read_handle).expect("TODO") { +) -> Result { + if !block_exists(block.header.previous, blockchain_read_handle).await.expect("TODO") { return Err(IncomingBlockError::Orphan); } @@ -39,7 +39,14 @@ pub async fn handle_incoming_block( .await .expect("TODO") { - return Ok(()); + return Ok(false); + } + + // TODO: Get transactions from the tx pool first. + if given_txs.len() != block.transactions.len() { + return Err(IncomingBlockError::UnknownTransactions( + (0..usize_to_u64(block.transactions.len())).collect(), + )); } let prepped_txs = given_txs @@ -51,21 +58,14 @@ pub async fn handle_incoming_block( .collect::>() .map_err(IncomingBlockError::InvalidBlock)?; - // TODO: Get transactions from the tx pool first. - if given_txs.len() != block.transactions.len() { - return Err(IncomingBlockError::UnknownTransactions( - (0..usize_to_u64(block.transactions.len())).collect(), - )); - } - let Some(incoming_block_tx) = INCOMING_BLOCK_TX.get() else { - return Ok(()); + return Ok(false); }; let (response_tx, response_rx) = oneshot::channel(); incoming_block_tx - .send(IncomingBlock { + .send( BlockchainManagerCommand::AddBlock { block, prepped_txs, response_tx, @@ -73,7 +73,7 @@ pub async fn handle_incoming_block( .await .expect("TODO: don't actually panic here"); - response_rx.await.map_err(IncomingBlockError::InvalidBlock) + response_rx.await.unwrap().map_err(IncomingBlockError::InvalidBlock) } async fn block_exists( diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index ae5a1d3d8..b208436cf 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,4 +1,5 @@ mod handler; +pub(super) mod commands; use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; @@ -9,21 +10,20 @@ use cuprate_consensus::{ VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, }; use cuprate_p2p::block_downloader::BlockBatch; +use cuprate_p2p::BroadcastSvc; +use cuprate_p2p_core::ClearNet; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::{Chain, TransactionVerificationData}; use futures::StreamExt; use monero_serai::block::Block; use std::collections::HashMap; +use std::sync::Arc; use tokio::sync::mpsc; use tokio::sync::{oneshot, Notify}; use tower::{Service, ServiceExt}; use tracing::error; - -pub struct IncomingBlock { - pub block: Block, - pub prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, - pub response_tx: oneshot::Sender>, -} +use tracing_subscriber::fmt::time::FormatTime; +use crate::blockchain::manager::commands::BlockchainManagerCommand; pub struct BlockchainManager { blockchain_write_handle: BlockchainWriteHandle, @@ -35,7 +35,8 @@ pub struct BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, - stop_current_block_downloader: Notify, + stop_current_block_downloader: Arc, + broadcast_svc: BroadcastSvc, } impl BlockchainManager { @@ -48,6 +49,8 @@ impl BlockchainManager { TxVerifierService, ConsensusBlockchainReadHandle, >, + stop_current_block_downloader: Arc, + broadcast_svc: BroadcastSvc, ) -> Self { let BlockChainContextResponse::Context(blockchain_context) = blockchain_context_service .ready() @@ -66,13 +69,15 @@ impl BlockchainManager { blockchain_context_service, cached_blockchain_context: blockchain_context.unchecked_blockchain_context().clone(), block_verifier_service, + stop_current_block_downloader, + broadcast_svc, } } pub async fn run( mut self, mut block_batch_rx: mpsc::Receiver, - mut block_single_rx: mpsc::Receiver, + mut command_rx: mpsc::Receiver, ) { loop { tokio::select! { @@ -81,15 +86,8 @@ impl BlockchainManager { batch, ).await; } - Some(incoming_block) = block_single_rx.recv() => { - let IncomingBlock { - block, - prepped_txs, - response_tx - } = incoming_block; - - let res = self.handle_incoming_block(block, prepped_txs).await; - let _ = response_tx.send(res); + Some(incoming_command) = command_rx.recv() => { + self.handle_command(incoming_command).await; } else => { todo!("TODO: exit the BC manager") diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs new file mode 100644 index 000000000..1b6f4a48b --- /dev/null +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +use monero_serai::block::Block; +use tokio::sync::oneshot; + +use cuprate_types::TransactionVerificationData; + +pub enum BlockchainManagerCommand { + AddBlock { + block: Block, + prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, + response_tx: oneshot::Sender>, + }, + + PopBlocks, +} + diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 1bdae16c2..a925e7102 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, sync::Arc}; - +use bytes::Bytes; use futures::{TryFutureExt, TryStreamExt}; use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; +use std::{collections::HashMap, sync::Arc}; use tower::{Service, ServiceExt}; use tracing::info; @@ -13,15 +13,45 @@ use cuprate_consensus::{ ExtendedConsensusError, VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, }; -use cuprate_p2p::{block_downloader::BlockBatch, constants::LONG_BAN}; +use cuprate_helper::cast::usize_to_u64; +use cuprate_p2p::{block_downloader::BlockBatch, constants::LONG_BAN, BroadcastRequest}; use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse, BlockchainWriteRequest}, AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; +use crate::blockchain::manager::commands::BlockchainManagerCommand; impl super::BlockchainManager { + pub async fn handle_command(&mut self, command: BlockchainManagerCommand) { + match command { + BlockchainManagerCommand::AddBlock { + block, + prepped_txs, + response_tx + } => { + let res = self.handle_incoming_block(block, prepped_txs).await; + + drop(response_tx.send(res)); + } + BlockchainManagerCommand::PopBlocks => todo!() + } + } + + async fn broadcast_block(&mut self, block_bytes: Bytes, blockchain_height: usize) { + self.broadcast_svc + .ready() + .await + .expect("TODO") + .call(BroadcastRequest::Block { + block_bytes, + current_blockchain_height: usize_to_u64(blockchain_height), + }) + .await + .expect("TODO"); + } + /// Handle an incoming [`Block`]. /// /// This function will route to [`Self::handle_incoming_alt_block`] if the block does not follow @@ -56,8 +86,12 @@ impl super::BlockchainManager { panic!("Incorrect response!"); }; + let block_blob = Bytes::copy_from_slice(&verified_block.block_blob); self.add_valid_block_to_main_chain(verified_block).await; + self.broadcast_block(block_blob, self.cached_blockchain_context.chain_height) + .await; + Ok(true) } @@ -101,11 +135,6 @@ impl super::BlockchainManager { batch.blocks.first().unwrap().0.number().unwrap() ); - let ban_cancel_download = || { - batch.peer_handle.ban_peer(LONG_BAN); - self.stop_current_block_downloader.notify_one(); - }; - let batch_prep_res = self .block_verifier_service .ready() @@ -119,7 +148,8 @@ impl super::BlockchainManager { let prepped_blocks = match batch_prep_res { Ok(VerifyBlockResponse::MainChainBatchPrepped(prepped_blocks)) => prepped_blocks, Err(_) => { - ban_cancel_download(); + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); return; } _ => panic!("Incorrect response!"), @@ -134,10 +164,11 @@ impl super::BlockchainManager { .call(VerifyBlockRequest::MainChainPrepped { block, txs }) .await; - let VerifyBlockResponse::MainChain(verified_block) = match verify_res { + let verified_block = match verify_res { Ok(VerifyBlockResponse::MainChain(verified_block)) => verified_block, Err(_) => { - ban_cancel_download(); + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); return; } _ => panic!("Incorrect response!"), @@ -145,8 +176,6 @@ impl super::BlockchainManager { self.add_valid_block_to_main_chain(verified_block).await; } - - Ok(()) } /// Handles an incoming [`BlockBatch`] that does not follow the main-chain. @@ -175,7 +204,7 @@ impl super::BlockchainManager { self.handle_incoming_alt_block(block, txs).await?; - Ok(()) + Ok::<_, anyhow::Error>(()) } .await; diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index fbf4a88f5..286d8a502 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1,4 +1,5 @@ use std::pin::pin; +use std::sync::Arc; use std::time::Duration; use futures::StreamExt; @@ -31,7 +32,7 @@ pub async fn syncer( our_chain: CN, clearnet_interface: NetworkInterface, incoming_block_batch_tx: mpsc::Sender, - stop_current_block_downloader: Notify, + stop_current_block_downloader: Arc, block_downloader_config: BlockDownloaderConfig, ) -> Result<(), SyncerError> where diff --git a/p2p_state.bin b/p2p_state.bin deleted file mode 100644 index 9faaaff02d13da96127ed10c9c77a8ab0a7b45ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172906 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%0A{RoxM>5@T7r~%u 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 HlFl(~ry$*{w%YsfQ&{yJW`Qmfi_z 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 QLsy&BFD~>|7LKg^9}&O5%>V!Z From d2ab8e20c45cea6cb040c2e16ef792f47c5b1682 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 15 Sep 2024 01:20:24 +0100 Subject: [PATCH 31/46] add fluffy block handler --- binaries/cuprated/src/blockchain.rs | 4 +- binaries/cuprated/src/blockchain/free.rs | 17 +++++-- binaries/cuprated/src/blockchain/manager.rs | 4 +- .../src/blockchain/manager/commands.rs | 1 - .../src/blockchain/manager/handler.rs | 6 +-- binaries/cuprated/src/p2p/request_handler.rs | 50 ++++++++++++++++++- p2p/p2p-core/src/protocol.rs | 2 + p2p/p2p-core/src/protocol/try_from.rs | 1 + 8 files changed, 71 insertions(+), 14 deletions(-) diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index f05878ac2..2668dd8f4 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -22,12 +22,14 @@ mod manager; mod syncer; mod types; +use crate::blockchain::free::INCOMING_BLOCK_TX; use manager::BlockchainManager; use types::{ ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, ConsensusBlockchainReadHandle, }; -use crate::blockchain::free::INCOMING_BLOCK_TX; + +pub use free::{handle_incoming_block, IncomingBlockError}; /// Checks if the genesis block is in the blockchain and adds it if not. pub async fn check_add_genesis( diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs index eb80a31d1..6f44572cb 100644 --- a/binaries/cuprated/src/blockchain/free.rs +++ b/binaries/cuprated/src/blockchain/free.rs @@ -1,3 +1,4 @@ +use crate::blockchain::manager::commands::BlockchainManagerCommand; use cuprate_blockchain::service::BlockchainReadHandle; use cuprate_consensus::transactions::new_tx_verification_data; use cuprate_helper::cast::usize_to_u64; @@ -10,14 +11,13 @@ use std::collections::HashMap; use std::sync::OnceLock; use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; -use crate::blockchain::manager::commands::BlockchainManagerCommand; pub static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); #[derive(Debug, thiserror::Error)] pub enum IncomingBlockError { #[error("Unknown transactions in block.")] - UnknownTransactions(Vec), + UnknownTransactions([u8; 32], Vec), #[error("The block has an unknown parent.")] Orphan, #[error(transparent)] @@ -29,7 +29,10 @@ pub async fn handle_incoming_block( given_txs: Vec, blockchain_read_handle: &mut BlockchainReadHandle, ) -> Result { - if !block_exists(block.header.previous, blockchain_read_handle).await.expect("TODO") { + if !block_exists(block.header.previous, blockchain_read_handle) + .await + .expect("TODO") + { return Err(IncomingBlockError::Orphan); } @@ -45,6 +48,7 @@ pub async fn handle_incoming_block( // TODO: Get transactions from the tx pool first. if given_txs.len() != block.transactions.len() { return Err(IncomingBlockError::UnknownTransactions( + block_hash, (0..usize_to_u64(block.transactions.len())).collect(), )); } @@ -65,7 +69,7 @@ pub async fn handle_incoming_block( let (response_tx, response_rx) = oneshot::channel(); incoming_block_tx - .send( BlockchainManagerCommand::AddBlock { + .send(BlockchainManagerCommand::AddBlock { block, prepped_txs, response_tx, @@ -73,7 +77,10 @@ pub async fn handle_incoming_block( .await .expect("TODO: don't actually panic here"); - response_rx.await.unwrap().map_err(IncomingBlockError::InvalidBlock) + response_rx + .await + .unwrap() + .map_err(IncomingBlockError::InvalidBlock) } async fn block_exists( diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index b208436cf..d5a7ff9aa 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,6 +1,7 @@ -mod handler; pub(super) mod commands; +mod handler; +use crate::blockchain::manager::commands::BlockchainManagerCommand; use crate::blockchain::types::ConsensusBlockchainReadHandle; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::context::RawBlockChainContext; @@ -23,7 +24,6 @@ use tokio::sync::{oneshot, Notify}; use tower::{Service, ServiceExt}; use tracing::error; use tracing_subscriber::fmt::time::FormatTime; -use crate::blockchain::manager::commands::BlockchainManagerCommand; pub struct BlockchainManager { blockchain_write_handle: BlockchainWriteHandle, diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs index 1b6f4a48b..c60c7ef0b 100644 --- a/binaries/cuprated/src/blockchain/manager/commands.rs +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -14,4 +14,3 @@ pub enum BlockchainManagerCommand { PopBlocks, } - diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index a925e7102..50268761c 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -20,8 +20,8 @@ use cuprate_types::{ AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; -use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; use crate::blockchain::manager::commands::BlockchainManagerCommand; +use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; impl super::BlockchainManager { pub async fn handle_command(&mut self, command: BlockchainManagerCommand) { @@ -29,13 +29,13 @@ impl super::BlockchainManager { BlockchainManagerCommand::AddBlock { block, prepped_txs, - response_tx + response_tx, } => { let res = self.handle_incoming_block(block, prepped_txs).await; drop(response_tx.send(res)); } - BlockchainManagerCommand::PopBlocks => todo!() + BlockchainManagerCommand::PopBlocks => todo!(), } } diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index 0a4412fa3..ee219392f 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -2,17 +2,24 @@ use bytes::Bytes; use cuprate_p2p_core::{ProtocolRequest, ProtocolResponse}; use futures::future::BoxFuture; use futures::FutureExt; +use monero_serai::block::Block; +use monero_serai::transaction::Transaction; +use rayon::prelude::*; use std::task::{Context, Poll}; use tower::{Service, ServiceExt}; use tracing::trace; +use crate::blockchain::{handle_incoming_block, IncomingBlockError}; use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_consensus::transactions::new_tx_verification_data; +use cuprate_fixed_bytes::ByteArray; +use cuprate_helper::asynch::rayon_spawn_async; use cuprate_helper::cast::usize_to_u64; use cuprate_helper::map::split_u128_into_low_high_bits; use cuprate_p2p::constants::{MAX_BLOCKCHAIN_SUPPLEMENT_LEN, MAX_BLOCK_BATCH_LEN}; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::BlockCompleteEntry; -use cuprate_wire::protocol::{ChainRequest, ChainResponse, GetObjectsRequest, GetObjectsResponse}; +use cuprate_wire::protocol::{ChainRequest, ChainResponse, FluffyMissingTransactionsRequest, GetObjectsRequest, GetObjectsResponse, NewFluffyBlock}; #[derive(Clone)] pub struct P2pProtocolRequestHandler { @@ -39,7 +46,7 @@ impl Service for P2pProtocolRequestHandler { ProtocolRequest::FluffyMissingTxs(_) => async { Ok(ProtocolResponse::NA) }.boxed(), ProtocolRequest::GetTxPoolCompliment(_) => async { Ok(ProtocolResponse::NA) }.boxed(), ProtocolRequest::NewBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), - ProtocolRequest::NewFluffyBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + ProtocolRequest::NewFluffyBlock(block) => new_fluffy_block(self.blockchain_read_handle.clone(), block).boxed(), ProtocolRequest::NewTransactions(_) => async { Ok(ProtocolResponse::NA) }.boxed(), } } @@ -125,3 +132,42 @@ async fn get_chain( first_block: first_missing_block.map_or(Bytes::new(), Bytes::from), })) } + +async fn new_fluffy_block( + mut blockchain_read_handle: BlockchainReadHandle, + incoming_block: NewFluffyBlock, +) -> Result { + let peer_blockchain_height = incoming_block.current_blockchain_height; + + let (block, txs) = rayon_spawn_async(move || { + let block = Block::read(&mut incoming_block.b.block.as_ref())?; + let txs = incoming_block + .b + .txs + .take_normal() + .expect("TODO") + .into_par_iter() + .map(|tx| { + let tx = Transaction::read(&mut tx.as_ref())?; + Ok(tx) + }) + .collect::>()?; + + Ok::<_, tower::BoxError>((block, txs)) + }) + .await?; + + let res = handle_incoming_block(block, txs, &mut blockchain_read_handle).await; + + match res { + Err(IncomingBlockError::UnknownTransactions(block_hash, tx_indexes)) => { + return Ok(ProtocolResponse::FluffyMissingTxs(FluffyMissingTransactionsRequest{ + block_hash: ByteArray::from(block_hash), + current_blockchain_height: peer_blockchain_height, + missing_tx_indices: tx_indexes, + })) + } + Err(IncomingBlockError::InvalidBlock(e)) => Err(e)?, + Err(IncomingBlockError::Orphan) | Ok(_) => Ok(ProtocolResponse::NA), + } +} diff --git a/p2p/p2p-core/src/protocol.rs b/p2p/p2p-core/src/protocol.rs index 5e4f4d7e5..fc3cb7c86 100644 --- a/p2p/p2p-core/src/protocol.rs +++ b/p2p/p2p-core/src/protocol.rs @@ -116,6 +116,7 @@ pub enum ProtocolResponse { GetChain(ChainResponse), NewFluffyBlock(NewFluffyBlock), NewTransactions(NewTransactions), + FluffyMissingTxs(FluffyMissingTransactionsRequest), NA, } @@ -139,6 +140,7 @@ impl PeerResponse { ProtocolResponse::GetChain(_) => MessageID::GetChain, ProtocolResponse::NewFluffyBlock(_) => MessageID::NewBlock, ProtocolResponse::NewTransactions(_) => MessageID::NewFluffyBlock, + ProtocolResponse::FluffyMissingTxs(_) => MessageID::FluffyMissingTxs, ProtocolResponse::NA => return None, }, diff --git a/p2p/p2p-core/src/protocol/try_from.rs b/p2p/p2p-core/src/protocol/try_from.rs index 8a0b67d26..b3c5203db 100644 --- a/p2p/p2p-core/src/protocol/try_from.rs +++ b/p2p/p2p-core/src/protocol/try_from.rs @@ -75,6 +75,7 @@ impl TryFrom for ProtocolMessage { ProtocolResponse::NewFluffyBlock(val) => ProtocolMessage::NewFluffyBlock(val), ProtocolResponse::GetChain(val) => ProtocolMessage::ChainEntryResponse(val), ProtocolResponse::GetObjects(val) => ProtocolMessage::GetObjectsResponse(val), + ProtocolResponse::FluffyMissingTxs(val) => ProtocolMessage::FluffyMissingTransactionsRequest(val), ProtocolResponse::NA => return Err(MessageConversionError), }) } From 291ffe324d37c76f5dcf14be6709e183da60830b Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 15 Sep 2024 01:59:05 +0100 Subject: [PATCH 32/46] fix new block handling --- binaries/cuprated/src/blockchain/free.rs | 18 ++++++++++---- binaries/cuprated/src/p2p/request_handler.rs | 25 +++++++++++++------- p2p/p2p-core/src/protocol/try_from.rs | 4 +++- storage/blockchain/src/service/read.rs | 14 ++++++----- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs index 6f44572cb..b32bb7336 100644 --- a/binaries/cuprated/src/blockchain/free.rs +++ b/binaries/cuprated/src/blockchain/free.rs @@ -7,13 +7,15 @@ use cuprate_types::Chain; use monero_serai::block::Block; use monero_serai::transaction::Transaction; use rayon::prelude::*; -use std::collections::HashMap; -use std::sync::OnceLock; +use std::collections::{HashMap, HashSet}; +use std::sync::{Mutex, OnceLock}; use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; pub static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); +pub static BLOCKS_BEING_HANDLED: OnceLock>> = OnceLock::new(); + #[derive(Debug, thiserror::Error)] pub enum IncomingBlockError { #[error("Unknown transactions in block.")] @@ -62,6 +64,10 @@ pub async fn handle_incoming_block( .collect::>() .map_err(IncomingBlockError::InvalidBlock)?; + if !BLOCKS_BEING_HANDLED.get_or_init(|| Mutex::new(HashSet::new())).lock().unwrap().insert(block_hash) { + return Ok(false); + } + let Some(incoming_block_tx) = INCOMING_BLOCK_TX.get() else { return Ok(false); }; @@ -77,10 +83,14 @@ pub async fn handle_incoming_block( .await .expect("TODO: don't actually panic here"); - response_rx + let res =response_rx .await .unwrap() - .map_err(IncomingBlockError::InvalidBlock) + .map_err(IncomingBlockError::InvalidBlock); + + BLOCKS_BEING_HANDLED.get().unwrap().lock().unwrap().remove(&block_hash); + + res } async fn block_exists( diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index ee219392f..e3245e7ea 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -19,7 +19,10 @@ use cuprate_helper::map::split_u128_into_low_high_bits; use cuprate_p2p::constants::{MAX_BLOCKCHAIN_SUPPLEMENT_LEN, MAX_BLOCK_BATCH_LEN}; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::BlockCompleteEntry; -use cuprate_wire::protocol::{ChainRequest, ChainResponse, FluffyMissingTransactionsRequest, GetObjectsRequest, GetObjectsResponse, NewFluffyBlock}; +use cuprate_wire::protocol::{ + ChainRequest, ChainResponse, FluffyMissingTransactionsRequest, GetObjectsRequest, + GetObjectsResponse, NewFluffyBlock, +}; #[derive(Clone)] pub struct P2pProtocolRequestHandler { @@ -46,7 +49,9 @@ impl Service for P2pProtocolRequestHandler { ProtocolRequest::FluffyMissingTxs(_) => async { Ok(ProtocolResponse::NA) }.boxed(), ProtocolRequest::GetTxPoolCompliment(_) => async { Ok(ProtocolResponse::NA) }.boxed(), ProtocolRequest::NewBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), - ProtocolRequest::NewFluffyBlock(block) => new_fluffy_block(self.blockchain_read_handle.clone(), block).boxed(), + ProtocolRequest::NewFluffyBlock(block) => { + new_fluffy_block(self.blockchain_read_handle.clone(), block).boxed() + } ProtocolRequest::NewTransactions(_) => async { Ok(ProtocolResponse::NA) }.boxed(), } } @@ -158,14 +163,16 @@ async fn new_fluffy_block( .await?; let res = handle_incoming_block(block, txs, &mut blockchain_read_handle).await; - - match res { + + match res { Err(IncomingBlockError::UnknownTransactions(block_hash, tx_indexes)) => { - return Ok(ProtocolResponse::FluffyMissingTxs(FluffyMissingTransactionsRequest{ - block_hash: ByteArray::from(block_hash), - current_blockchain_height: peer_blockchain_height, - missing_tx_indices: tx_indexes, - })) + return Ok(ProtocolResponse::FluffyMissingTxs( + FluffyMissingTransactionsRequest { + block_hash: ByteArray::from(block_hash), + current_blockchain_height: peer_blockchain_height, + missing_tx_indices: tx_indexes, + }, + )) } Err(IncomingBlockError::InvalidBlock(e)) => Err(e)?, Err(IncomingBlockError::Orphan) | Ok(_) => Ok(ProtocolResponse::NA), diff --git a/p2p/p2p-core/src/protocol/try_from.rs b/p2p/p2p-core/src/protocol/try_from.rs index b3c5203db..ae21a07d3 100644 --- a/p2p/p2p-core/src/protocol/try_from.rs +++ b/p2p/p2p-core/src/protocol/try_from.rs @@ -75,7 +75,9 @@ impl TryFrom for ProtocolMessage { ProtocolResponse::NewFluffyBlock(val) => ProtocolMessage::NewFluffyBlock(val), ProtocolResponse::GetChain(val) => ProtocolMessage::ChainEntryResponse(val), ProtocolResponse::GetObjects(val) => ProtocolMessage::GetObjectsResponse(val), - ProtocolResponse::FluffyMissingTxs(val) => ProtocolMessage::FluffyMissingTransactionsRequest(val), + ProtocolResponse::FluffyMissingTxs(val) => { + ProtocolMessage::FluffyMissingTransactionsRequest(val) + } ProtocolResponse::NA => return Err(MessageConversionError), }) } diff --git a/storage/blockchain/src/service/read.rs b/storage/blockchain/src/service/read.rs index e502c9f1d..89f924248 100644 --- a/storage/blockchain/src/service/read.rs +++ b/storage/blockchain/src/service/read.rs @@ -268,12 +268,14 @@ fn find_block(env: &ConcreteEnv, block_hash: BlockHash) -> ResponseResult { let table_alt_block_heights = env_inner.open_db_ro::(&tx_ro)?; - let height = table_alt_block_heights.get(&block_hash)?; - - Ok(BlockchainResponse::FindBlock(Some(( - Chain::Alt(height.chain_id.into()), - height.height, - )))) + match table_alt_block_heights.get(&block_hash) { + Ok(height) => Ok(BlockchainResponse::FindBlock(Some(( + Chain::Alt(height.chain_id.into()), + height.height, + )))), + Err(RuntimeError::KeyNotFound) => Ok(BlockchainResponse::FindBlock(None)), + Err(e) => return Err(e), + } } /// [`BlockchainReadRequest::FilterUnknownHashes`]. From c0a3f7a71898b23398802aa06d91041261f38cae Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 15 Sep 2024 18:08:49 +0100 Subject: [PATCH 33/46] small cleanup --- binaries/cuprated/src/blockchain/free.rs | 8 ++++---- binaries/cuprated/src/blockchain/manager/commands.rs | 2 -- binaries/cuprated/src/blockchain/manager/handler.rs | 1 - binaries/cuprated/src/config.rs | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs index b32bb7336..d44d273c7 100644 --- a/binaries/cuprated/src/blockchain/free.rs +++ b/binaries/cuprated/src/blockchain/free.rs @@ -64,14 +64,14 @@ pub async fn handle_incoming_block( .collect::>() .map_err(IncomingBlockError::InvalidBlock)?; - if !BLOCKS_BEING_HANDLED.get_or_init(|| Mutex::new(HashSet::new())).lock().unwrap().insert(block_hash) { - return Ok(false); - } - let Some(incoming_block_tx) = INCOMING_BLOCK_TX.get() else { return Ok(false); }; + if !BLOCKS_BEING_HANDLED.get_or_init(|| Mutex::new(HashSet::new())).lock().unwrap().insert(block_hash) { + return Ok(false); + } + let (response_tx, response_rx) = oneshot::channel(); incoming_block_tx diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs index c60c7ef0b..800144f05 100644 --- a/binaries/cuprated/src/blockchain/manager/commands.rs +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -11,6 +11,4 @@ pub enum BlockchainManagerCommand { prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, response_tx: oneshot::Sender>, }, - - PopBlocks, } diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 50268761c..6a92ba735 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -35,7 +35,6 @@ impl super::BlockchainManager { drop(response_tx.send(res)); } - BlockchainManagerCommand::PopBlocks => todo!(), } } diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index c71c40c87..0cbb0cdf5 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -26,7 +26,7 @@ impl CupratedConfig { pub fn clearnet_config(&self) -> P2PConfig { P2PConfig { network: Network::Mainnet, - outbound_connections: 64, + outbound_connections: 16, extra_outbound_connections: 0, max_inbound_connections: 0, gray_peers_percent: 0.7, From fa54df27d330f26d417d3dd89d8b919936eccf9a Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Mon, 16 Sep 2024 01:39:01 +0100 Subject: [PATCH 34/46] increase outbound peer count --- binaries/cuprated/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index 0cbb0cdf5..c71c40c87 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -26,7 +26,7 @@ impl CupratedConfig { pub fn clearnet_config(&self) -> P2PConfig { P2PConfig { network: Network::Mainnet, - outbound_connections: 16, + outbound_connections: 64, extra_outbound_connections: 0, max_inbound_connections: 0, gray_peers_percent: 0.7, From 69f9d84ae1706310a9bf581ba2d4a28603541ac4 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 01:53:47 +0100 Subject: [PATCH 35/46] fix merge --- Cargo.lock | 48 ++++++++++++++ binaries/cuprated/src/blockchain.rs | 2 +- binaries/cuprated/src/blockchain/free.rs | 16 ++++- binaries/cuprated/src/blockchain/syncer.rs | 26 ++++---- binaries/cuprated/src/main.rs | 1 + binaries/cuprated/src/p2p/request_handler.rs | 68 ++++++++++++++------ consensus/src/lib.rs | 14 ++-- p2p/p2p-core/src/protocol/try_from.rs | 1 + p2p/p2p/src/client_pool.rs | 13 ++++ p2p/p2p/src/constants.rs | 4 +- p2p/p2p/src/lib.rs | 7 +- storage/blockchain/Cargo.toml | 1 - 12 files changed, 147 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca5c1546c..dd303bf69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1862,6 +1862,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1899,6 +1909,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "page_size" version = "0.6.0" @@ -2514,6 +2530,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2894,6 +2919,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", ] [[package]] @@ -2902,7 +2939,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", "tracing-core", + "tracing-log", ] [[package]] @@ -2985,6 +3027,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.5" diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 2668dd8f4..649dcef0a 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -35,7 +35,7 @@ pub use free::{handle_incoming_block, IncomingBlockError}; pub async fn check_add_genesis( blockchain_read_handle: &mut BlockchainReadHandle, blockchain_write_handle: &mut BlockchainWriteHandle, - network: &Network, + network: Network, ) { // Try to get the chain height, will fail if the genesis block is not in the DB. if blockchain_read_handle diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/free.rs index d44d273c7..56c68edf4 100644 --- a/binaries/cuprated/src/blockchain/free.rs +++ b/binaries/cuprated/src/blockchain/free.rs @@ -68,7 +68,12 @@ pub async fn handle_incoming_block( return Ok(false); }; - if !BLOCKS_BEING_HANDLED.get_or_init(|| Mutex::new(HashSet::new())).lock().unwrap().insert(block_hash) { + if !BLOCKS_BEING_HANDLED + .get_or_init(|| Mutex::new(HashSet::new())) + .lock() + .unwrap() + .insert(block_hash) + { return Ok(false); } @@ -83,12 +88,17 @@ pub async fn handle_incoming_block( .await .expect("TODO: don't actually panic here"); - let res =response_rx + let res = response_rx .await .unwrap() .map_err(IncomingBlockError::InvalidBlock); - BLOCKS_BEING_HANDLED.get().unwrap().lock().unwrap().remove(&block_hash); + BLOCKS_BEING_HANDLED + .get() + .unwrap() + .lock() + .unwrap() + .remove(&block_hash); res } diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index 286d8a502..4b286cebb 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use std::time::Duration; use futures::StreamExt; +use tokio::time::interval; use tokio::{ sync::{mpsc, Notify}, time::sleep, @@ -17,6 +18,8 @@ use cuprate_p2p::{ }; use cuprate_p2p_core::ClearNet; +const CHECK_SYNC_FREQUENCY: Duration = Duration::from_secs(30); + /// An error returned from the [`syncer`]. #[derive(Debug, thiserror::Error)] pub enum SyncerError { @@ -50,6 +53,8 @@ where { tracing::info!("Starting blockchain syncer"); + let mut check_sync_interval = interval(CHECK_SYNC_FREQUENCY); + let BlockChainContextResponse::Context(mut blockchain_ctx) = context_svc .ready() .await? @@ -59,26 +64,21 @@ where panic!("Blockchain context service returned wrong response!"); }; - let mut peer_sync_watch = clearnet_interface.top_sync_stream(); + let client_pool = clearnet_interface.client_pool(); tracing::debug!("Waiting for new sync info in top sync channel"); - while let Some(top_sync_info) = peer_sync_watch.next().await { - tracing::info!( - "New sync info seen, top height: {}, top block hash: {}", - top_sync_info.chain_height, - hex::encode(top_sync_info.top_hash) - ); + loop { + check_sync_interval.tick().await; - // The new info could be from a peer giving us a block, so wait a couple seconds to allow the block to - // be added to our blockchain. - sleep(Duration::from_secs(2)).await; + tracing::trace!("Checking connected peers to see if we are behind",); check_update_blockchain_context(&mut context_svc, &mut blockchain_ctx).await?; let raw_blockchain_context = blockchain_ctx.unchecked_blockchain_context(); - if top_sync_info.cumulative_difficulty <= raw_blockchain_context.cumulative_difficulty { - tracing::debug!("New peer sync info is not ahead, nothing to do."); + if !client_pool.contains_client_with_more_cumulative_difficulty( + raw_blockchain_context.cumulative_difficulty, + ) { continue; } @@ -103,8 +103,6 @@ where } } } - - Ok(()) } async fn check_update_blockchain_context( diff --git a/binaries/cuprated/src/main.rs b/binaries/cuprated/src/main.rs index 775843df9..ad7382d74 100644 --- a/binaries/cuprated/src/main.rs +++ b/binaries/cuprated/src/main.rs @@ -16,6 +16,7 @@ mod config; mod constants; mod p2p; mod rpc; +mod signals; mod statics; mod txpool; diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index e3245e7ea..86507f1fa 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -1,5 +1,5 @@ use bytes::Bytes; -use cuprate_p2p_core::{ProtocolRequest, ProtocolResponse}; +use cuprate_p2p_core::{NetworkZone, ProtocolRequest, ProtocolResponse}; use futures::future::BoxFuture; use futures::FutureExt; use monero_serai::block::Block; @@ -16,7 +16,8 @@ use cuprate_fixed_bytes::ByteArray; use cuprate_helper::asynch::rayon_spawn_async; use cuprate_helper::cast::usize_to_u64; use cuprate_helper::map::split_u128_into_low_high_bits; -use cuprate_p2p::constants::{MAX_BLOCKCHAIN_SUPPLEMENT_LEN, MAX_BLOCK_BATCH_LEN}; +use cuprate_p2p::constants::MAX_BLOCK_BATCH_LEN; +use cuprate_p2p_core::client::PeerInformation; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::BlockCompleteEntry; use cuprate_wire::protocol::{ @@ -25,12 +26,12 @@ use cuprate_wire::protocol::{ }; #[derive(Clone)] -pub struct P2pProtocolRequestHandler { - pub(crate) blockchain_read_handle: BlockchainReadHandle, +pub struct P2pProtocolRequestHandlerMaker { + pub blockchain_read_handle: BlockchainReadHandle, } -impl Service for P2pProtocolRequestHandler { - type Response = ProtocolResponse; +impl Service> for P2pProtocolRequestHandlerMaker { + type Response = P2pProtocolRequestHandler; type Error = tower::BoxError; type Future = BoxFuture<'static, Result>; @@ -38,22 +39,38 @@ impl Service for P2pProtocolRequestHandler { Poll::Ready(Ok(())) } - fn call(&mut self, req: ProtocolRequest) -> Self::Future { - match req { - ProtocolRequest::GetObjects(req) => { - get_objects(self.blockchain_read_handle.clone(), req).boxed() - } - ProtocolRequest::GetChain(req) => { - get_chain(self.blockchain_read_handle.clone(), req).boxed() - } - ProtocolRequest::FluffyMissingTxs(_) => async { Ok(ProtocolResponse::NA) }.boxed(), - ProtocolRequest::GetTxPoolCompliment(_) => async { Ok(ProtocolResponse::NA) }.boxed(), - ProtocolRequest::NewBlock(_) => async { Ok(ProtocolResponse::NA) }.boxed(), - ProtocolRequest::NewFluffyBlock(block) => { - new_fluffy_block(self.blockchain_read_handle.clone(), block).boxed() - } - ProtocolRequest::NewTransactions(_) => async { Ok(ProtocolResponse::NA) }.boxed(), + fn call(&mut self, peer_information: PeerInformation) -> Self::Future { + // TODO: check peer info. + + let blockchain_read_handle = self.blockchain_read_handle.clone(); + + async { + Ok(P2pProtocolRequestHandler { + peer_information, + blockchain_read_handle, + }) } + .boxed() + } +} + +#[derive(Clone)] +pub struct P2pProtocolRequestHandler { + peer_information: PeerInformation, + blockchain_read_handle: BlockchainReadHandle, +} + +impl Service for P2pProtocolRequestHandler { + type Response = ProtocolResponse; + type Error = tower::BoxError; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: ProtocolRequest) -> Self::Future { + async { Ok(ProtocolResponse::NA) }.boxed() } } @@ -73,6 +90,9 @@ async fn get_objects( // de-allocate the backing [`Bytes`] drop(req); + return Ok(ProtocolResponse::NA); + /* + let res = blockchain_read_handle .oneshot(BlockchainReadRequest::BlockCompleteEntries(block_ids)) .await?; @@ -91,6 +111,8 @@ async fn get_objects( missed_ids: missed_ids.into(), current_blockchain_height: usize_to_u64(current_blockchain_height), })) + + */ } async fn get_chain( @@ -100,7 +122,9 @@ async fn get_chain( if req.block_ids.is_empty() { Err("No block hashes sent in a `ChainRequest`")?; } + return Ok(ProtocolResponse::NA); + /* if req.block_ids.len() > MAX_BLOCKCHAIN_SUPPLEMENT_LEN { Err("Too many block hashes in a `ChainRequest`")?; } @@ -136,6 +160,8 @@ async fn get_chain( m_block_weights: vec![], first_block: first_missing_block.map_or(Bytes::new(), Bytes::from), })) + + */ } async fn new_fluffy_block( diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index e104cec9e..1e4731588 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -37,6 +37,7 @@ pub use context::{ pub use transactions::{TxVerifierService, VerifyTxRequest, VerifyTxResponse}; // re-export. +pub use cuprate_consensus_rules::genesis::generate_genesis_block; pub use cuprate_types::{ blockchain::{BlockchainReadRequest, BlockchainResponse}, HardFork, @@ -68,13 +69,10 @@ pub enum ExtendedConsensusError { pub fn initialize_verifier( database: D, ctx_svc: Ctx, -) -> Result< - ( - BlockVerifierService, D>, - TxVerifierService, - ), - ConsensusError, -> +) -> ( + BlockVerifierService, D>, + TxVerifierService, +) where D: Database + Clone + Send + Sync + 'static, D::Future: Send + 'static, @@ -90,7 +88,7 @@ where { let tx_svc = TxVerifierService::new(database.clone()); let block_svc = BlockVerifierService::new(ctx_svc, tx_svc.clone(), database); - Ok((block_svc, tx_svc)) + (block_svc, tx_svc) } use __private::Database; diff --git a/p2p/p2p-core/src/protocol/try_from.rs b/p2p/p2p-core/src/protocol/try_from.rs index d3a7260fd..b3c1ae3d6 100644 --- a/p2p/p2p-core/src/protocol/try_from.rs +++ b/p2p/p2p-core/src/protocol/try_from.rs @@ -71,6 +71,7 @@ impl TryFrom for ProtocolMessage { ProtocolResponse::NewFluffyBlock(val) => Self::NewFluffyBlock(val), ProtocolResponse::GetChain(val) => Self::ChainEntryResponse(val), ProtocolResponse::GetObjects(val) => Self::GetObjectsResponse(val), + ProtocolResponse::FluffyMissingTxs(val) => Self::FluffyMissingTransactionsRequest(val), ProtocolResponse::NA => return Err(MessageConversionError), }) } diff --git a/p2p/p2p/src/client_pool.rs b/p2p/p2p/src/client_pool.rs index 77d3b6e5c..735be76d1 100644 --- a/p2p/p2p/src/client_pool.rs +++ b/p2p/p2p/src/client_pool.rs @@ -153,6 +153,19 @@ impl ClientPool { self.borrow_clients(&peers).collect() } + + pub fn contains_client_with_more_cumulative_difficulty( + &self, + cumulative_difficulty: u128, + ) -> bool { + self.clients + .iter() + .find(|element| { + let sync_data = element.value().info.core_sync_data.lock().unwrap(); + sync_data.cumulative_difficulty() > cumulative_difficulty + }) + .is_some() + } } mod sealed { diff --git a/p2p/p2p/src/constants.rs b/p2p/p2p/src/constants.rs index f2349600a..f79bcbf00 100644 --- a/p2p/p2p/src/constants.rs +++ b/p2p/p2p/src/constants.rs @@ -23,7 +23,7 @@ pub(crate) const SHORT_BAN: Duration = Duration::from_secs(60 * 10); pub(crate) const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); /// The durations of a long ban. -pub(crate) const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); +pub const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); /// The default amount of time between inbound diffusion flushes. pub(crate) const DIFFUSION_FLUSH_AVERAGE_SECONDS_INBOUND: Duration = Duration::from_secs(5); @@ -53,7 +53,7 @@ pub(crate) const INITIAL_CHAIN_REQUESTS_TO_SEND: usize = 3; /// The enforced maximum amount of blocks to request in a batch. /// /// Requesting more than this will cause the peer to disconnect and potentially lead to bans. -pub(crate) const MAX_BLOCK_BATCH_LEN: usize = 100; +pub const MAX_BLOCK_BATCH_LEN: usize = 100; /// The timeout that the block downloader will use for requests. pub(crate) const BLOCK_DOWNLOADER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index dbff56d5c..37416b2d6 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -174,9 +174,8 @@ impl NetworkInterface { self.address_book.clone() } - /// Pulls a client from the client pool, returning it in a guard that will return it there when it's - /// dropped. - pub fn borrow_client(&self, peer: &InternalPeerID) -> Option> { - self.pool.borrow_client(peer) + /// TODO + pub fn client_pool(&self) -> &Arc> { + &self.pool } } diff --git a/storage/blockchain/Cargo.toml b/storage/blockchain/Cargo.toml index c1473b6df..6eecb892b 100644 --- a/storage/blockchain/Cargo.toml +++ b/storage/blockchain/Cargo.toml @@ -35,7 +35,6 @@ serde = { workspace = true, optional = true } tower = { workspace = true } thread_local = { workspace = true, optional = true } rayon = { workspace = true, optional = true } -bytes = "1.6.0" [dev-dependencies] cuprate-helper = { path = "../../helper", features = ["thread", "cast"] } From caaeceda2ee22f712d9f88c73b0aafa4a537fab5 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 03:36:33 +0100 Subject: [PATCH 36/46] clean up the blockchain manger --- binaries/cuprated/src/blockchain.rs | 43 +------ .../src/blockchain/{free.rs => interface.rs} | 0 binaries/cuprated/src/blockchain/manager.rs | 110 ++++++++++++------ .../src/blockchain/manager/handler.rs | 72 +++++++----- binaries/cuprated/src/constants.rs | 3 + 5 files changed, 125 insertions(+), 103 deletions(-) rename binaries/cuprated/src/blockchain/{free.rs => interface.rs} (100%) diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 649dcef0a..80d10bf2a 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -17,19 +17,19 @@ use cuprate_types::{ VerifiedBlockInformation, }; -mod free; +mod interface; mod manager; mod syncer; mod types; -use crate::blockchain::free::INCOMING_BLOCK_TX; +use crate::blockchain::interface::INCOMING_BLOCK_TX; use manager::BlockchainManager; use types::{ ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, ConsensusBlockchainReadHandle, }; -pub use free::{handle_incoming_block, IncomingBlockError}; +pub use interface::{handle_incoming_block, IncomingBlockError}; /// Checks if the genesis block is in the blockchain and adds it if not. pub async fn check_add_genesis( @@ -100,40 +100,3 @@ pub async fn init_consensus( Ok((block_verifier_svc, tx_verifier_svc, ctx_service)) } - -/// Initializes the blockchain manager task and syncer. -pub async fn init_blockchain_manager( - clearnet_interface: NetworkInterface, - blockchain_write_handle: BlockchainWriteHandle, - blockchain_read_handle: BlockchainReadHandle, - blockchain_context_service: BlockChainContextService, - block_verifier_service: ConcreteBlockVerifierService, - block_downloader_config: BlockDownloaderConfig, -) { - let (batch_tx, batch_rx) = mpsc::channel(1); - let stop_current_block_downloader = Arc::new(Notify::new()); - let (command_tx, command_rx) = mpsc::channel(1); - - INCOMING_BLOCK_TX.set(command_tx).unwrap(); - - tokio::spawn(syncer::syncer( - blockchain_context_service.clone(), - ChainService(blockchain_read_handle.clone()), - clearnet_interface.clone(), - batch_tx, - stop_current_block_downloader.clone(), - block_downloader_config, - )); - - let manager = BlockchainManager::new( - blockchain_write_handle, - blockchain_read_handle, - blockchain_context_service, - block_verifier_service, - stop_current_block_downloader, - clearnet_interface.broadcast_svc(), - ) - .await; - - tokio::spawn(manager.run(batch_rx, command_rx)); -} diff --git a/binaries/cuprated/src/blockchain/free.rs b/binaries/cuprated/src/blockchain/interface.rs similarity index 100% rename from binaries/cuprated/src/blockchain/free.rs rename to binaries/cuprated/src/blockchain/interface.rs diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index d5a7ff9aa..314519d04 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,8 +1,13 @@ pub(super) mod commands; mod handler; +use crate::blockchain::interface::INCOMING_BLOCK_TX; use crate::blockchain::manager::commands::BlockchainManagerCommand; -use crate::blockchain::types::ConsensusBlockchainReadHandle; +use crate::blockchain::types::ChainService; +use crate::blockchain::{ + syncer, + types::{ConcreteBlockVerifierService, ConsensusBlockchainReadHandle}, +}; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::context::RawBlockChainContext; use cuprate_consensus::{ @@ -10,8 +15,8 @@ use cuprate_consensus::{ BlockVerifierService, ExtendedConsensusError, TxVerifierService, VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, }; -use cuprate_p2p::block_downloader::BlockBatch; -use cuprate_p2p::BroadcastSvc; +use cuprate_p2p::block_downloader::{BlockBatch, BlockDownloaderConfig}; +use cuprate_p2p::{BroadcastSvc, NetworkInterface}; use cuprate_p2p_core::ClearNet; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; use cuprate_types::{Chain, TransactionVerificationData}; @@ -25,55 +30,86 @@ use tower::{Service, ServiceExt}; use tracing::error; use tracing_subscriber::fmt::time::FormatTime; +pub async fn init_blockchain_manger( + clearnet_interface: NetworkInterface, + blockchain_write_handle: BlockchainWriteHandle, + blockchain_read_handle: BlockchainReadHandle, + mut blockchain_context_service: BlockChainContextService, + block_verifier_service: ConcreteBlockVerifierService, + block_downloader_config: BlockDownloaderConfig, +) { + let (batch_tx, batch_rx) = mpsc::channel(1); + let stop_current_block_downloader = Arc::new(Notify::new()); + let (command_tx, command_rx) = mpsc::channel(1); + + INCOMING_BLOCK_TX.set(command_tx).unwrap(); + + tokio::spawn(syncer::syncer( + blockchain_context_service.clone(), + ChainService(blockchain_read_handle.clone()), + clearnet_interface.clone(), + batch_tx, + stop_current_block_downloader.clone(), + block_downloader_config, + )); + + let BlockChainContextResponse::Context(blockchain_context) = blockchain_context_service + .ready() + .await + .expect("TODO") + .call(BlockChainContextRequest::GetContext) + .await + .expect("TODO") + else { + panic!("Blockchain context service returned wrong response!"); + }; + + let manger = BlockchainManager { + blockchain_write_handle, + blockchain_read_handle, + blockchain_context_service, + cached_blockchain_context: blockchain_context.unchecked_blockchain_context().clone(), + block_verifier_service, + stop_current_block_downloader, + broadcast_svc, + }; + + tokio::spawn(manger.run(batch_rx, command_rx)); +} + +/// The blockchain manager. +/// +/// This handles all mutation of the blockchain, anything that changes the state of the blockchain must +/// go through this. +/// +/// Other parts of Cuprate can interface with this by using the functions in [`interface`](super::interface). pub struct BlockchainManager { + /// The [`BlockchainWriteHandle`], this is the _only_ part of Cuprate where a [`BlockchainWriteHandle`] + /// is held. blockchain_write_handle: BlockchainWriteHandle, + /// A [`BlockchainReadHandle`]. blockchain_read_handle: BlockchainReadHandle, + // TODO: Improve the API of the cache service. + // TODO: rename the cache service -> `BlockchainContextService`. + /// The blockchain context cache, this caches the current state of the blockchain to quickly calculate/retrieve + /// values without needing to go to a [`BlockchainReadHandle`]. blockchain_context_service: BlockChainContextService, + /// A cached context representing the current state. cached_blockchain_context: RawBlockChainContext, + /// The block verifier service, to verify incoming blocks. block_verifier_service: BlockVerifierService< BlockChainContextService, TxVerifierService, ConsensusBlockchainReadHandle, >, + /// A [`Notify`] to tell the [syncer](syncer::syncer) that we want to cancel this current download + /// attempt. stop_current_block_downloader: Arc, + /// The broadcast service, to broadcast new blocks. broadcast_svc: BroadcastSvc, } impl BlockchainManager { - pub async fn new( - blockchain_write_handle: BlockchainWriteHandle, - blockchain_read_handle: BlockchainReadHandle, - mut blockchain_context_service: BlockChainContextService, - block_verifier_service: BlockVerifierService< - BlockChainContextService, - TxVerifierService, - ConsensusBlockchainReadHandle, - >, - stop_current_block_downloader: Arc, - broadcast_svc: BroadcastSvc, - ) -> Self { - let BlockChainContextResponse::Context(blockchain_context) = blockchain_context_service - .ready() - .await - .expect("TODO") - .call(BlockChainContextRequest::GetContext) - .await - .expect("TODO") - else { - panic!("Blockchain context service returned wrong response!"); - }; - - Self { - blockchain_write_handle, - blockchain_read_handle, - blockchain_context_service, - cached_blockchain_context: blockchain_context.unchecked_blockchain_context().clone(), - block_verifier_service, - stop_current_block_downloader, - broadcast_svc, - } - } - pub async fn run( mut self, mut block_batch_rx: mpsc::Receiver, diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 6a92ba735..7d4c81c4a 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -21,6 +21,7 @@ use cuprate_types::{ }; use crate::blockchain::manager::commands::BlockchainManagerCommand; +use crate::constants::PANIC_CRITICAL_SERVICE_ERROR; use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; impl super::BlockchainManager { @@ -42,13 +43,13 @@ impl super::BlockchainManager { self.broadcast_svc .ready() .await - .expect("TODO") + .expect("Broadcast service cannot error.") .call(BroadcastRequest::Block { block_bytes, current_blockchain_height: usize_to_u64(blockchain_height), }) .await - .expect("TODO"); + .expect("Broadcast service cannot error."); } /// Handle an incoming [`Block`]. @@ -75,7 +76,7 @@ impl super::BlockchainManager { .block_verifier_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(VerifyBlockRequest::MainChain { block, prepared_txs, @@ -102,7 +103,7 @@ impl super::BlockchainManager { /// # Panics /// /// This function will panic if the batch is empty or if any internal service returns an unexpected - /// error that we cannot recover from. + /// error that we cannot recover from or if the incoming batch contains no blocks. pub async fn handle_incoming_block_batch(&mut self, batch: BlockBatch) { let (first_block, _) = batch .blocks @@ -127,7 +128,7 @@ impl super::BlockchainManager { /// # Panics /// /// This function will panic if any internal service returns an unexpected error that we cannot - /// recover from. + /// recover from or if the incoming batch contains no blocks. async fn handle_incoming_block_batch_main_chain(&mut self, batch: BlockBatch) { info!( "Handling batch to main chain height: {}", @@ -138,7 +139,7 @@ impl super::BlockchainManager { .block_verifier_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(VerifyBlockRequest::MainChainBatchPrepareBlocks { blocks: batch.blocks, }) @@ -159,7 +160,7 @@ impl super::BlockchainManager { .block_verifier_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(VerifyBlockRequest::MainChainPrepped { block, txs }) .await; @@ -189,8 +190,12 @@ impl super::BlockchainManager { /// /// This function will panic if any internal service returns an unexpected error that we cannot /// recover from. - async fn handle_incoming_block_batch_alt_chain(&mut self, batch: BlockBatch) { - for (block, txs) in batch.blocks { + async fn handle_incoming_block_batch_alt_chain(&mut self, mut batch: BlockBatch) { + // TODO: this needs testing (this whole section does but this specifically). + + let mut blocks = batch.blocks.into_iter(); + + while let Some((block, txs)) = blocks.next() { // async blocks work as try blocks. let res = async { let txs = txs @@ -201,16 +206,28 @@ impl super::BlockchainManager { }) .collect::>()?; - self.handle_incoming_alt_block(block, txs).await?; + let reorged = self.handle_incoming_alt_block(block, txs).await?; - Ok::<_, anyhow::Error>(()) + Ok::<_, anyhow::Error>(reorged) } .await; - if let Err(e) = res { - batch.peer_handle.ban_peer(LONG_BAN); - self.stop_current_block_downloader.notify_one(); - return; + match res { + Err(e) => { + batch.peer_handle.ban_peer(LONG_BAN); + self.stop_current_block_downloader.notify_one(); + return; + } + // the chain was reorged + Ok(true) => { + // Collect the remaining blocks and add them to the main chain instead. + batch.blocks = blocks.collect(); + self.handle_incoming_block_batch_main_chain(batch).await; + + return; + } + // continue adding alt blocks. + Ok(false) => (), } } } @@ -221,6 +238,8 @@ impl super::BlockchainManager { /// of the alt chain is higher than the main chain it will attempt a reorg otherwise it will add /// the alt block to the alt block cache. /// + /// This function returns a [`bool`] indicating if the chain was reorganised ([`true`]) or not ([`false`]). + /// /// # Errors /// /// This will return an [`Err`] if: @@ -235,12 +254,12 @@ impl super::BlockchainManager { &mut self, block: Block, prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, - ) -> Result<(), anyhow::Error> { + ) -> Result { let VerifyBlockResponse::AltChain(alt_block_info) = self .block_verifier_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(VerifyBlockRequest::AltChain { block, prepared_txs, @@ -250,23 +269,24 @@ impl super::BlockchainManager { panic!("Incorrect response!"); }; - // TODO: check in consensus crate if alt block already exists. + // TODO: check in consensus crate if alt block with this hash already exists. + // If this alt chain if alt_block_info.cumulative_difficulty > self.cached_blockchain_context.cumulative_difficulty { self.try_do_reorg(alt_block_info).await?; - return Ok(()); + return Ok(true); } self.blockchain_write_handle .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainWriteRequest::WriteAltBlock(alt_block_info)) .await?; - Ok(()) + Ok(false) } /// Attempt a re-org with the given top block of the alt-chain. @@ -294,7 +314,7 @@ impl super::BlockchainManager { .blockchain_read_handle .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainReadRequest::AltBlocksInChain( top_alt_block.chain_id, )) @@ -312,12 +332,12 @@ impl super::BlockchainManager { .blockchain_write_handle .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainWriteRequest::PopBlocks( current_main_chain_height - split_height + 1, )) .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) else { panic!("Incorrect response!"); }; @@ -325,12 +345,12 @@ impl super::BlockchainManager { self.blockchain_context_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockChainContextRequest::PopBlocks { numb_blocks: current_main_chain_height - split_height + 1, }) .await - .expect("TODO"); + .expect(PANIC_CRITICAL_SERVICE_ERROR); let reorg_res = self.verify_add_alt_blocks_to_main_chain(alt_blocks).await; diff --git a/binaries/cuprated/src/constants.rs b/binaries/cuprated/src/constants.rs index 9463d4763..d4dfc1ad4 100644 --- a/binaries/cuprated/src/constants.rs +++ b/binaries/cuprated/src/constants.rs @@ -14,6 +14,9 @@ pub const VERSION_BUILD: &str = if cfg!(debug_assertions) { formatcp!("{VERSION}-release") }; +pub const PANIC_CRITICAL_SERVICE_ERROR: &str = + "A service critical to Cuprate's function returned an unexpected error."; + #[cfg(test)] mod test { use super::*; From 8cff481227016e6c39e88d8ba0f041779eab20a0 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 21:35:42 +0100 Subject: [PATCH 37/46] add more docs + cleanup imports --- binaries/cuprated/src/blockchain.rs | 28 ++++--- binaries/cuprated/src/blockchain/interface.rs | 78 +++++++++++++++---- binaries/cuprated/src/blockchain/manager.rs | 78 ++++++++++--------- .../src/blockchain/manager/handler.rs | 31 +++++--- binaries/cuprated/src/blockchain/syncer.rs | 5 +- binaries/cuprated/src/blockchain/types.rs | 44 +++++------ binaries/cuprated/src/p2p/request_handler.rs | 21 +++-- binaries/cuprated/src/signals.rs | 6 ++ consensus/src/block.rs | 10 +-- consensus/src/lib.rs | 1 - p2p/p2p/src/client_pool.rs | 11 +-- p2p/p2p/src/lib.rs | 4 +- 12 files changed, 180 insertions(+), 137 deletions(-) diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 80d10bf2a..52043a065 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -1,11 +1,11 @@ //! Blockchain //! //! Will contain the chain manager and syncer. +use std::sync::Arc; use futures::FutureExt; -use std::sync::Arc; use tokio::sync::{mpsc, Notify}; -use tower::{Service, ServiceExt}; +use tower::{BoxError, Service, ServiceExt}; use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; use cuprate_consensus::{generate_genesis_block, BlockChainContextService, ContextConfig}; @@ -22,11 +22,8 @@ mod manager; mod syncer; mod types; -use crate::blockchain::interface::INCOMING_BLOCK_TX; -use manager::BlockchainManager; use types::{ - ChainService, ConcreteBlockVerifierService, ConcreteTxVerifierService, - ConsensusBlockchainReadHandle, + ConcreteBlockVerifierService, ConcreteTxVerifierService, ConsensusBlockchainReadHandle, }; pub use interface::{handle_incoming_block, IncomingBlockError}; @@ -51,6 +48,9 @@ pub async fn check_add_genesis( let genesis = generate_genesis_block(network); + assert_eq!(genesis.miner_transaction.prefix().outputs.len(), 1); + assert!(genesis.transactions.is_empty()); + blockchain_write_handle .ready() .await @@ -87,16 +87,14 @@ pub async fn init_consensus( ), tower::BoxError, > { - let ctx_service = cuprate_consensus::initialize_blockchain_context( - context_config, - ConsensusBlockchainReadHandle(blockchain_read_handle.clone()), - ) - .await?; + let read_handle = ConsensusBlockchainReadHandle::new(blockchain_read_handle, BoxError::from); + + let ctx_service = + cuprate_consensus::initialize_blockchain_context(context_config, read_handle.clone()) + .await?; - let (block_verifier_svc, tx_verifier_svc) = cuprate_consensus::initialize_verifier( - ConsensusBlockchainReadHandle(blockchain_read_handle), - ctx_service.clone(), - ); + let (block_verifier_svc, tx_verifier_svc) = + cuprate_consensus::initialize_verifier(read_handle, ctx_service.clone()); Ok((block_verifier_svc, tx_verifier_svc, ctx_service)) } diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index 56c68edf4..18376f391 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -1,39 +1,74 @@ -use crate::blockchain::manager::commands::BlockchainManagerCommand; -use cuprate_blockchain::service::BlockchainReadHandle; -use cuprate_consensus::transactions::new_tx_verification_data; -use cuprate_helper::cast::usize_to_u64; -use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use cuprate_types::Chain; -use monero_serai::block::Block; -use monero_serai::transaction::Transaction; +//! The blockchain manger interface. +//! +//! This module contains all the functions to mutate the blockchains state in any way, through the +//! blockchain manger. +use std::{ + collections::{HashMap, HashSet}, + sync::{Mutex, OnceLock}, +}; + +use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; -use std::collections::{HashMap, HashSet}; -use std::sync::{Mutex, OnceLock}; use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; -pub static INCOMING_BLOCK_TX: OnceLock> = OnceLock::new(); +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_consensus::transactions::new_tx_verification_data; +use cuprate_helper::cast::usize_to_u64; +use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse}, + Chain, +}; +use crate::{ + blockchain::manager::BlockchainManagerCommand, constants::PANIC_CRITICAL_SERVICE_ERROR, +}; + +/// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manger. +pub static COMMAND_TX: OnceLock> = OnceLock::new(); + +/// A [`HashSet`] of block hashes that the blockchain manager is currently handling. pub static BLOCKS_BEING_HANDLED: OnceLock>> = OnceLock::new(); +/// An error that can be returned from [`handle_incoming_block`]. #[derive(Debug, thiserror::Error)] pub enum IncomingBlockError { + /// Some transactions in the block were unknown. + /// + /// The inner values are the block hash and the indexes of the missing txs in the block. #[error("Unknown transactions in block.")] UnknownTransactions([u8; 32], Vec), + /// We are missing the block's parent. #[error("The block has an unknown parent.")] Orphan, + /// The block was invalid. #[error(transparent)] InvalidBlock(anyhow::Error), } +/// Try to add a new block to the blockchain. +/// +/// This returns a [`bool`] indicating if the block was added to the main-chain ([`true`]) or an alt-chain +/// ([`false`]). +/// +/// If we already knew about this block or the blockchain manger is not setup yet `Ok(false)` is returned. +/// +/// # Errors +/// +/// This function will return an error if: +/// - the block was invalid +/// - we are missing transactions +/// - the block's parent is unknown pub async fn handle_incoming_block( block: Block, given_txs: Vec, blockchain_read_handle: &mut BlockchainReadHandle, ) -> Result { + // FIXME: we should look in the tx-pool for txs when that is ready. + if !block_exists(block.header.previous, blockchain_read_handle) .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) { return Err(IncomingBlockError::Orphan); } @@ -42,12 +77,12 @@ pub async fn handle_incoming_block( if block_exists(block_hash, blockchain_read_handle) .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) { return Ok(false); } - // TODO: Get transactions from the tx pool first. + // TODO: remove this when we have a working tx-pool. if given_txs.len() != block.transactions.len() { return Err(IncomingBlockError::UnknownTransactions( block_hash, @@ -55,6 +90,7 @@ pub async fn handle_incoming_block( )); } + // TODO: check we actually go given the right txs. let prepped_txs = given_txs .into_par_iter() .map(|tx| { @@ -64,19 +100,25 @@ pub async fn handle_incoming_block( .collect::>() .map_err(IncomingBlockError::InvalidBlock)?; - let Some(incoming_block_tx) = INCOMING_BLOCK_TX.get() else { + let Some(incoming_block_tx) = COMMAND_TX.get() else { + // We could still be starting up the blockchain manger, so just return this as there is nothing + // else we can do. return Ok(false); }; + // Add the blocks hash to the blocks being handled. if !BLOCKS_BEING_HANDLED .get_or_init(|| Mutex::new(HashSet::new())) .lock() .unwrap() .insert(block_hash) { + // If another place is already adding this block then we can stop. return Ok(false); } + // From this point on we MUST not early return without removing the block hash from `BLOCKS_BEING_HANDLED`. + let (response_tx, response_rx) = oneshot::channel(); incoming_block_tx @@ -86,13 +128,14 @@ pub async fn handle_incoming_block( response_tx, }) .await - .expect("TODO: don't actually panic here"); + .expect("TODO: don't actually panic here, an err means we are shutting down"); let res = response_rx .await - .unwrap() + .expect("The blockchain manager will always respond") .map_err(IncomingBlockError::InvalidBlock); + // Remove the block hash from the blocks being handled. BLOCKS_BEING_HANDLED .get() .unwrap() @@ -103,6 +146,7 @@ pub async fn handle_incoming_block( res } +/// Check if we have a block with the given hash. async fn block_exists( block_hash: [u8; 32], blockchain_read_handle: &mut BlockchainReadHandle, diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 314519d04..2d3c1798d 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -1,35 +1,46 @@ -pub(super) mod commands; -mod handler; +use std::{collections::HashMap, sync::Arc}; -use crate::blockchain::interface::INCOMING_BLOCK_TX; -use crate::blockchain::manager::commands::BlockchainManagerCommand; -use crate::blockchain::types::ChainService; -use crate::blockchain::{ - syncer, - types::{ConcreteBlockVerifierService, ConsensusBlockchainReadHandle}, -}; -use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; -use cuprate_consensus::context::RawBlockChainContext; -use cuprate_consensus::{ - BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, - BlockVerifierService, ExtendedConsensusError, TxVerifierService, VerifyBlockRequest, - VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, -}; -use cuprate_p2p::block_downloader::{BlockBatch, BlockDownloaderConfig}; -use cuprate_p2p::{BroadcastSvc, NetworkInterface}; -use cuprate_p2p_core::ClearNet; -use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use cuprate_types::{Chain, TransactionVerificationData}; use futures::StreamExt; use monero_serai::block::Block; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio::sync::{oneshot, Notify}; +use tokio::sync::{mpsc, oneshot, Notify}; use tower::{Service, ServiceExt}; use tracing::error; -use tracing_subscriber::fmt::time::FormatTime; +use cuprate_blockchain::service::{BlockchainReadHandle, BlockchainWriteHandle}; +use cuprate_consensus::{ + context::RawBlockChainContext, BlockChainContextRequest, BlockChainContextResponse, + BlockChainContextService, BlockVerifierService, ExtendedConsensusError, TxVerifierService, + VerifyBlockRequest, VerifyBlockResponse, VerifyTxRequest, VerifyTxResponse, +}; +use cuprate_p2p::{ + block_downloader::{BlockBatch, BlockDownloaderConfig}, + BroadcastSvc, NetworkInterface, +}; +use cuprate_p2p_core::ClearNet; +use cuprate_types::{ + blockchain::{BlockchainReadRequest, BlockchainResponse}, + Chain, TransactionVerificationData, +}; + +use crate::{ + blockchain::{ + interface::COMMAND_TX, + syncer, + types::ChainService, + types::{ConcreteBlockVerifierService, ConsensusBlockchainReadHandle}, + }, + constants::PANIC_CRITICAL_SERVICE_ERROR, +}; + +mod commands; +mod handler; + +pub use commands::BlockchainManagerCommand; + +/// Initialize the blockchain manger. +/// +/// This function sets up the [`BlockchainManager`] and the [`syncer`] so that the functions in [`interface`](super::interface) +/// can be called. pub async fn init_blockchain_manger( clearnet_interface: NetworkInterface, blockchain_write_handle: BlockchainWriteHandle, @@ -42,7 +53,7 @@ pub async fn init_blockchain_manger( let stop_current_block_downloader = Arc::new(Notify::new()); let (command_tx, command_rx) = mpsc::channel(1); - INCOMING_BLOCK_TX.set(command_tx).unwrap(); + COMMAND_TX.set(command_tx).unwrap(); tokio::spawn(syncer::syncer( blockchain_context_service.clone(), @@ -56,10 +67,10 @@ pub async fn init_blockchain_manger( let BlockChainContextResponse::Context(blockchain_context) = blockchain_context_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockChainContextRequest::GetContext) .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) else { panic!("Blockchain context service returned wrong response!"); }; @@ -71,7 +82,7 @@ pub async fn init_blockchain_manger( cached_blockchain_context: blockchain_context.unchecked_blockchain_context().clone(), block_verifier_service, stop_current_block_downloader, - broadcast_svc, + broadcast_svc: clearnet_interface.broadcast_svc(), }; tokio::spawn(manger.run(batch_rx, command_rx)); @@ -97,11 +108,7 @@ pub struct BlockchainManager { /// A cached context representing the current state. cached_blockchain_context: RawBlockChainContext, /// The block verifier service, to verify incoming blocks. - block_verifier_service: BlockVerifierService< - BlockChainContextService, - TxVerifierService, - ConsensusBlockchainReadHandle, - >, + block_verifier_service: ConcreteBlockVerifierService, /// A [`Notify`] to tell the [syncer](syncer::syncer) that we want to cancel this current download /// attempt. stop_current_block_downloader: Arc, @@ -110,6 +117,7 @@ pub struct BlockchainManager { } impl BlockchainManager { + /// The [`BlockchainManager`] task. pub async fn run( mut self, mut block_batch_rx: mpsc::Receiver, diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 7d4c81c4a..62b54d697 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,8 +1,9 @@ +use std::{collections::HashMap, sync::Arc}; + use bytes::Bytes; use futures::{TryFutureExt, TryStreamExt}; use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; -use std::{collections::HashMap, sync::Arc}; use tower::{Service, ServiceExt}; use tracing::info; @@ -20,11 +21,16 @@ use cuprate_types::{ AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; -use crate::blockchain::manager::commands::BlockchainManagerCommand; -use crate::constants::PANIC_CRITICAL_SERVICE_ERROR; -use crate::{blockchain::types::ConsensusBlockchainReadHandle, signals::REORG_LOCK}; +use crate::{ + blockchain::{ + manager::commands::BlockchainManagerCommand, types::ConsensusBlockchainReadHandle, + }, + constants::PANIC_CRITICAL_SERVICE_ERROR, + signals::REORG_LOCK, +}; impl super::BlockchainManager { + /// Handle an incoming command from another part of Cuprate. pub async fn handle_command(&mut self, command: BlockchainManagerCommand) { match command { BlockchainManagerCommand::AddBlock { @@ -39,6 +45,7 @@ impl super::BlockchainManager { } } + /// Broadcast a valid block to the network. async fn broadcast_block(&mut self, block_bytes: Bytes, blockchain_height: usize) { self.broadcast_svc .ready() @@ -191,7 +198,7 @@ impl super::BlockchainManager { /// This function will panic if any internal service returns an unexpected error that we cannot /// recover from. async fn handle_incoming_block_batch_alt_chain(&mut self, mut batch: BlockBatch) { - // TODO: this needs testing (this whole section does but this specifically). + // TODO: this needs testing (this whole section does but alt-blocks specifically). let mut blocks = batch.blocks.into_iter(); @@ -394,7 +401,7 @@ impl super::BlockchainManager { .block_verifier_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(VerifyBlockRequest::MainChainPrepped { block: prepped_block, txs: prepped_txs, @@ -426,7 +433,7 @@ impl super::BlockchainManager { self.blockchain_context_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockChainContextRequest::Update(NewBlockData { block_hash: verified_block.block_hash, height: verified_block.height, @@ -438,24 +445,24 @@ impl super::BlockchainManager { cumulative_difficulty: verified_block.cumulative_difficulty, })) .await - .expect("TODO"); + .expect(PANIC_CRITICAL_SERVICE_ERROR); self.blockchain_write_handle .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainWriteRequest::WriteBlock(verified_block)) .await - .expect("TODO"); + .expect(PANIC_CRITICAL_SERVICE_ERROR); let BlockChainContextResponse::Context(blockchain_context) = self .blockchain_context_service .ready() .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockChainContextRequest::GetContext) .await - .expect("TODO") + .expect(PANIC_CRITICAL_SERVICE_ERROR) else { panic!("Incorrect response!"); }; diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index 4b286cebb..c772b1282 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1,6 +1,4 @@ -use std::pin::pin; -use std::sync::Arc; -use std::time::Duration; +use std::{pin::pin, sync::Arc, time::Duration}; use futures::StreamExt; use tokio::time::interval; @@ -18,6 +16,7 @@ use cuprate_p2p::{ }; use cuprate_p2p_core::ClearNet; +// FIXME: This whole module is not great and should be rewritten when the PeerSet is made. const CHECK_SYNC_FREQUENCY: Duration = Duration::from_secs(30); /// An error returned from the [`syncer`]. diff --git a/binaries/cuprated/src/blockchain/types.rs b/binaries/cuprated/src/blockchain/types.rs index 46576a46d..1bf921ecc 100644 --- a/binaries/cuprated/src/blockchain/types.rs +++ b/binaries/cuprated/src/blockchain/types.rs @@ -1,41 +1,32 @@ -use cuprate_blockchain::cuprate_database::RuntimeError; -use cuprate_blockchain::service::BlockchainReadHandle; +use std::task::{Context, Poll}; + +use futures::future::BoxFuture; +use futures::{FutureExt, TryFutureExt}; +use tower::{util::MapErr, Service}; + +use cuprate_blockchain::{cuprate_database::RuntimeError, service::BlockchainReadHandle}; use cuprate_consensus::{BlockChainContextService, BlockVerifierService, TxVerifierService}; use cuprate_p2p::block_downloader::{ChainSvcRequest, ChainSvcResponse}; use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use futures::future::{BoxFuture, MapErr}; -use futures::{FutureExt, TryFutureExt}; -use std::task::{Context, Poll}; -use tower::Service; +/// The [`BlockVerifierService`] with all generic types defined. pub type ConcreteBlockVerifierService = BlockVerifierService< BlockChainContextService, - TxVerifierService, + ConcreteTxVerifierService, ConsensusBlockchainReadHandle, >; +/// The [`TxVerifierService`] with all generic types defined. pub type ConcreteTxVerifierService = TxVerifierService; -#[derive(Clone)] -pub struct ConsensusBlockchainReadHandle(pub BlockchainReadHandle); - -impl Service for ConsensusBlockchainReadHandle { - type Response = BlockchainResponse; - type Error = tower::BoxError; - type Future = MapErr< - >::Future, - fn(RuntimeError) -> tower::BoxError, - >; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.0.poll_ready(cx).map_err(Into::into) - } - - fn call(&mut self, req: BlockchainReadRequest) -> Self::Future { - self.0.call(req).map_err(Into::into) - } -} +/// The [`BlockchainReadHandle`] with the [`tower::Service::Error`] mapped to conform to what the consensus crate requires. +pub type ConsensusBlockchainReadHandle = + MapErr tower::BoxError>; +/// That service that allows retrieving the chain state to give to the P2P crates, so we can figure out +/// what blocks we need. +/// +/// This has a more minimal interface than [`BlockchainReadRequest`] to make using the p2p crates easier. #[derive(Clone)] pub struct ChainService(pub BlockchainReadHandle); @@ -79,6 +70,7 @@ impl Service for ChainService { .call(BlockchainReadRequest::CompactChainHistory) .map_ok(|res| { // TODO create a custom request instead of hijacking this one. + // TODO: use the context cache. let BlockchainResponse::CompactChainHistory { cumulative_difficulty, .. diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index 86507f1fa..419c3b427 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -90,7 +90,7 @@ async fn get_objects( // de-allocate the backing [`Bytes`] drop(req); - return Ok(ProtocolResponse::NA); + Ok(ProtocolResponse::NA) /* let res = blockchain_read_handle @@ -122,7 +122,8 @@ async fn get_chain( if req.block_ids.is_empty() { Err("No block hashes sent in a `ChainRequest`")?; } - return Ok(ProtocolResponse::NA); + + Ok(ProtocolResponse::NA) /* if req.block_ids.len() > MAX_BLOCKCHAIN_SUPPLEMENT_LEN { @@ -191,15 +192,13 @@ async fn new_fluffy_block( let res = handle_incoming_block(block, txs, &mut blockchain_read_handle).await; match res { - Err(IncomingBlockError::UnknownTransactions(block_hash, tx_indexes)) => { - return Ok(ProtocolResponse::FluffyMissingTxs( - FluffyMissingTransactionsRequest { - block_hash: ByteArray::from(block_hash), - current_blockchain_height: peer_blockchain_height, - missing_tx_indices: tx_indexes, - }, - )) - } + Err(IncomingBlockError::UnknownTransactions(block_hash, tx_indexes)) => Ok( + ProtocolResponse::FluffyMissingTxs(FluffyMissingTransactionsRequest { + block_hash: ByteArray::from(block_hash), + current_blockchain_height: peer_blockchain_height, + missing_tx_indices: tx_indexes, + }), + ), Err(IncomingBlockError::InvalidBlock(e)) => Err(e)?, Err(IncomingBlockError::Orphan) | Ok(_) => Ok(ProtocolResponse::NA), } diff --git a/binaries/cuprated/src/signals.rs b/binaries/cuprated/src/signals.rs index cafd8cdbb..54467559a 100644 --- a/binaries/cuprated/src/signals.rs +++ b/binaries/cuprated/src/signals.rs @@ -1,3 +1,9 @@ +//! Signals for Cuprate state used throughout the binary. + use tokio::sync::RwLock; +/// Reorg lock. +/// +/// A [`RwLock`] where a write lock is taken during a reorg and a read lock can be taken +/// for any operation which must complete without a reorg happening. pub static REORG_LOCK: RwLock<()> = RwLock::const_new(()); diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 08e6cef0d..53eb14694 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -8,7 +8,6 @@ use std::{ }; use futures::FutureExt; -use monero_serai::generators::H; use monero_serai::{ block::Block, transaction::{Input, Transaction}, @@ -124,10 +123,7 @@ impl PreparedBlock { /// /// The randomX VM must be Some if RX is needed or this will panic. /// The randomX VM must also be initialised with the correct seed. - pub fn new( - block: Block, - randomx_vm: Option<&R>, - ) -> Result { + pub fn new(block: Block, randomx_vm: Option<&R>) -> Result { let (hf_version, hf_vote) = HardFork::from_block_header(&block.header) .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?; @@ -185,8 +181,8 @@ impl PreparedBlock { }) } - pub fn new_alt_block(block: AltBlockInformation) -> Result { - Ok(PreparedBlock { + pub fn new_alt_block(block: AltBlockInformation) -> Result { + Ok(Self { block_blob: block.block_blob, hf_vote: HardFork::from_version(block.block.header.hardfork_version) .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?, diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 1e4731588..7280f2ff5 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -65,7 +65,6 @@ pub enum ExtendedConsensusError { } /// Initialize the 2 verifier [`tower::Service`]s (block and transaction). -#[expect(clippy::type_complexity)] pub fn initialize_verifier( database: D, ctx_svc: Ctx, diff --git a/p2p/p2p/src/client_pool.rs b/p2p/p2p/src/client_pool.rs index 735be76d1..99981f0ce 100644 --- a/p2p/p2p/src/client_pool.rs +++ b/p2p/p2p/src/client_pool.rs @@ -158,13 +158,10 @@ impl ClientPool { &self, cumulative_difficulty: u128, ) -> bool { - self.clients - .iter() - .find(|element| { - let sync_data = element.value().info.core_sync_data.lock().unwrap(); - sync_data.cumulative_difficulty() > cumulative_difficulty - }) - .is_some() + self.clients.iter().any(|element| { + let sync_data = element.value().info.core_sync_data.lock().unwrap(); + sync_data.cumulative_difficulty() > cumulative_difficulty + }) } } diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index 37416b2d6..ad082fc6f 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -12,7 +12,6 @@ use tracing::{instrument, Instrument, Span}; use cuprate_async_buffer::BufferStream; use cuprate_p2p_core::{ client::Connector, - client::InternalPeerID, services::{AddressBookRequest, AddressBookResponse}, CoreSyncSvc, NetworkZone, ProtocolRequestHandlerMaker, }; @@ -27,7 +26,6 @@ mod inbound_server; use block_downloader::{BlockBatch, BlockDownloaderConfig, ChainSvcRequest, ChainSvcResponse}; pub use broadcast::{BroadcastRequest, BroadcastSvc}; -use client_pool::ClientPoolDropGuard; pub use config::{AddressBookConfig, P2PConfig}; use connection_maintainer::MakeConnectionRequest; @@ -175,7 +173,7 @@ impl NetworkInterface { } /// TODO - pub fn client_pool(&self) -> &Arc> { + pub const fn client_pool(&self) -> &Arc> { &self.pool } } From 4af0f896b00b74525419e375c69d9a9e56a0265c Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 21:39:18 +0100 Subject: [PATCH 38/46] fix typo --- binaries/cuprated/src/blockchain/manager/handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 62b54d697..4406421fb 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -300,7 +300,7 @@ impl super::BlockchainManager { /// /// This function will take a write lock on [`REORG_LOCK`] and then set up the blockchain database /// and context cache to verify the alt-chain. It will then attempt to verify and add each block - /// in the alt-chain to tha main-chain. Releasing the lock on [`REORG_LOCK`] when finished. + /// in the alt-chain to the main-chain. Releasing the lock on [`REORG_LOCK`] when finished. /// /// # Errors /// From 6702dbee11c45a0dce79ae647b0af3b90b917aff Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 21:56:42 +0100 Subject: [PATCH 39/46] fix doc --- p2p/p2p/src/block_downloader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/p2p/src/block_downloader.rs b/p2p/p2p/src/block_downloader.rs index eccb38502..b80b7aa47 100644 --- a/p2p/p2p/src/block_downloader.rs +++ b/p2p/p2p/src/block_downloader.rs @@ -1,6 +1,6 @@ //! # Block Downloader //! -//! This module contains the [`BlockDownloader`], which finds a chain to +//! This module contains the block downloader, which finds a chain to //! download from our connected peers and downloads it. See the actual //! `struct` documentation for implementation details. //! From 403964bc2216bd5f493358d9ccf10a0e6177172d Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Thu, 3 Oct 2024 22:52:47 +0100 Subject: [PATCH 40/46] remove unrelated changes --- binaries/cuprated/Cargo.toml | 4 +- binaries/cuprated/src/blockchain.rs | 16 +- binaries/cuprated/src/blockchain/interface.rs | 8 +- binaries/cuprated/src/blockchain/manager.rs | 5 +- .../src/blockchain/manager/commands.rs | 6 + .../src/blockchain/manager/handler.rs | 5 +- binaries/cuprated/src/blockchain/syncer.rs | 10 +- binaries/cuprated/src/config.rs | 61 ------ binaries/cuprated/src/p2p.rs | 1 - binaries/cuprated/src/p2p/core_sync_svc.rs | 51 ----- binaries/cuprated/src/p2p/request_handler.rs | 204 ------------------ binaries/cuprated/src/rpc/request_handler.rs | 1 - consensus/src/block.rs | 1 + helper/src/tx_utils.rs | 34 --- p2p/p2p-core/src/protocol.rs | 2 - p2p/p2p-core/src/protocol/try_from.rs | 1 - p2p/p2p/src/client_pool.rs | 2 + p2p/p2p/src/constants.rs | 7 +- p2p/p2p/src/lib.rs | 2 +- 19 files changed, 43 insertions(+), 378 deletions(-) delete mode 100644 binaries/cuprated/src/p2p/core_sync_svc.rs delete mode 100644 binaries/cuprated/src/rpc/request_handler.rs delete mode 100644 helper/src/tx_utils.rs diff --git a/binaries/cuprated/Cargo.toml b/binaries/cuprated/Cargo.toml index 080bb1176..325406bf7 100644 --- a/binaries/cuprated/Cargo.toml +++ b/binaries/cuprated/Cargo.toml @@ -73,8 +73,8 @@ tower = { workspace = true } tracing-subscriber = { workspace = true, features = ["std", "fmt", "default"] } tracing = { workspace = true } -#[lints] -#workspace = true +[lints] +workspace = true [profile.dev] panic = "abort" diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 52043a065..1a9c0b7ce 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -1,6 +1,6 @@ //! Blockchain //! -//! Will contain the chain manager and syncer. +//! Contains the blockchain manager, syncer and an interface to mutate the blockchain. use std::sync::Arc; use futures::FutureExt; @@ -17,7 +17,9 @@ use cuprate_types::{ VerifiedBlockInformation, }; -mod interface; +use crate::constants::PANIC_CRITICAL_SERVICE_ERROR; + +pub mod interface; mod manager; mod syncer; mod types; @@ -26,8 +28,6 @@ use types::{ ConcreteBlockVerifierService, ConcreteTxVerifierService, ConsensusBlockchainReadHandle, }; -pub use interface::{handle_incoming_block, IncomingBlockError}; - /// Checks if the genesis block is in the blockchain and adds it if not. pub async fn check_add_genesis( blockchain_read_handle: &mut BlockchainReadHandle, @@ -38,7 +38,7 @@ pub async fn check_add_genesis( if blockchain_read_handle .ready() .await - .unwrap() + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainReadRequest::ChainHeight) .await .is_ok() @@ -54,7 +54,7 @@ pub async fn check_add_genesis( blockchain_write_handle .ready() .await - .unwrap() + .expect(PANIC_CRITICAL_SERVICE_ERROR) .call(BlockchainWriteRequest::WriteBlock( VerifiedBlockInformation { block_blob: genesis.serialize(), @@ -72,7 +72,7 @@ pub async fn check_add_genesis( }, )) .await - .unwrap(); + .expect(PANIC_CRITICAL_SERVICE_ERROR); } /// Initializes the consensus services. @@ -85,7 +85,7 @@ pub async fn init_consensus( ConcreteTxVerifierService, BlockChainContextService, ), - tower::BoxError, + BoxError, > { let read_handle = ConsensusBlockchainReadHandle::new(blockchain_read_handle, BoxError::from); diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index 18376f391..189828e9c 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -1,6 +1,6 @@ //! The blockchain manger interface. //! -//! This module contains all the functions to mutate the blockchains state in any way, through the +//! This module contains all the functions to mutate the blockchain's state in any way, through the //! blockchain manger. use std::{ collections::{HashMap, HashSet}, @@ -28,6 +28,10 @@ use crate::{ pub static COMMAND_TX: OnceLock> = OnceLock::new(); /// A [`HashSet`] of block hashes that the blockchain manager is currently handling. +/// +/// This is used over something like a dashmap as we expect a lot of collisions in a short amount of +/// time for new blocks so we would lose the benefit of sharded locks. A dashmap is made up of `RwLocks` +/// which are also more expensive than `Mutex`s. pub static BLOCKS_BEING_HANDLED: OnceLock>> = OnceLock::new(); /// An error that can be returned from [`handle_incoming_block`]. @@ -90,7 +94,7 @@ pub async fn handle_incoming_block( )); } - // TODO: check we actually go given the right txs. + // TODO: check we actually got given the right txs. let prepped_txs = given_txs .into_par_iter() .map(|tx| { diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 2d3c1798d..8725aecaa 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -49,9 +49,10 @@ pub async fn init_blockchain_manger( block_verifier_service: ConcreteBlockVerifierService, block_downloader_config: BlockDownloaderConfig, ) { + // TODO: find good values for these size limits let (batch_tx, batch_rx) = mpsc::channel(1); let stop_current_block_downloader = Arc::new(Notify::new()); - let (command_tx, command_rx) = mpsc::channel(1); + let (command_tx, command_rx) = mpsc::channel(3); COMMAND_TX.set(command_tx).unwrap(); @@ -60,7 +61,7 @@ pub async fn init_blockchain_manger( ChainService(blockchain_read_handle.clone()), clearnet_interface.clone(), batch_tx, - stop_current_block_downloader.clone(), + Arc::clone(&stop_current_block_downloader), block_downloader_config, )); diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs index 800144f05..a8d847335 100644 --- a/binaries/cuprated/src/blockchain/manager/commands.rs +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -1,3 +1,4 @@ +//! This module contains the commands for th blockchain manager. use std::collections::HashMap; use monero_serai::block::Block; @@ -5,10 +6,15 @@ use tokio::sync::oneshot; use cuprate_types::TransactionVerificationData; +/// The blockchain manager commands. pub enum BlockchainManagerCommand { + /// Attempt to add a new block to the blockchain. AddBlock { + /// The [`Block`] to add. block: Block, + /// All the transactions defined in [`Block::transactions`]. prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, + /// The channel to send the response down. response_tx: oneshot::Sender>, }, } diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 4406421fb..14073062f 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,3 +1,4 @@ +//! The blockchain manger handler functions. use std::{collections::HashMap, sync::Arc}; use bytes::Bytes; @@ -67,7 +68,7 @@ impl super::BlockchainManager { /// Otherwise, this function will validate and add the block to the main chain. /// /// On success returns a [`bool`] indicating if the block was added to the main chain ([`true`]) - /// of an alt-chain ([`false`]). + /// or an alt-chain ([`false`]). pub async fn handle_incoming_block( &mut self, block: Block, @@ -373,7 +374,7 @@ impl super::BlockchainManager { /// /// This function assumes the first [`AltBlockInformation`] is the next block in the blockchain /// for the blockchain database and the context cache, or in other words that the blockchain database - /// and context cache has had the top blocks popped to where the alt-chain meets the main-chain. + /// and context cache have already had the top blocks popped to where the alt-chain meets the main-chain. /// /// # Errors /// diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index c772b1282..8c58c54eb 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -1,3 +1,4 @@ +// FIXME: This whole module is not great and should be rewritten when the PeerSet is made. use std::{pin::pin, sync::Arc, time::Duration}; use futures::StreamExt; @@ -16,7 +17,6 @@ use cuprate_p2p::{ }; use cuprate_p2p_core::ClearNet; -// FIXME: This whole module is not great and should be rewritten when the PeerSet is made. const CHECK_SYNC_FREQUENCY: Duration = Duration::from_secs(30); /// An error returned from the [`syncer`]. @@ -28,6 +28,11 @@ pub enum SyncerError { ServiceError(#[from] tower::BoxError), } +/// The syncer tasks that makes sure we are fully synchronised with our connected peers. +#[expect( + clippy::significant_drop_tightening, + reason = "Client pool which will be removed" +)] #[instrument(level = "debug", skip_all)] pub async fn syncer( mut context_svc: C, @@ -89,7 +94,7 @@ where loop { tokio::select! { - _ = stop_current_block_downloader.notified() => { + () = stop_current_block_downloader.notified() => { tracing::info!("Stopping block downloader"); break; } @@ -104,6 +109,7 @@ where } } +/// Checks if we should update the given [`BlockChainContext`] and updates it if needed. async fn check_update_blockchain_context( context_svc: C, old_context: &mut BlockChainContext, diff --git a/binaries/cuprated/src/config.rs b/binaries/cuprated/src/config.rs index c71c40c87..d613c1fcc 100644 --- a/binaries/cuprated/src/config.rs +++ b/binaries/cuprated/src/config.rs @@ -1,62 +1 @@ //! cuprated config -use std::time::Duration; - -use cuprate_blockchain::config::{ - Config as BlockchainConfig, ConfigBuilder as BlockchainConfigBuilder, -}; -use cuprate_consensus::ContextConfig; -use cuprate_p2p::{block_downloader::BlockDownloaderConfig, AddressBookConfig, P2PConfig}; -use cuprate_p2p_core::{ClearNet, Network}; - -pub fn config() -> CupratedConfig { - // TODO: read config options from the conf files & cli args. - - CupratedConfig {} -} - -pub struct CupratedConfig { - // TODO: expose config options we want to allow changing. -} - -impl CupratedConfig { - pub fn blockchain_config(&self) -> BlockchainConfig { - BlockchainConfigBuilder::new().fast().build() - } - - pub fn clearnet_config(&self) -> P2PConfig { - P2PConfig { - network: Network::Mainnet, - outbound_connections: 64, - extra_outbound_connections: 0, - max_inbound_connections: 0, - gray_peers_percent: 0.7, - server_config: None, - p2p_port: 0, - rpc_port: 0, - address_book_config: AddressBookConfig { - max_white_list_length: 1000, - max_gray_list_length: 5000, - peer_store_file: "p2p_state.bin".into(), - peer_save_period: Duration::from_secs(60), - }, - } - } - - pub fn block_downloader_config(&self) -> BlockDownloaderConfig { - BlockDownloaderConfig { - buffer_size: 50_000_000, - in_progress_queue_size: 50_000_000, - check_client_pool_interval: Duration::from_secs(45), - target_batch_size: 10_000_000, - initial_batch_size: 1, - } - } - - pub fn network(&self) -> Network { - Network::Mainnet - } - - pub fn context_config(&self) -> ContextConfig { - ContextConfig::main_net() - } -} diff --git a/binaries/cuprated/src/p2p.rs b/binaries/cuprated/src/p2p.rs index 7715be7c3..f55d41db1 100644 --- a/binaries/cuprated/src/p2p.rs +++ b/binaries/cuprated/src/p2p.rs @@ -2,5 +2,4 @@ //! //! Will handle initiating the P2P and contains a protocol request handler. -pub mod core_sync_svc; pub mod request_handler; diff --git a/binaries/cuprated/src/p2p/core_sync_svc.rs b/binaries/cuprated/src/p2p/core_sync_svc.rs deleted file mode 100644 index 34c91e477..000000000 --- a/binaries/cuprated/src/p2p/core_sync_svc.rs +++ /dev/null @@ -1,51 +0,0 @@ -use cuprate_blockchain::cuprate_database::RuntimeError; -use cuprate_blockchain::service::BlockchainReadHandle; -use cuprate_consensus::{ - BlockChainContextRequest, BlockChainContextResponse, BlockChainContextService, -}; -use cuprate_p2p_core::services::{CoreSyncDataRequest, CoreSyncDataResponse}; -use cuprate_p2p_core::CoreSyncData; -use cuprate_types::blockchain::BlockchainReadRequest; -use futures::future::{BoxFuture, MapErr, MapOk}; -use futures::{FutureExt, TryFutureExt}; -use std::task::{Context, Poll}; -use tower::Service; - -#[derive(Clone)] -pub struct CoreSyncService(pub BlockChainContextService); - -impl Service for CoreSyncService { - type Response = CoreSyncDataResponse; - type Error = tower::BoxError; - type Future = MapOk< - >::Future, - fn(BlockChainContextResponse) -> CoreSyncDataResponse, - >; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.0.poll_ready(cx) - } - - fn call(&mut self, _: CoreSyncDataRequest) -> Self::Future { - self.0 - .call(BlockChainContextRequest::GetContext) - .map_ok(|res| { - let BlockChainContextResponse::Context(ctx) = res else { - panic!("blockchain context service returned wrong response."); - }; - - let raw_ctx = ctx.unchecked_blockchain_context(); - - // TODO: the hardfork here should be the version of the top block not the current HF, - // on HF boundaries these will be different. - CoreSyncDataResponse(CoreSyncData::new( - raw_ctx.cumulative_difficulty, - // TODO: - raw_ctx.chain_height as u64, - 0, - raw_ctx.top_hash, - raw_ctx.current_hf.as_u8(), - )) - }) - } -} diff --git a/binaries/cuprated/src/p2p/request_handler.rs b/binaries/cuprated/src/p2p/request_handler.rs index 419c3b427..8b1378917 100644 --- a/binaries/cuprated/src/p2p/request_handler.rs +++ b/binaries/cuprated/src/p2p/request_handler.rs @@ -1,205 +1 @@ -use bytes::Bytes; -use cuprate_p2p_core::{NetworkZone, ProtocolRequest, ProtocolResponse}; -use futures::future::BoxFuture; -use futures::FutureExt; -use monero_serai::block::Block; -use monero_serai::transaction::Transaction; -use rayon::prelude::*; -use std::task::{Context, Poll}; -use tower::{Service, ServiceExt}; -use tracing::trace; -use crate::blockchain::{handle_incoming_block, IncomingBlockError}; -use cuprate_blockchain::service::BlockchainReadHandle; -use cuprate_consensus::transactions::new_tx_verification_data; -use cuprate_fixed_bytes::ByteArray; -use cuprate_helper::asynch::rayon_spawn_async; -use cuprate_helper::cast::usize_to_u64; -use cuprate_helper::map::split_u128_into_low_high_bits; -use cuprate_p2p::constants::MAX_BLOCK_BATCH_LEN; -use cuprate_p2p_core::client::PeerInformation; -use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; -use cuprate_types::BlockCompleteEntry; -use cuprate_wire::protocol::{ - ChainRequest, ChainResponse, FluffyMissingTransactionsRequest, GetObjectsRequest, - GetObjectsResponse, NewFluffyBlock, -}; - -#[derive(Clone)] -pub struct P2pProtocolRequestHandlerMaker { - pub blockchain_read_handle: BlockchainReadHandle, -} - -impl Service> for P2pProtocolRequestHandlerMaker { - type Response = P2pProtocolRequestHandler; - type Error = tower::BoxError; - type Future = BoxFuture<'static, Result>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, peer_information: PeerInformation) -> Self::Future { - // TODO: check peer info. - - let blockchain_read_handle = self.blockchain_read_handle.clone(); - - async { - Ok(P2pProtocolRequestHandler { - peer_information, - blockchain_read_handle, - }) - } - .boxed() - } -} - -#[derive(Clone)] -pub struct P2pProtocolRequestHandler { - peer_information: PeerInformation, - blockchain_read_handle: BlockchainReadHandle, -} - -impl Service for P2pProtocolRequestHandler { - type Response = ProtocolResponse; - type Error = tower::BoxError; - type Future = BoxFuture<'static, Result>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: ProtocolRequest) -> Self::Future { - async { Ok(ProtocolResponse::NA) }.boxed() - } -} - -async fn get_objects( - blockchain_read_handle: BlockchainReadHandle, - req: GetObjectsRequest, -) -> Result { - if req.blocks.is_empty() { - Err("No blocks requested in a GetObjectsRequest")?; - } - - if req.blocks.len() > MAX_BLOCK_BATCH_LEN { - Err("Too many blocks requested in a GetObjectsRequest")?; - } - - let block_ids: Vec<[u8; 32]> = (&req.blocks).into(); - // de-allocate the backing [`Bytes`] - drop(req); - - Ok(ProtocolResponse::NA) - /* - - let res = blockchain_read_handle - .oneshot(BlockchainReadRequest::BlockCompleteEntries(block_ids)) - .await?; - - let BlockchainResponse::BlockCompleteEntries { - blocks, - missed_ids, - current_blockchain_height, - } = res - else { - panic!("Blockchain service returned wrong response!"); - }; - - Ok(ProtocolResponse::GetObjects(GetObjectsResponse { - blocks, - missed_ids: missed_ids.into(), - current_blockchain_height: usize_to_u64(current_blockchain_height), - })) - - */ -} - -async fn get_chain( - blockchain_read_handle: BlockchainReadHandle, - req: ChainRequest, -) -> Result { - if req.block_ids.is_empty() { - Err("No block hashes sent in a `ChainRequest`")?; - } - - Ok(ProtocolResponse::NA) - - /* - if req.block_ids.len() > MAX_BLOCKCHAIN_SUPPLEMENT_LEN { - Err("Too many block hashes in a `ChainRequest`")?; - } - - let block_ids: Vec<[u8; 32]> = (&req.block_ids).into(); - // de-allocate the backing [`Bytes`] - drop(req); - - let res = blockchain_read_handle - .oneshot(BlockchainReadRequest::NextMissingChainEntry(block_ids)) - .await?; - - let BlockchainResponse::NextMissingChainEntry { - next_entry, - first_missing_block, - start_height, - chain_height, - cumulative_difficulty, - } = res - else { - panic!("Blockchain service returned wrong response!"); - }; - - let (cumulative_difficulty_low64, cumulative_difficulty_top64) = - split_u128_into_low_high_bits(cumulative_difficulty); - - Ok(ProtocolResponse::GetChain(ChainResponse { - start_height: usize_to_u64(start_height), - total_height: usize_to_u64(chain_height), - cumulative_difficulty_low64, - cumulative_difficulty_top64, - m_block_ids: next_entry.into(), - m_block_weights: vec![], - first_block: first_missing_block.map_or(Bytes::new(), Bytes::from), - })) - - */ -} - -async fn new_fluffy_block( - mut blockchain_read_handle: BlockchainReadHandle, - incoming_block: NewFluffyBlock, -) -> Result { - let peer_blockchain_height = incoming_block.current_blockchain_height; - - let (block, txs) = rayon_spawn_async(move || { - let block = Block::read(&mut incoming_block.b.block.as_ref())?; - let txs = incoming_block - .b - .txs - .take_normal() - .expect("TODO") - .into_par_iter() - .map(|tx| { - let tx = Transaction::read(&mut tx.as_ref())?; - Ok(tx) - }) - .collect::>()?; - - Ok::<_, tower::BoxError>((block, txs)) - }) - .await?; - - let res = handle_incoming_block(block, txs, &mut blockchain_read_handle).await; - - match res { - Err(IncomingBlockError::UnknownTransactions(block_hash, tx_indexes)) => Ok( - ProtocolResponse::FluffyMissingTxs(FluffyMissingTransactionsRequest { - block_hash: ByteArray::from(block_hash), - current_blockchain_height: peer_blockchain_height, - missing_tx_indices: tx_indexes, - }), - ), - Err(IncomingBlockError::InvalidBlock(e)) => Err(e)?, - Err(IncomingBlockError::Orphan) | Ok(_) => Ok(ProtocolResponse::NA), - } -} diff --git a/binaries/cuprated/src/rpc/request_handler.rs b/binaries/cuprated/src/rpc/request_handler.rs deleted file mode 100644 index 8b1378917..000000000 --- a/binaries/cuprated/src/rpc/request_handler.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/consensus/src/block.rs b/consensus/src/block.rs index 53eb14694..ada46aa42 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -181,6 +181,7 @@ impl PreparedBlock { }) } + /// Creates a new [`PreparedBlock`] from an [`AltBlockInformation`]. pub fn new_alt_block(block: AltBlockInformation) -> Result { Ok(Self { block_blob: block.block_blob, diff --git a/helper/src/tx_utils.rs b/helper/src/tx_utils.rs deleted file mode 100644 index aeccf32b2..000000000 --- a/helper/src/tx_utils.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Utils for working with [`Transaction`] - -use monero_serai::transaction::{Input, Transaction}; - -/// Calculates the fee of the [`Transaction`]. -/// -/// # Panics -/// This will panic if the inputs overflow or the transaction outputs too much, so should only -/// be used on known to be valid txs. -pub fn tx_fee(tx: &Transaction) -> u64 { - let mut fee = 0_u64; - - match &tx { - Transaction::V1 { prefix, .. } => { - for input in &prefix.inputs { - match input { - Input::Gen(_) => return 0, - Input::ToKey { amount, .. } => { - fee = fee.checked_add(amount.unwrap_or(0)).unwrap(); - } - } - } - - for output in &prefix.outputs { - fee.checked_sub(output.amount.unwrap_or(0)).unwrap(); - } - } - Transaction::V2 { proofs, .. } => { - fee = proofs.as_ref().unwrap().base.fee; - } - }; - - fee -} diff --git a/p2p/p2p-core/src/protocol.rs b/p2p/p2p-core/src/protocol.rs index cdb0c2f37..7d8d431b8 100644 --- a/p2p/p2p-core/src/protocol.rs +++ b/p2p/p2p-core/src/protocol.rs @@ -116,7 +116,6 @@ pub enum ProtocolResponse { GetChain(ChainResponse), NewFluffyBlock(NewFluffyBlock), NewTransactions(NewTransactions), - FluffyMissingTxs(FluffyMissingTransactionsRequest), NA, } @@ -140,7 +139,6 @@ impl PeerResponse { ProtocolResponse::GetChain(_) => MessageID::GetChain, ProtocolResponse::NewFluffyBlock(_) => MessageID::NewBlock, ProtocolResponse::NewTransactions(_) => MessageID::NewFluffyBlock, - ProtocolResponse::FluffyMissingTxs(_) => MessageID::FluffyMissingTxs, ProtocolResponse::NA => return None, }, diff --git a/p2p/p2p-core/src/protocol/try_from.rs b/p2p/p2p-core/src/protocol/try_from.rs index b3c1ae3d6..d3a7260fd 100644 --- a/p2p/p2p-core/src/protocol/try_from.rs +++ b/p2p/p2p-core/src/protocol/try_from.rs @@ -71,7 +71,6 @@ impl TryFrom for ProtocolMessage { ProtocolResponse::NewFluffyBlock(val) => Self::NewFluffyBlock(val), ProtocolResponse::GetChain(val) => Self::ChainEntryResponse(val), ProtocolResponse::GetObjects(val) => Self::GetObjectsResponse(val), - ProtocolResponse::FluffyMissingTxs(val) => Self::FluffyMissingTransactionsRequest(val), ProtocolResponse::NA => return Err(MessageConversionError), }) } diff --git a/p2p/p2p/src/client_pool.rs b/p2p/p2p/src/client_pool.rs index 99981f0ce..fc97fc1b6 100644 --- a/p2p/p2p/src/client_pool.rs +++ b/p2p/p2p/src/client_pool.rs @@ -154,6 +154,8 @@ impl ClientPool { self.borrow_clients(&peers).collect() } + /// Checks all clients in the pool checking if any claim a higher cumulative difficulty than the + /// amount specified. pub fn contains_client_with_more_cumulative_difficulty( &self, cumulative_difficulty: u128, diff --git a/p2p/p2p/src/constants.rs b/p2p/p2p/src/constants.rs index f79bcbf00..f70d64c92 100644 --- a/p2p/p2p/src/constants.rs +++ b/p2p/p2p/src/constants.rs @@ -16,11 +16,10 @@ pub(crate) const MAX_SEED_CONNECTIONS: usize = 3; pub(crate) const OUTBOUND_CONNECTION_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(5); /// The durations of a short ban. -#[cfg_attr(not(test), expect(dead_code))] -pub(crate) const SHORT_BAN: Duration = Duration::from_secs(60 * 10); +pub const SHORT_BAN: Duration = Duration::from_secs(60 * 10); /// The durations of a medium ban. -pub(crate) const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); +pub const MEDIUM_BAN: Duration = Duration::from_secs(60 * 60 * 24); /// The durations of a long ban. pub const LONG_BAN: Duration = Duration::from_secs(60 * 60 * 24 * 7); @@ -53,7 +52,7 @@ pub(crate) const INITIAL_CHAIN_REQUESTS_TO_SEND: usize = 3; /// The enforced maximum amount of blocks to request in a batch. /// /// Requesting more than this will cause the peer to disconnect and potentially lead to bans. -pub const MAX_BLOCK_BATCH_LEN: usize = 100; +pub(crate) const MAX_BLOCK_BATCH_LEN: usize = 100; /// The timeout that the block downloader will use for requests. pub(crate) const BLOCK_DOWNLOADER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/p2p/p2p/src/lib.rs b/p2p/p2p/src/lib.rs index ad082fc6f..b3577a77d 100644 --- a/p2p/p2p/src/lib.rs +++ b/p2p/p2p/src/lib.rs @@ -172,7 +172,7 @@ impl NetworkInterface { self.address_book.clone() } - /// TODO + /// Borrows the `ClientPool`, for access to connected peers. pub const fn client_pool(&self) -> &Arc> { &self.pool } From 783f4260b4fc195544cb4110532da5a8c16327d4 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 5 Oct 2024 16:39:33 +0100 Subject: [PATCH 41/46] improve interface globals --- binaries/cuprated/src/blockchain/interface.rs | 39 ++++++++----------- binaries/cuprated/src/signals.rs | 3 ++ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index 189828e9c..84c81e06f 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -2,13 +2,13 @@ //! //! This module contains all the functions to mutate the blockchain's state in any way, through the //! blockchain manger. +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; +use std::sync::LazyLock; use std::{ collections::{HashMap, HashSet}, sync::{Mutex, OnceLock}, }; - -use monero_serai::{block::Block, transaction::Transaction}; -use rayon::prelude::*; use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; @@ -25,14 +25,9 @@ use crate::{ }; /// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manger. -pub static COMMAND_TX: OnceLock> = OnceLock::new(); - -/// A [`HashSet`] of block hashes that the blockchain manager is currently handling. /// -/// This is used over something like a dashmap as we expect a lot of collisions in a short amount of -/// time for new blocks so we would lose the benefit of sharded locks. A dashmap is made up of `RwLocks` -/// which are also more expensive than `Mutex`s. -pub static BLOCKS_BEING_HANDLED: OnceLock>> = OnceLock::new(); +/// This channel is initialized in [`init_blockchain_manger`](super::manager::init_blockchain_manger). +pub(super) static COMMAND_TX: OnceLock> = OnceLock::new(); /// An error that can be returned from [`handle_incoming_block`]. #[derive(Debug, thiserror::Error)] @@ -68,6 +63,16 @@ pub async fn handle_incoming_block( given_txs: Vec, blockchain_read_handle: &mut BlockchainReadHandle, ) -> Result { + /// A [`HashSet`] of block hashes that the blockchain manager is currently handling. + /// + /// This lock prevents sending the same block to the blockchain manager from multiple connections + /// before one of them actually gets added to the chain, allowing peers to do other things. + /// + /// This is used over something like a dashmap as we expect a lot of collisions in a short amount of + /// time for new blocks, so we would lose the benefit of sharded locks. A dashmap is made up of `RwLocks` + /// which are also more expensive than `Mutex`s. + static BLOCKS_BEING_HANDLED: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); // FIXME: we should look in the tx-pool for txs when that is ready. if !block_exists(block.header.previous, blockchain_read_handle) @@ -111,12 +116,7 @@ pub async fn handle_incoming_block( }; // Add the blocks hash to the blocks being handled. - if !BLOCKS_BEING_HANDLED - .get_or_init(|| Mutex::new(HashSet::new())) - .lock() - .unwrap() - .insert(block_hash) - { + if !BLOCKS_BEING_HANDLED.lock().unwrap().insert(block_hash) { // If another place is already adding this block then we can stop. return Ok(false); } @@ -140,12 +140,7 @@ pub async fn handle_incoming_block( .map_err(IncomingBlockError::InvalidBlock); // Remove the block hash from the blocks being handled. - BLOCKS_BEING_HANDLED - .get() - .unwrap() - .lock() - .unwrap() - .remove(&block_hash); + BLOCKS_BEING_HANDLED.lock().unwrap().remove(&block_hash); res } diff --git a/binaries/cuprated/src/signals.rs b/binaries/cuprated/src/signals.rs index 54467559a..8502679cf 100644 --- a/binaries/cuprated/src/signals.rs +++ b/binaries/cuprated/src/signals.rs @@ -6,4 +6,7 @@ use tokio::sync::RwLock; /// /// A [`RwLock`] where a write lock is taken during a reorg and a read lock can be taken /// for any operation which must complete without a reorg happening. +/// +/// Currently, the only operation that needs to take a read lock is adding txs to the tx-pool, +/// this can potentially be removed in the future, see: TODO pub static REORG_LOCK: RwLock<()> = RwLock::const_new(()); From 375a1e1826f651019051e61474a7a9ea79d239ae Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 5 Oct 2024 16:58:15 +0100 Subject: [PATCH 42/46] manger -> manager --- binaries/cuprated/src/blockchain/interface.rs | 20 +++++++++---------- binaries/cuprated/src/blockchain/manager.rs | 8 ++++---- .../src/blockchain/manager/handler.rs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index 84c81e06f..bdf1bf334 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -1,16 +1,16 @@ -//! The blockchain manger interface. +//! The blockchain manager interface. //! //! This module contains all the functions to mutate the blockchain's state in any way, through the -//! blockchain manger. -use monero_serai::{block::Block, transaction::Transaction}; -use rayon::prelude::*; -use std::sync::LazyLock; +//! blockchain manager. use std::{ collections::{HashMap, HashSet}, - sync::{Mutex, OnceLock}, + sync::{Mutex, OnceLock, LazyLock}, }; + use tokio::sync::{mpsc, oneshot}; use tower::{Service, ServiceExt}; +use monero_serai::{block::Block, transaction::Transaction}; +use rayon::prelude::*; use cuprate_blockchain::service::BlockchainReadHandle; use cuprate_consensus::transactions::new_tx_verification_data; @@ -24,9 +24,9 @@ use crate::{ blockchain::manager::BlockchainManagerCommand, constants::PANIC_CRITICAL_SERVICE_ERROR, }; -/// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manger. +/// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manager. /// -/// This channel is initialized in [`init_blockchain_manger`](super::manager::init_blockchain_manger). +/// This channel is initialized in [`init_blockchain_manager`](super::manager::init_blockchain_manager). pub(super) static COMMAND_TX: OnceLock> = OnceLock::new(); /// An error that can be returned from [`handle_incoming_block`]. @@ -50,7 +50,7 @@ pub enum IncomingBlockError { /// This returns a [`bool`] indicating if the block was added to the main-chain ([`true`]) or an alt-chain /// ([`false`]). /// -/// If we already knew about this block or the blockchain manger is not setup yet `Ok(false)` is returned. +/// If we already knew about this block or the blockchain manager is not setup yet `Ok(false)` is returned. /// /// # Errors /// @@ -110,7 +110,7 @@ pub async fn handle_incoming_block( .map_err(IncomingBlockError::InvalidBlock)?; let Some(incoming_block_tx) = COMMAND_TX.get() else { - // We could still be starting up the blockchain manger, so just return this as there is nothing + // We could still be starting up the blockchain manager, so just return this as there is nothing // else we can do. return Ok(false); }; diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 8725aecaa..eae925203 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -37,11 +37,11 @@ mod handler; pub use commands::BlockchainManagerCommand; -/// Initialize the blockchain manger. +/// Initialize the blockchain manager. /// /// This function sets up the [`BlockchainManager`] and the [`syncer`] so that the functions in [`interface`](super::interface) /// can be called. -pub async fn init_blockchain_manger( +pub async fn init_blockchain_manager( clearnet_interface: NetworkInterface, blockchain_write_handle: BlockchainWriteHandle, blockchain_read_handle: BlockchainReadHandle, @@ -76,7 +76,7 @@ pub async fn init_blockchain_manger( panic!("Blockchain context service returned wrong response!"); }; - let manger = BlockchainManager { + let manager = BlockchainManager { blockchain_write_handle, blockchain_read_handle, blockchain_context_service, @@ -86,7 +86,7 @@ pub async fn init_blockchain_manger( broadcast_svc: clearnet_interface.broadcast_svc(), }; - tokio::spawn(manger.run(batch_rx, command_rx)); + tokio::spawn(manager.run(batch_rx, command_rx)); } /// The blockchain manager. diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 14073062f..23e8295b7 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,4 +1,4 @@ -//! The blockchain manger handler functions. +//! The blockchain manager handler functions. use std::{collections::HashMap, sync::Arc}; use bytes::Bytes; From f50d92145978a70c6ffa5b6bee9b8df89d2b714d Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 5 Oct 2024 19:49:26 +0100 Subject: [PATCH 43/46] enums instead of bools --- binaries/cuprated/src/blockchain/interface.rs | 23 +++++---- binaries/cuprated/src/blockchain/manager.rs | 2 +- .../src/blockchain/manager/commands.rs | 14 +++++- .../src/blockchain/manager/handler.rs | 50 +++++++++++-------- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index bdf1bf334..879103e02 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -4,13 +4,13 @@ //! blockchain manager. use std::{ collections::{HashMap, HashSet}, - sync::{Mutex, OnceLock, LazyLock}, + sync::{LazyLock, Mutex, OnceLock}, }; -use tokio::sync::{mpsc, oneshot}; -use tower::{Service, ServiceExt}; use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; +use tokio::sync::{mpsc, oneshot}; +use tower::{Service, ServiceExt}; use cuprate_blockchain::service::BlockchainReadHandle; use cuprate_consensus::transactions::new_tx_verification_data; @@ -21,12 +21,14 @@ use cuprate_types::{ }; use crate::{ - blockchain::manager::BlockchainManagerCommand, constants::PANIC_CRITICAL_SERVICE_ERROR, + blockchain::manager::{BlockchainManagerCommand, IncomingBlockOk}, + constants::PANIC_CRITICAL_SERVICE_ERROR, }; /// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manager. /// -/// This channel is initialized in [`init_blockchain_manager`](super::manager::init_blockchain_manager). +/// This channel is initialized in [`init_blockchain_manager`](super::manager::init_blockchain_manager), the functions +/// in this file document what happens if this is not initialized when they are called. pub(super) static COMMAND_TX: OnceLock> = OnceLock::new(); /// An error that can be returned from [`handle_incoming_block`]. @@ -62,7 +64,7 @@ pub async fn handle_incoming_block( block: Block, given_txs: Vec, blockchain_read_handle: &mut BlockchainReadHandle, -) -> Result { +) -> Result { /// A [`HashSet`] of block hashes that the blockchain manager is currently handling. /// /// This lock prevents sending the same block to the blockchain manager from multiple connections @@ -88,7 +90,7 @@ pub async fn handle_incoming_block( .await .expect(PANIC_CRITICAL_SERVICE_ERROR) { - return Ok(false); + return Ok(IncomingBlockOk::AlreadyHave); } // TODO: remove this when we have a working tx-pool. @@ -110,15 +112,14 @@ pub async fn handle_incoming_block( .map_err(IncomingBlockError::InvalidBlock)?; let Some(incoming_block_tx) = COMMAND_TX.get() else { - // We could still be starting up the blockchain manager, so just return this as there is nothing - // else we can do. - return Ok(false); + // We could still be starting up the blockchain manager. + return Ok(IncomingBlockOk::NotReady); }; // Add the blocks hash to the blocks being handled. if !BLOCKS_BEING_HANDLED.lock().unwrap().insert(block_hash) { // If another place is already adding this block then we can stop. - return Ok(false); + return Ok(IncomingBlockOk::AlreadyHave); } // From this point on we MUST not early return without removing the block hash from `BLOCKS_BEING_HANDLED`. diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index eae925203..6019592f9 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -35,7 +35,7 @@ use crate::{ mod commands; mod handler; -pub use commands::BlockchainManagerCommand; +pub use commands::{BlockchainManagerCommand, IncomingBlockOk}; /// Initialize the blockchain manager. /// diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs index a8d847335..f5890a833 100644 --- a/binaries/cuprated/src/blockchain/manager/commands.rs +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -15,6 +15,18 @@ pub enum BlockchainManagerCommand { /// All the transactions defined in [`Block::transactions`]. prepped_txs: HashMap<[u8; 32], TransactionVerificationData>, /// The channel to send the response down. - response_tx: oneshot::Sender>, + response_tx: oneshot::Sender>, }, } + +/// The [`Ok`] response for an incoming block. +pub enum IncomingBlockOk { + /// The block was added to the main-chain. + AddedToMainChain, + /// The blockchain manager is not ready yet. + NotReady, + /// The block was added to an alt-chain. + AddedToAltChain, + /// We already have the block. + AlreadyHave, +} diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index 23e8295b7..d37811bcc 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -1,10 +1,10 @@ //! The blockchain manager handler functions. -use std::{collections::HashMap, sync::Arc}; - use bytes::Bytes; use futures::{TryFutureExt, TryStreamExt}; use monero_serai::{block::Block, transaction::Transaction}; use rayon::prelude::*; +use std::ops::ControlFlow; +use std::{collections::HashMap, sync::Arc}; use tower::{Service, ServiceExt}; use tracing::info; @@ -22,6 +22,7 @@ use cuprate_types::{ AltBlockInformation, HardFork, TransactionVerificationData, VerifiedBlockInformation, }; +use crate::blockchain::manager::commands::IncomingBlockOk; use crate::{ blockchain::{ manager::commands::BlockchainManagerCommand, types::ConsensusBlockchainReadHandle, @@ -73,11 +74,10 @@ impl super::BlockchainManager { &mut self, block: Block, prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, - ) -> Result { + ) -> Result { if block.header.previous != self.cached_blockchain_context.top_hash { self.handle_incoming_alt_block(block, prepared_txs).await?; - - return Ok(false); + return Ok(IncomingBlockOk::AddedToAltChain); } let VerifyBlockResponse::MainChain(verified_block) = self @@ -91,7 +91,7 @@ impl super::BlockchainManager { }) .await? else { - panic!("Incorrect response!"); + unreachable!(); }; let block_blob = Bytes::copy_from_slice(&verified_block.block_blob); @@ -100,7 +100,7 @@ impl super::BlockchainManager { self.broadcast_block(block_blob, self.cached_blockchain_context.chain_height) .await; - Ok(true) + Ok(IncomingBlockOk::AddedToMainChain) } /// Handle an incoming [`BlockBatch`]. @@ -160,7 +160,7 @@ impl super::BlockchainManager { self.stop_current_block_downloader.notify_one(); return; } - _ => panic!("Incorrect response!"), + _ => unreachable!(), }; for (block, txs) in prepped_blocks { @@ -179,7 +179,7 @@ impl super::BlockchainManager { self.stop_current_block_downloader.notify_one(); return; } - _ => panic!("Incorrect response!"), + _ => unreachable!(), }; self.add_valid_block_to_main_chain(verified_block).await; @@ -226,16 +226,14 @@ impl super::BlockchainManager { self.stop_current_block_downloader.notify_one(); return; } - // the chain was reorged - Ok(true) => { + Ok(AddAltBlock::Reorged) => { // Collect the remaining blocks and add them to the main chain instead. batch.blocks = blocks.collect(); self.handle_incoming_block_batch_main_chain(batch).await; - return; } // continue adding alt blocks. - Ok(false) => (), + Ok(AddAltBlock::Cached) => (), } } } @@ -258,11 +256,11 @@ impl super::BlockchainManager { /// /// This function will panic if any internal service returns an unexpected error that we cannot /// recover from. - pub async fn handle_incoming_alt_block( + async fn handle_incoming_alt_block( &mut self, block: Block, prepared_txs: HashMap<[u8; 32], TransactionVerificationData>, - ) -> Result { + ) -> Result { let VerifyBlockResponse::AltChain(alt_block_info) = self .block_verifier_service .ready() @@ -274,7 +272,7 @@ impl super::BlockchainManager { }) .await? else { - panic!("Incorrect response!"); + unreachable!(); }; // TODO: check in consensus crate if alt block with this hash already exists. @@ -284,7 +282,7 @@ impl super::BlockchainManager { > self.cached_blockchain_context.cumulative_difficulty { self.try_do_reorg(alt_block_info).await?; - return Ok(true); + return Ok(AddAltBlock::Reorged); } self.blockchain_write_handle @@ -294,7 +292,7 @@ impl super::BlockchainManager { .call(BlockchainWriteRequest::WriteAltBlock(alt_block_info)) .await?; - Ok(false) + Ok(AddAltBlock::Cached) } /// Attempt a re-org with the given top block of the alt-chain. @@ -328,7 +326,7 @@ impl super::BlockchainManager { )) .await? else { - panic!("Incorrect response!"); + unreachable!(); }; alt_blocks.push(top_alt_block); @@ -347,7 +345,7 @@ impl super::BlockchainManager { .await .expect(PANIC_CRITICAL_SERVICE_ERROR) else { - panic!("Incorrect response!"); + unreachable!(); }; self.blockchain_context_service @@ -409,7 +407,7 @@ impl super::BlockchainManager { }) .await? else { - panic!("Incorrect response!"); + unreachable!(); }; self.add_valid_block_to_main_chain(verified_block).await; @@ -465,9 +463,17 @@ impl super::BlockchainManager { .await .expect(PANIC_CRITICAL_SERVICE_ERROR) else { - panic!("Incorrect response!"); + unreachable!(); }; self.cached_blockchain_context = blockchain_context.unchecked_blockchain_context().clone(); } } + +/// The result from successfully adding an alt-block. +enum AddAltBlock { + /// The alt-block was cached. + Cached, + /// The chain was reorged. + Reorged, +} From 27a8acdb04064fa70f476ed3fae83b46ac2eb418 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 5 Oct 2024 19:57:30 +0100 Subject: [PATCH 44/46] move chain service to separate file --- binaries/cuprated/src/blockchain.rs | 1 + .../cuprated/src/blockchain/chain_service.rs | 72 +++++++++++++++++++ binaries/cuprated/src/blockchain/manager.rs | 2 +- binaries/cuprated/src/blockchain/types.rs | 64 ----------------- 4 files changed, 74 insertions(+), 65 deletions(-) create mode 100644 binaries/cuprated/src/blockchain/chain_service.rs diff --git a/binaries/cuprated/src/blockchain.rs b/binaries/cuprated/src/blockchain.rs index 1a9c0b7ce..a06f3fa73 100644 --- a/binaries/cuprated/src/blockchain.rs +++ b/binaries/cuprated/src/blockchain.rs @@ -19,6 +19,7 @@ use cuprate_types::{ use crate::constants::PANIC_CRITICAL_SERVICE_ERROR; +mod chain_service; pub mod interface; mod manager; mod syncer; diff --git a/binaries/cuprated/src/blockchain/chain_service.rs b/binaries/cuprated/src/blockchain/chain_service.rs new file mode 100644 index 000000000..eeaf4a06e --- /dev/null +++ b/binaries/cuprated/src/blockchain/chain_service.rs @@ -0,0 +1,72 @@ +use std::task::{Context, Poll}; + +use futures::{future::BoxFuture, FutureExt, TryFutureExt}; +use tower::Service; + +use cuprate_blockchain::service::BlockchainReadHandle; +use cuprate_p2p::block_downloader::{ChainSvcRequest, ChainSvcResponse}; +use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse}; + +/// That service that allows retrieving the chain state to give to the P2P crates, so we can figure out +/// what blocks we need. +/// +/// This has a more minimal interface than [`BlockchainReadRequest`] to make using the p2p crates easier. +#[derive(Clone)] +pub struct ChainService(pub BlockchainReadHandle); + +impl Service for ChainService { + type Response = ChainSvcResponse; + type Error = tower::BoxError; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.0.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: ChainSvcRequest) -> Self::Future { + let map_res = |res: BlockchainResponse| match res { + BlockchainResponse::CompactChainHistory { + block_ids, + cumulative_difficulty, + } => ChainSvcResponse::CompactHistory { + block_ids, + cumulative_difficulty, + }, + BlockchainResponse::FindFirstUnknown(res) => ChainSvcResponse::FindFirstUnknown(res), + _ => panic!("Blockchain returned wrong response"), + }; + + match req { + ChainSvcRequest::CompactHistory => self + .0 + .call(BlockchainReadRequest::CompactChainHistory) + .map_ok(map_res) + .map_err(Into::into) + .boxed(), + ChainSvcRequest::FindFirstUnknown(req) => self + .0 + .call(BlockchainReadRequest::FindFirstUnknown(req)) + .map_ok(map_res) + .map_err(Into::into) + .boxed(), + ChainSvcRequest::CumulativeDifficulty => self + .0 + .call(BlockchainReadRequest::CompactChainHistory) + .map_ok(|res| { + // TODO create a custom request instead of hijacking this one. + // TODO: use the context cache. + let BlockchainResponse::CompactChainHistory { + cumulative_difficulty, + .. + } = res + else { + panic!("Blockchain returned wrong response"); + }; + + ChainSvcResponse::CumulativeDifficulty(cumulative_difficulty) + }) + .map_err(Into::into) + .boxed(), + } + } +} diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index 6019592f9..f6c11fc04 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -24,9 +24,9 @@ use cuprate_types::{ use crate::{ blockchain::{ + chain_service::ChainService, interface::COMMAND_TX, syncer, - types::ChainService, types::{ConcreteBlockVerifierService, ConsensusBlockchainReadHandle}, }, constants::PANIC_CRITICAL_SERVICE_ERROR, diff --git a/binaries/cuprated/src/blockchain/types.rs b/binaries/cuprated/src/blockchain/types.rs index 1bf921ecc..e3ee62b3c 100644 --- a/binaries/cuprated/src/blockchain/types.rs +++ b/binaries/cuprated/src/blockchain/types.rs @@ -22,67 +22,3 @@ pub type ConcreteTxVerifierService = TxVerifierService tower::BoxError>; - -/// That service that allows retrieving the chain state to give to the P2P crates, so we can figure out -/// what blocks we need. -/// -/// This has a more minimal interface than [`BlockchainReadRequest`] to make using the p2p crates easier. -#[derive(Clone)] -pub struct ChainService(pub BlockchainReadHandle); - -impl Service for ChainService { - type Response = ChainSvcResponse; - type Error = tower::BoxError; - type Future = BoxFuture<'static, Result>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.0.poll_ready(cx).map_err(Into::into) - } - - fn call(&mut self, req: ChainSvcRequest) -> Self::Future { - let map_res = |res: BlockchainResponse| match res { - BlockchainResponse::CompactChainHistory { - block_ids, - cumulative_difficulty, - } => ChainSvcResponse::CompactHistory { - block_ids, - cumulative_difficulty, - }, - BlockchainResponse::FindFirstUnknown(res) => ChainSvcResponse::FindFirstUnknown(res), - _ => panic!("Blockchain returned wrong response"), - }; - - match req { - ChainSvcRequest::CompactHistory => self - .0 - .call(BlockchainReadRequest::CompactChainHistory) - .map_ok(map_res) - .map_err(Into::into) - .boxed(), - ChainSvcRequest::FindFirstUnknown(req) => self - .0 - .call(BlockchainReadRequest::FindFirstUnknown(req)) - .map_ok(map_res) - .map_err(Into::into) - .boxed(), - ChainSvcRequest::CumulativeDifficulty => self - .0 - .call(BlockchainReadRequest::CompactChainHistory) - .map_ok(|res| { - // TODO create a custom request instead of hijacking this one. - // TODO: use the context cache. - let BlockchainResponse::CompactChainHistory { - cumulative_difficulty, - .. - } = res - else { - panic!("Blockchain returned wrong response"); - }; - - ChainSvcResponse::CumulativeDifficulty(cumulative_difficulty) - }) - .map_err(Into::into) - .boxed(), - } - } -} From a21e489fdc9b2fd755c32479e4deb05c12fb76d5 Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sat, 5 Oct 2024 21:16:51 +0100 Subject: [PATCH 45/46] more review fixes --- binaries/cuprated/src/blockchain/chain_service.rs | 4 ++-- binaries/cuprated/src/blockchain/interface.rs | 7 ++----- binaries/cuprated/src/blockchain/manager.rs | 2 +- .../cuprated/src/blockchain/manager/commands.rs | 2 +- binaries/cuprated/src/blockchain/manager/handler.rs | 13 +++++++++---- binaries/cuprated/src/blockchain/syncer.rs | 4 ++-- binaries/cuprated/src/constants.rs | 1 + 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/binaries/cuprated/src/blockchain/chain_service.rs b/binaries/cuprated/src/blockchain/chain_service.rs index eeaf4a06e..af862d1d2 100644 --- a/binaries/cuprated/src/blockchain/chain_service.rs +++ b/binaries/cuprated/src/blockchain/chain_service.rs @@ -33,7 +33,7 @@ impl Service for ChainService { cumulative_difficulty, }, BlockchainResponse::FindFirstUnknown(res) => ChainSvcResponse::FindFirstUnknown(res), - _ => panic!("Blockchain returned wrong response"), + _ => unreachable!(), }; match req { @@ -60,7 +60,7 @@ impl Service for ChainService { .. } = res else { - panic!("Blockchain returned wrong response"); + unreachable!() }; ChainSvcResponse::CumulativeDifficulty(cumulative_difficulty) diff --git a/binaries/cuprated/src/blockchain/interface.rs b/binaries/cuprated/src/blockchain/interface.rs index 879103e02..985e60d80 100644 --- a/binaries/cuprated/src/blockchain/interface.rs +++ b/binaries/cuprated/src/blockchain/interface.rs @@ -49,10 +49,7 @@ pub enum IncomingBlockError { /// Try to add a new block to the blockchain. /// -/// This returns a [`bool`] indicating if the block was added to the main-chain ([`true`]) or an alt-chain -/// ([`false`]). -/// -/// If we already knew about this block or the blockchain manager is not setup yet `Ok(false)` is returned. +/// On success returns [`IncomingBlockOk`]. /// /// # Errors /// @@ -157,7 +154,7 @@ async fn block_exists( .call(BlockchainReadRequest::FindBlock(block_hash)) .await? else { - panic!("Invalid blockchain response!"); + unreachable!(); }; Ok(chain.is_some()) diff --git a/binaries/cuprated/src/blockchain/manager.rs b/binaries/cuprated/src/blockchain/manager.rs index f6c11fc04..568ed572d 100644 --- a/binaries/cuprated/src/blockchain/manager.rs +++ b/binaries/cuprated/src/blockchain/manager.rs @@ -73,7 +73,7 @@ pub async fn init_blockchain_manager( .await .expect(PANIC_CRITICAL_SERVICE_ERROR) else { - panic!("Blockchain context service returned wrong response!"); + unreachable!() }; let manager = BlockchainManager { diff --git a/binaries/cuprated/src/blockchain/manager/commands.rs b/binaries/cuprated/src/blockchain/manager/commands.rs index f5890a833..643ed88cb 100644 --- a/binaries/cuprated/src/blockchain/manager/commands.rs +++ b/binaries/cuprated/src/blockchain/manager/commands.rs @@ -1,4 +1,4 @@ -//! This module contains the commands for th blockchain manager. +//! This module contains the commands for the blockchain manager. use std::collections::HashMap; use monero_serai::block::Block; diff --git a/binaries/cuprated/src/blockchain/manager/handler.rs b/binaries/cuprated/src/blockchain/manager/handler.rs index d37811bcc..e9e6c3c36 100644 --- a/binaries/cuprated/src/blockchain/manager/handler.rs +++ b/binaries/cuprated/src/blockchain/manager/handler.rs @@ -33,6 +33,11 @@ use crate::{ impl super::BlockchainManager { /// Handle an incoming command from another part of Cuprate. + /// + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn handle_command(&mut self, command: BlockchainManagerCommand) { match command { BlockchainManagerCommand::AddBlock { @@ -68,8 +73,10 @@ impl super::BlockchainManager { /// /// Otherwise, this function will validate and add the block to the main chain. /// - /// On success returns a [`bool`] indicating if the block was added to the main chain ([`true`]) - /// or an alt-chain ([`false`]). + /// # Panics + /// + /// This function will panic if any internal service returns an unexpected error that we cannot + /// recover from. pub async fn handle_incoming_block( &mut self, block: Block, @@ -244,8 +251,6 @@ impl super::BlockchainManager { /// of the alt chain is higher than the main chain it will attempt a reorg otherwise it will add /// the alt block to the alt block cache. /// - /// This function returns a [`bool`] indicating if the chain was reorganised ([`true`]) or not ([`false`]). - /// /// # Errors /// /// This will return an [`Err`] if: diff --git a/binaries/cuprated/src/blockchain/syncer.rs b/binaries/cuprated/src/blockchain/syncer.rs index 8c58c54eb..7e72c36ca 100644 --- a/binaries/cuprated/src/blockchain/syncer.rs +++ b/binaries/cuprated/src/blockchain/syncer.rs @@ -65,7 +65,7 @@ where .call(BlockChainContextRequest::GetContext) .await? else { - panic!("Blockchain context service returned wrong response!"); + unreachable!(); }; let client_pool = clearnet_interface.client_pool(); @@ -130,7 +130,7 @@ where .oneshot(BlockChainContextRequest::GetContext) .await? else { - panic!("Blockchain context service returned wrong response!"); + unreachable!(); }; *old_context = ctx; diff --git a/binaries/cuprated/src/constants.rs b/binaries/cuprated/src/constants.rs index d4dfc1ad4..2f3c7bb64 100644 --- a/binaries/cuprated/src/constants.rs +++ b/binaries/cuprated/src/constants.rs @@ -14,6 +14,7 @@ pub const VERSION_BUILD: &str = if cfg!(debug_assertions) { formatcp!("{VERSION}-release") }; +/// The panic message used when cuprated encounters a critical service error. pub const PANIC_CRITICAL_SERVICE_ERROR: &str = "A service critical to Cuprate's function returned an unexpected error."; From c87edf2282e45759c7530234cfe020e40856986a Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 6 Oct 2024 18:03:11 +0100 Subject: [PATCH 46/46] add link to issue --- binaries/cuprated/src/signals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binaries/cuprated/src/signals.rs b/binaries/cuprated/src/signals.rs index 8502679cf..42148ca83 100644 --- a/binaries/cuprated/src/signals.rs +++ b/binaries/cuprated/src/signals.rs @@ -8,5 +8,5 @@ use tokio::sync::RwLock; /// for any operation which must complete without a reorg happening. /// /// Currently, the only operation that needs to take a read lock is adding txs to the tx-pool, -/// this can potentially be removed in the future, see: TODO +/// this can potentially be removed in the future, see: pub static REORG_LOCK: RwLock<()> = RwLock::const_new(());