Skip to main content

cuprated/blockchain/
interface.rs

1//! The blockchain manager interface.
2//!
3//! This module contains all the functions to mutate the blockchain's state in any way, through the
4//! blockchain manager.
5use std::{
6    collections::{HashMap, HashSet},
7    sync::{Arc, Mutex},
8};
9
10use monero_oxide::{block::Block, transaction::Transaction};
11use tokio::sync::{mpsc, oneshot};
12use tower::{Service, ServiceExt};
13
14use cuprate_blockchain::service::BlockchainReadHandle;
15use cuprate_consensus::transactions::new_tx_verification_data;
16use cuprate_txpool::service::{
17    interface::{TxpoolReadRequest, TxpoolReadResponse},
18    TxpoolReadHandle,
19};
20use cuprate_types::blockchain::{BlockchainReadRequest, BlockchainResponse};
21
22use crate::{
23    blockchain::manager::{BlockchainManagerCommand, IncomingBlockOk},
24    constants::PANIC_CRITICAL_SERVICE_ERROR,
25};
26
27/// Handle for the blockchain manager.
28///
29/// Created by `init_blockchain_manager`.
30#[derive(Clone)]
31pub struct BlockchainManagerHandle {
32    /// The channel used to send [`BlockchainManagerCommand`]s to the blockchain manager.
33    command_tx: mpsc::Sender<BlockchainManagerCommand>,
34    /// A [`HashSet`] of block hashes that the blockchain manager is currently handling.
35    ///
36    /// This prevents sending the same block to the blockchain manager from multiple connections
37    /// before one of them actually gets added to the chain, allowing peers to do other things.
38    ///
39    /// This is used over something like a dashmap as we expect a lot of collisions in a short amount of
40    /// time for new blocks, so we would lose the benefit of sharded locks. A dashmap is made up of `RwLocks`
41    /// which are also more expensive than `Mutex`s.
42    blocks_being_handled: Arc<Mutex<HashSet<[u8; 32]>>>,
43}
44
45/// An error that can be returned from [`BlockchainManagerHandle::handle_incoming_block`].
46#[derive(Debug, thiserror::Error)]
47pub enum IncomingBlockError {
48    /// Some transactions in the block were unknown.
49    ///
50    /// The inner values are the block hash and the indexes of the missing txs in the block.
51    #[error("Unknown transactions in block.")]
52    UnknownTransactions([u8; 32], Vec<usize>),
53    /// We are missing the block's parent.
54    #[error("The block has an unknown parent.")]
55    Orphan,
56    /// The block was invalid.
57    #[error(transparent)]
58    InvalidBlock(anyhow::Error),
59    /// The blockchain manager command channel is closed.
60    #[error("The blockchain manager command channel is closed.")]
61    ChannelClosed,
62}
63
64impl BlockchainManagerHandle {
65    /// Create a new handle and command receiver pair.
66    pub(crate) fn new() -> (Self, mpsc::Receiver<BlockchainManagerCommand>) {
67        let (command_tx, command_rx) = mpsc::channel(3);
68        (
69            Self {
70                command_tx,
71                blocks_being_handled: Arc::new(Mutex::new(HashSet::new())),
72            },
73            command_rx,
74        )
75    }
76
77    /// Returns `true` if the given block hash is currently being handled.
78    pub fn is_block_being_handled(&self, hash: &[u8; 32]) -> bool {
79        self.blocks_being_handled.lock().unwrap().contains(hash)
80    }
81
82    /// Try to add a new block to the blockchain.
83    ///
84    /// On success returns `IncomingBlockOk`.
85    ///
86    /// # Errors
87    ///
88    /// This function will return an error if:
89    ///  - the block was invalid
90    ///  - we are missing transactions
91    ///  - the block's parent is unknown
92    ///  - the blockchain manager command channel is closed
93    pub async fn handle_incoming_block(
94        &self,
95        block: Block,
96        mut given_txs: HashMap<[u8; 32], Transaction>,
97        blockchain_read_handle: &mut BlockchainReadHandle,
98        txpool_read_handle: &mut TxpoolReadHandle,
99    ) -> Result<IncomingBlockOk, IncomingBlockError> {
100        if given_txs.len() > block.transactions.len() {
101            return Err(IncomingBlockError::InvalidBlock(anyhow::anyhow!(
102                "Too many transactions given for block"
103            )));
104        }
105
106        if !block_exists(block.header.previous, blockchain_read_handle)
107            .await
108            .expect(PANIC_CRITICAL_SERVICE_ERROR)
109        {
110            return Err(IncomingBlockError::Orphan);
111        }
112
113        let block_hash = block.hash();
114
115        if block_exists(block_hash, blockchain_read_handle)
116            .await
117            .expect(PANIC_CRITICAL_SERVICE_ERROR)
118        {
119            return Ok(IncomingBlockOk::AlreadyHave);
120        }
121
122        let TxpoolReadResponse::TxsForBlock { mut txs, missing } = txpool_read_handle
123            .ready()
124            .await
125            .expect(PANIC_CRITICAL_SERVICE_ERROR)
126            .call(TxpoolReadRequest::TxsForBlock(block.transactions.clone()))
127            .await
128            .expect(PANIC_CRITICAL_SERVICE_ERROR)
129        else {
130            unreachable!()
131        };
132
133        if !missing.is_empty() {
134            let needed_hashes = missing.iter().map(|index| block.transactions[*index]);
135
136            for needed_hash in needed_hashes {
137                let Some(tx) = given_txs.remove(&needed_hash) else {
138                    // We return back the indexes of all txs missing from our pool, not taking into account the txs
139                    // that were given with the block, as these txs will be dropped. It is not worth it to try to add
140                    // these txs to the pool as this will only happen with a misbehaving peer or if the txpool reaches
141                    // the size limit.
142                    return Err(IncomingBlockError::UnknownTransactions(block_hash, missing));
143                };
144
145                txs.insert(
146                    needed_hash,
147                    new_tx_verification_data(tx)
148                        .map_err(|e| IncomingBlockError::InvalidBlock(e.into()))?,
149                );
150            }
151        }
152
153        // Add the blocks hash to the blocks being handled.
154        if !self.blocks_being_handled.lock().unwrap().insert(block_hash) {
155            // If another place is already adding this block then we can stop.
156            return Ok(IncomingBlockOk::AlreadyHave);
157        }
158
159        // We must remove the block hash from `blocks_being_handled`.
160        let blocks = Arc::clone(&self.blocks_being_handled);
161        let _guard = {
162            struct RemoveFromBlocksBeingHandled {
163                block_hash: [u8; 32],
164                blocks: Arc<Mutex<HashSet<[u8; 32]>>>,
165            }
166            impl Drop for RemoveFromBlocksBeingHandled {
167                fn drop(&mut self) {
168                    self.blocks.lock().unwrap().remove(&self.block_hash);
169                }
170            }
171            RemoveFromBlocksBeingHandled { block_hash, blocks }
172        };
173
174        let (response_tx, response_rx) = oneshot::channel();
175
176        self.command_tx
177            .send(BlockchainManagerCommand::AddBlock {
178                block,
179                prepped_txs: txs,
180                response_tx,
181            })
182            .await
183            .map_err(|_| IncomingBlockError::ChannelClosed)?;
184
185        response_rx
186            .await
187            .map_err(|_| IncomingBlockError::ChannelClosed)?
188            .map_err(IncomingBlockError::InvalidBlock)
189    }
190
191    /// Pop blocks from the top of the blockchain.
192    ///
193    /// # Errors
194    ///
195    /// Will error if the blockchain manager channel is closed.
196    pub async fn pop_blocks(&self, numb_blocks: usize) -> Result<(), anyhow::Error> {
197        let (response_tx, response_rx) = oneshot::channel();
198
199        self.command_tx
200            .send(BlockchainManagerCommand::PopBlocks {
201                numb_blocks,
202                response_tx,
203            })
204            .await?;
205
206        Ok(response_rx.await?)
207    }
208}
209
210/// Check if we have a block with the given hash.
211async fn block_exists(
212    block_hash: [u8; 32],
213    blockchain_read_handle: &mut BlockchainReadHandle,
214) -> Result<bool, anyhow::Error> {
215    let BlockchainResponse::FindBlock(chain) = blockchain_read_handle
216        .ready()
217        .await?
218        .call(BlockchainReadRequest::FindBlock(block_hash))
219        .await?
220    else {
221        unreachable!();
222    };
223
224    Ok(chain.is_some())
225}