From cbe44314e5e8b209d8bf45f90b8091b9d11dea6a Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Mon, 18 Mar 2024 15:38:29 +0200 Subject: [PATCH] Cache new block template For the block template protocol: - Cached new block template and use it in case it is asked for the same best block repetitively. - Added an exit clause to the potential endless loop. --- .../src/block_template_data.rs | 160 +++++++++++++----- .../src/block_template_protocol.rs | 145 ++++++++++++---- .../minotari_merge_mining_proxy/src/error.rs | 2 + .../minotari_merge_mining_proxy/src/proxy.rs | 39 +---- 4 files changed, 243 insertions(+), 103 deletions(-) diff --git a/applications/minotari_merge_mining_proxy/src/block_template_data.rs b/applications/minotari_merge_mining_proxy/src/block_template_data.rs index cd043824c2..e26fef6897 100644 --- a/applications/minotari_merge_mining_proxy/src/block_template_data.rs +++ b/applications/minotari_merge_mining_proxy/src/block_template_data.rs @@ -33,28 +33,56 @@ use tari_core::proof_of_work::monero_rx::FixedByteArray; use tokio::sync::RwLock; use tracing::trace; -use crate::{block_template_protocol::FinalBlockTemplateData, error::MmProxyError}; +use crate::{ + block_template_protocol::{FinalBlockTemplateData, NewBlockTemplateData}, + error::MmProxyError, +}; const LOG_TARGET: &str = "minotari_mm_proxy::xmrig"; -/// Structure for holding hashmap of hashes -> [BlockTemplateRepositoryItem] +/// Structure for holding hashmap of hashes -> [BlockRepositoryItem] and [TemplateRepositoryItem]. #[derive(Debug, Clone)] pub struct BlockTemplateRepository { - blocks: Arc, BlockTemplateRepositoryItem>>>, + blocks: Arc, BlockRepositoryItem>>>, + templates: Arc, TemplateRepositoryItem>>>, } -/// Structure holding [BlockTemplateData] along with a timestamp. +/// Structure holding [NewBlockTemplate] along with a timestamp. #[derive(Debug, Clone)] -pub struct BlockTemplateRepositoryItem { +pub struct TemplateRepositoryItem { + pub new_block_template: NewBlockTemplateData, + pub template_with_coinbase: grpc::NewBlockTemplate, + datetime: DateTime, +} + +impl TemplateRepositoryItem { + /// Create new [Self] with current time in UTC. + pub fn new(new_block_template: NewBlockTemplateData, template_with_coinbase: grpc::NewBlockTemplate) -> Self { + Self { + new_block_template, + template_with_coinbase, + datetime: Utc::now(), + } + } + + /// Get the timestamp of creation. + pub fn datetime(&self) -> DateTime { + self.datetime + } +} + +/// Structure holding [FinalBlockTemplateData] along with a timestamp. +#[derive(Debug, Clone)] +pub struct BlockRepositoryItem { pub data: FinalBlockTemplateData, datetime: DateTime, } -impl BlockTemplateRepositoryItem { +impl BlockRepositoryItem { /// Create new [Self] with current time in UTC. - pub fn new(block_template: FinalBlockTemplateData) -> Self { + pub fn new(final_block: FinalBlockTemplateData) -> Self { Self { - data: block_template, + data: final_block, datetime: Utc::now(), } } @@ -69,35 +97,71 @@ impl BlockTemplateRepository { pub fn new() -> Self { Self { blocks: Arc::new(RwLock::new(HashMap::new())), + templates: Arc::new(RwLock::new(HashMap::new())), } } /// Return [BlockTemplateData] with the associated hash. None if the hash is not stored. - pub async fn get>(&self, hash: T) -> Option { - trace!( - target: LOG_TARGET, - "Retrieving blocktemplate with merge mining hash: {:?}", - hex::encode(hash.as_ref()) - ); + pub async fn get_final_template>(&self, merge_mining_hash: T) -> Option { let b = self.blocks.read().await; - b.get(hash.as_ref()).map(|item| item.data.clone()) + b.get(merge_mining_hash.as_ref()).map(|item| { + trace!( + target: LOG_TARGET, + "Retrieving block template at height #{} with merge mining hash: {:?}", + item.data.clone().template.new_block_template.header.unwrap_or_default().height, + hex::encode(merge_mining_hash.as_ref()) + ); + item.data.clone() + }) + } + + /// Return [BlockTemplateData] with the associated hash. None if the hash is not stored. + pub async fn get_new_template>( + &self, + best_block_hash: T, + ) -> Option<(NewBlockTemplateData, grpc::NewBlockTemplate)> { + let b = self.templates.read().await; + b.get(best_block_hash.as_ref()) + .map(|item| (item.new_block_template.clone(), item.template_with_coinbase.clone())) } - /// Store [BlockTemplateData] at the hash value if the key does not exist. - pub async fn save_if_key_unique(&self, hash: Vec, block_template: FinalBlockTemplateData) { + /// Store [FinalBlockTemplateData] at the hash value if the key does not exist. + pub async fn save_final_block_template_if_key_unique( + &self, + merge_mining_hash: Vec, + block_template: FinalBlockTemplateData, + ) { let mut b = self.blocks.write().await; - b.entry(hash.clone()).or_insert_with(|| { + b.entry(merge_mining_hash.clone()).or_insert_with(|| { + trace!( + target: LOG_TARGET, + "Saving final block template with merge mining hash: {:?}", + hex::encode(&merge_mining_hash) + ); + BlockRepositoryItem::new(block_template) + }); + } + + /// Store [NewBlockTemplate] at the hash value if the key does not exist. + pub async fn save_new_block_template_if_key_unique( + &self, + best_block_hash: Vec, + new_block_template: NewBlockTemplateData, + template_with_coinbase: grpc::NewBlockTemplate, + ) { + let mut b = self.templates.write().await; + b.entry(best_block_hash.clone()).or_insert_with(|| { trace!( target: LOG_TARGET, - "Saving blocktemplate with merge mining hash: {:?}", - hex::encode(&hash) + "Saving new block template for best block hash: {:?}", + hex::encode(&best_block_hash) ); - BlockTemplateRepositoryItem::new(block_template) + TemplateRepositoryItem::new(new_block_template, template_with_coinbase) }); } /// Check if the repository contains a block template with best_previous_block_hash - pub async fn contains(&self, current_best_block_hash: FixedHash) -> Option { + pub async fn blocks_contains(&self, current_best_block_hash: FixedHash) -> Option { let b = self.blocks.read().await; b.values() .find(|item| { @@ -109,25 +173,43 @@ impl BlockTemplateRepository { /// Remove any data that is older than 20 minutes. pub async fn remove_outdated(&self) { - trace!(target: LOG_TARGET, "Removing outdated blocktemplates"); + trace!(target: LOG_TARGET, "Removing outdated final block templates"); let mut b = self.blocks.write().await; #[cfg(test)] let threshold = Utc::now(); #[cfg(not(test))] let threshold = Utc::now() - Duration::minutes(20); *b = b.drain().filter(|(_, i)| i.datetime() >= threshold).collect(); + trace!(target: LOG_TARGET, "Removing outdated new block templates"); + let mut b = self.templates.write().await; + #[cfg(test)] + let threshold = Utc::now(); + #[cfg(not(test))] + let threshold = Utc::now() - Duration::minutes(20); + *b = b.drain().filter(|(_, i)| i.datetime() >= threshold).collect(); } - /// Remove a particular hash and return the associated [BlockTemplateRepositoryItem] if any. - pub async fn remove>(&self, hash: T) -> Option { + /// Remove a particularfinla block template for hash and return the associated [BlockRepositoryItem] if any. + pub async fn remove_final_block_template>(&self, hash: T) -> Option { trace!( target: LOG_TARGET, - "Blocktemplate removed with merge mining hash {:?}", + "Final block template removed with merge mining hash {:?}", hex::encode(hash.as_ref()) ); let mut b = self.blocks.write().await; b.remove(hash.as_ref()) } + + /// Remove a particular new block template for ash and return the associated [BlockRepositoryItem] if any. + pub async fn remove_new_block_template>(&self, hash: T) -> Option { + trace!( + target: LOG_TARGET, + "New block template removed with best block hash {:?}", + hex::encode(hash.as_ref()) + ); + let mut b = self.templates.write().await; + b.remove(hash.as_ref()) + } } /// Setup values for the new block. @@ -299,19 +381,21 @@ pub mod test { let hash2 = vec![2; 32]; let hash3 = vec![3; 32]; let block_template = create_block_template_data(); - btr.save_if_key_unique(hash1.clone(), block_template.clone()).await; - btr.save_if_key_unique(hash2.clone(), block_template).await; - assert!(btr.get(hash1.clone()).await.is_some()); - assert!(btr.get(hash2.clone()).await.is_some()); - assert!(btr.get(hash3.clone()).await.is_none()); - assert!(btr.remove(hash1.clone()).await.is_some()); - assert!(btr.get(hash1.clone()).await.is_none()); - assert!(btr.get(hash2.clone()).await.is_some()); - assert!(btr.get(hash3.clone()).await.is_none()); + btr.save_final_block_template_if_key_unique(hash1.clone(), block_template.clone()) + .await; + btr.save_final_block_template_if_key_unique(hash2.clone(), block_template) + .await; + assert!(btr.get_final_template(hash1.clone()).await.is_some()); + assert!(btr.get_final_template(hash2.clone()).await.is_some()); + assert!(btr.get_final_template(hash3.clone()).await.is_none()); + assert!(btr.remove_final_block_template(hash1.clone()).await.is_some()); + assert!(btr.get_final_template(hash1.clone()).await.is_none()); + assert!(btr.get_final_template(hash2.clone()).await.is_some()); + assert!(btr.get_final_template(hash3.clone()).await.is_none()); btr.remove_outdated().await; - assert!(btr.get(hash1).await.is_none()); - assert!(btr.get(hash2).await.is_none()); - assert!(btr.get(hash3).await.is_none()); + assert!(btr.get_final_template(hash1).await.is_none()); + assert!(btr.get_final_template(hash2).await.is_none()); + assert!(btr.get_final_template(hash3).await.is_none()); } #[test] diff --git a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs index fa0b685476..afd7809755 100644 --- a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs +++ b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs @@ -36,10 +36,10 @@ use tari_core::{ transaction_components::{TransactionKernel, TransactionOutput}, }, }; -use tari_utilities::hex::Hex; +use tari_utilities::{hex::Hex, ByteArray}; use crate::{ - block_template_data::{BlockTemplateData, BlockTemplateDataBuilder}, + block_template_data::{BlockTemplateData, BlockTemplateDataBuilder, BlockTemplateRepository}, common::merge_mining, config::MergeMiningProxyConfig, error::MmProxyError, @@ -79,11 +79,29 @@ impl BlockTemplateProtocol<'_> { pub async fn get_next_block_template( mut self, monero_mining_data: MoneroMiningData, - existing_block_template: Option, + block_templates: &BlockTemplateRepository, ) -> Result { - let mut existing_block_template = existing_block_template; + let best_block_hash = self.get_current_best_block_hash().await?; + let existing_block_template = block_templates.blocks_contains(best_block_hash).await; + + let mut final_block_template = existing_block_template; + let mut loop_count = 0; loop { - let (final_template_data, block_height) = if let Some(data) = existing_block_template.clone() { + if loop_count >= 10 { + warn!(target: LOG_TARGET, "Failed to get block template after {} retries", loop_count); + return Err(MmProxyError::FailedToGetBlockTemplate(format!( + "Retried {} times", + loop_count + ))); + } + if loop_count == 1 && final_block_template.is_some() { + final_block_template = None; + } + if loop_count > 0 { + tokio::time::sleep(std::time::Duration::from_millis(loop_count * 250)).await; + } + loop_count += 1; + let (final_template_data, block_height) = if let Some(data) = final_block_template.clone() { let height = data .template .tari_block @@ -93,8 +111,9 @@ impl BlockTemplateProtocol<'_> { .unwrap_or_default(); debug!( target: LOG_TARGET, - "Used existing block template and block for height: #{}, block hash: `{}`", + "Used existing block template and block for height: #{} (try {}), block hash: `{}`", height, + loop_count, match data.template.tari_block.header.as_ref() { Some(h) => h.hash.to_hex(), None => "None".to_string(), @@ -102,25 +121,62 @@ impl BlockTemplateProtocol<'_> { ); (data, height) } else { - let new_template = self.get_new_block_template().await?; - let height = new_template - .template - .header - .as_ref() - .map(|h| h.height) - .unwrap_or_default(); - debug!(target: LOG_TARGET, "Requested new block template at height: #{}", height); - let (coinbase_output, coinbase_kernel) = self.get_coinbase(&new_template).await?; + let (new_template, block_template_with_coinbase, height) = match block_templates + .get_new_template(best_block_hash) + .await + { + None => { + let new_template = match self.get_new_block_template().await { + Ok(val) => val, + Err(err) => { + error!(target: LOG_TARGET, "grpc get_new_block_template ({})", err.to_string()); + return Err(err); + }, + }; + let height = new_template + .template + .header + .as_ref() + .map(|h| h.height) + .unwrap_or_default(); + debug!(target: LOG_TARGET, "Requested new block template at height: #{} (try {})", height, loop_count); + let (coinbase_output, coinbase_kernel) = self.get_coinbase(&new_template).await?; + + let template_with_coinbase = merge_mining::add_coinbase( + &coinbase_output, + &coinbase_kernel, + new_template.template.clone(), + )?; + debug!(target: LOG_TARGET, "Added coinbase to new block template (try {})", loop_count); + + block_templates + .save_new_block_template_if_key_unique( + best_block_hash.to_vec(), + new_template.clone(), + template_with_coinbase.clone(), + ) + .await; + + (new_template, template_with_coinbase, height) + }, + Some((new_template, template_with_coinbase)) => { + let height = new_template + .template + .header + .as_ref() + .map(|h| h.height) + .unwrap_or_default(); + debug!(target: LOG_TARGET, "Used existing new block template at height: #{} (try {})", height, loop_count); + (new_template, template_with_coinbase, height) + }, + }; - let block_template_with_coinbase = - merge_mining::add_coinbase(&coinbase_output, &coinbase_kernel, new_template.template.clone())?; - debug!(target: LOG_TARGET, "Added coinbase to new block template"); let block = match self.get_new_block(block_template_with_coinbase).await { Ok(b) => { debug!( target: LOG_TARGET, - "Requested new block at height: #{}, block hash: `{}`", - height, + "Requested new block at height: #{} (try {}), block hash: `{}`", + height, loop_count, { let block_header = b.block.as_ref().map(|b| b.header.as_ref()).unwrap_or_default(); block_header.map(|h| h.hash.clone()).unwrap_or_default().to_hex() @@ -131,12 +187,15 @@ impl BlockTemplateProtocol<'_> { Err(MmProxyError::FailedPreconditionBlockLostRetry) => { debug!( target: LOG_TARGET, - "Chain tip has progressed past template height {}. Fetching a new block template.", - new_template.template.header.as_ref().map(|h| h.height).unwrap_or(0) + "Chain tip has progressed past template height {}. Fetching a new block template (try {}).", + height, loop_count ); continue; }, - Err(err) => return Err(err), + Err(err) => { + error!(target: LOG_TARGET, "grpc get_new_block ({})", err.to_string()); + return Err(err); + }, }; ( @@ -145,24 +204,33 @@ impl BlockTemplateProtocol<'_> { ) }; + block_templates + .save_final_block_template_if_key_unique( + // `aux_chain_mr` is used as the key because it is stored in the ExtraData field in the Monero + // block + final_template_data.aux_chain_mr.clone(), + final_template_data.clone(), + ) + .await; + block_templates + .remove_new_block_template(best_block_hash.to_vec()) + .await; + if !self.check_expected_tip(block_height).await? { debug!( target: LOG_TARGET, - "Chain tip has progressed past template height {}. Fetching a new block template.", block_height + "Chain tip has progressed past template height {}. Fetching a new block template (try {}).", block_height, loop_count ); - if existing_block_template.is_some() { - existing_block_template = None; - } continue; } info!(target: LOG_TARGET, - "Block template for height: #{}, block hash: `{}`, {}", + "Block template for height: #{} (try {}), block hash: `{}`, {}", final_template_data .template.new_block_template .header .as_ref() .map(|h| h.height) - .unwrap_or_default(), + .unwrap_or_default(), loop_count, match final_template_data.template.tari_block.header.as_ref() { Some(h) => h.hash.to_hex(), None => "None".to_string(), @@ -273,6 +341,21 @@ impl BlockTemplateProtocol<'_> { .await?; Ok((coinbase_output, coinbase_kernel)) } + + async fn get_current_best_block_hash(&self) -> Result { + let tip = self + .base_node_client + .clone() + .get_tip_info(grpc::Empty {}) + .await? + .into_inner(); + let best_block_hash = tip + .metadata + .as_ref() + .map(|m| m.best_block_hash.clone()) + .unwrap_or(Vec::default()); + FixedHash::try_from(best_block_hash).map_err(|e| MmProxyError::ConversionError(e.to_string())) + } } /// This is an interim solution to calculate the merkle root for the aux chains when multiple aux chains will be @@ -350,8 +433,8 @@ fn add_monero_data( } /// Private convenience container struct for new template data -#[allow(dead_code)] -struct NewBlockTemplateData { +#[derive(Debug, Clone)] +pub struct NewBlockTemplateData { pub template: grpc::NewBlockTemplate, pub miner_data: grpc::MinerData, pub initial_sync_achieved: bool, diff --git a/applications/minotari_merge_mining_proxy/src/error.rs b/applications/minotari_merge_mining_proxy/src/error.rs index 2b3ad90a55..fe58c43f3a 100644 --- a/applications/minotari_merge_mining_proxy/src/error.rs +++ b/applications/minotari_merge_mining_proxy/src/error.rs @@ -113,6 +113,8 @@ pub enum MmProxyError { BaseNodeNotResponding(String), #[error("Unexpected missing data: {0}")] UnexpectedMissingData(String), + #[error("Failed to get block template: {0}")] + FailedToGetBlockTemplate(String), } impl From for MmProxyError { diff --git a/applications/minotari_merge_mining_proxy/src/proxy.rs b/applications/minotari_merge_mining_proxy/src/proxy.rs index bd2e71b0ad..6e55ca1c36 100644 --- a/applications/minotari_merge_mining_proxy/src/proxy.rs +++ b/applications/minotari_merge_mining_proxy/src/proxy.rs @@ -22,7 +22,7 @@ use std::{ cmp, - convert::{TryFrom, TryInto}, + convert::TryInto, future::Future, pin::Pin, sync::{ @@ -43,7 +43,7 @@ use minotari_app_utilities::parse_miner_input::BaseNodeGrpcClient; use minotari_node_grpc_client::grpc; use reqwest::{ResponseBuilderExt, Url}; use serde_json as json; -use tari_common_types::{tari_address::TariAddress, types::FixedHash}; +use tari_common_types::tari_address::TariAddress; use tari_core::{ consensus::ConsensusManager, proof_of_work::{monero_rx, monero_rx::FixedByteArray, randomx_difficulty, randomx_factory::RandomXFactory}, @@ -254,7 +254,7 @@ impl InnerService { hex::encode(hash) ); - let mut block_data = match self.block_templates.get(&hash).await { + let mut block_data = match self.block_templates.get_final_template(&hash).await { Some(d) => d, None => { info!( @@ -341,7 +341,7 @@ impl InnerService { json_resp ); } - self.block_templates.remove(&hash).await; + self.block_templates.remove_final_block_template(&hash).await; }, Err(err) => { debug!( @@ -375,21 +375,6 @@ impl InnerService { Ok(proxy::into_response(parts, &json_resp)) } - async fn get_current_best_block_hash(&self) -> Result { - let tip = self - .base_node_client - .clone() - .get_tip_info(grpc::Empty {}) - .await? - .into_inner(); - let best_block_hash = tip - .metadata - .as_ref() - .map(|m| m.best_block_hash.clone()) - .unwrap_or(Vec::default()); - FixedHash::try_from(best_block_hash).map_err(|e| MmProxyError::ConversionError(e.to_string())) - } - #[allow(clippy::too_many_lines)] async fn handle_get_block_template( &self, @@ -483,14 +468,8 @@ impl InnerService { difficulty, }; - let existing_block_template = if let Ok(best_block_hash) = self.get_current_best_block_hash().await { - self.block_templates.contains(best_block_hash).await - } else { - None - }; - let final_block_template_data = new_block_protocol - .get_next_block_template(monero_mining_data, existing_block_template) + .get_next_block_template(monero_mining_data, &self.block_templates) .await?; monerod_resp["result"]["blocktemplate_blob"] = final_block_template_data.blocktemplate_blob.clone().into(); @@ -524,14 +503,6 @@ impl InnerService { }), ); - self.block_templates - .save_if_key_unique( - // `aux_chain_mr` is used as the key because it is stored in the ExtraData field in the Monero block - final_block_template_data.aux_chain_mr.clone(), - final_block_template_data, - ) - .await; - debug!(target: LOG_TARGET, "Returning template result: {}", monerod_resp); Ok(proxy::into_response(parts, &monerod_resp)) }