diff --git a/applications/minotari_app_grpc/proto/base_node.proto b/applications/minotari_app_grpc/proto/base_node.proto index b8bfe08146..45ec93364d 100644 --- a/applications/minotari_app_grpc/proto/base_node.proto +++ b/applications/minotari_app_grpc/proto/base_node.proto @@ -160,6 +160,7 @@ message TipInfoResponse { MetaData metadata = 1; bool initial_sync_achieved = 2; BaseNodeState base_node_state = 3; + bytes parent_hash = 4; } enum BaseNodeState{ @@ -191,13 +192,13 @@ message GetNewBlockTemplateWithCoinbasesRequest{ PowAlgo algo = 1; //This field should be moved to optional once optional keyword is standard uint64 max_weight = 2; - repeated NewBlockCoinbase coinbases = 3; + repeated NewBlockCoinbase coinbases = 3; } -/// request type of GetNewBlockWithCoinbasesRequest +/// request type of GetNewBlockWithCoinbasesRequest message GetNewBlockWithCoinbasesRequest{ NewBlockTemplate new_template = 1; - repeated NewBlockCoinbase coinbases = 2; + repeated NewBlockCoinbase coinbases = 2; } message NewBlockCoinbase{ @@ -218,7 +219,7 @@ message NetworkDifficultyResponse { uint64 sha3x_estimated_hash_rate = 6; uint64 randomx_estimated_hash_rate = 7; uint64 num_coinbases = 8; - repeated bytes coinbase_extras = 9; + repeated bytes coinbase_extras = 9; } // A generic single value response for a specific height @@ -252,7 +253,7 @@ message BlockGroupRequest { CalcType calc_type = 4; } -/// GetBlockSize / GetBlockFees Response +/// GetBlockSize / GetBlockFees Response message BlockGroupResponse { repeated double value = 1; CalcType calc_type = 2; @@ -436,7 +437,7 @@ message GetPeersResponse{ message GetPeersRequest{} message SubmitTransactionRequest { - Transaction transaction = 1; + Transaction transaction = 1; } message SubmitTransactionResponse { @@ -447,7 +448,7 @@ enum SubmitTransactionResult { NONE = 0; ACCEPTED = 1; NOT_PROCESSABLE_AT_THIS_TIME = 2; - ALREADY_MINED = 3; + ALREADY_MINED = 3; REJECTED = 4; } @@ -461,7 +462,7 @@ message GetMempoolTransactionsResponse { } message TransactionStateRequest { - Signature excess_sig = 1; + Signature excess_sig = 1; } message TransactionStateResponse { 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 e1098205bd..cd63c8a111 100644 --- a/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs +++ b/applications/minotari_merge_mining_proxy/src/block_template_protocol.rs @@ -90,8 +90,8 @@ impl BlockTemplateProtocol<'_> { ) -> Result { 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 { if loop_count >= 10 { @@ -101,6 +101,7 @@ impl BlockTemplateProtocol<'_> { loop_count ))); } + // Invalidate the cached template as the tip is not the same anymore; force creating a new template if loop_count == 1 && final_block_template.is_some() { final_block_template = None; } @@ -108,7 +109,7 @@ impl BlockTemplateProtocol<'_> { 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 (final_template_data, block_height, parent_hash) = if let Some(data) = final_block_template.clone() { let height = data .template .tari_block @@ -116,6 +117,13 @@ impl BlockTemplateProtocol<'_> { .as_ref() .map(|h| h.height) .unwrap_or_default(); + let prev_hash = data + .template + .tari_block + .header + .as_ref() + .map(|h| h.prev_hash.clone()) + .unwrap_or_default(); debug!( target: LOG_TARGET, "Used existing block template and block for height: #{} (try {}), block hash: `{}`", @@ -126,7 +134,7 @@ impl BlockTemplateProtocol<'_> { None => "None".to_string(), } ); - (data, height) + (data, height, prev_hash) } else { let block = match self.p2pool_client.as_mut() { Some(client) => { @@ -188,6 +196,11 @@ impl BlockTemplateProtocol<'_> { .as_ref() .map(|b| b.header.as_ref().map(|h| h.height).unwrap_or_default()) .unwrap_or_default(); + let prev_hash = block + .block + .as_ref() + .map(|b| b.header.as_ref().map(|h| h.prev_hash.clone()).unwrap_or_default()) + .unwrap_or_default(); let miner_data = block .miner_data @@ -195,7 +208,11 @@ impl BlockTemplateProtocol<'_> { .cloned() .ok_or_else(|| MmProxyError::GrpcResponseMissingField("miner_data"))?; - (add_monero_data(block, monero_mining_data.clone(), miner_data)?, height) + ( + add_monero_data(block, monero_mining_data.clone(), miner_data)?, + height, + prev_hash, + ) }; block_templates @@ -210,10 +227,15 @@ impl BlockTemplateProtocol<'_> { .remove_new_block_template(best_block_hash.to_vec()) .await; - if !self.check_expected_tip(block_height).await? { + if !self + .check_expected_tip_and_parent(block_height, best_block_hash.as_slice(), &parent_hash) + .await? + { debug!( target: LOG_TARGET, - "Chain tip has progressed past template height {}. Fetching a new block template (try {}).", block_height, loop_count + "Template (height {}, parent {}) not based on current chain tip anymore, fetching a new block \ + template (try {}).", + block_height, parent_hash.to_hex(), loop_count ); continue; } @@ -224,7 +246,8 @@ impl BlockTemplateProtocol<'_> { .header .as_ref() .map(|h| h.height) - .unwrap_or_default(), loop_count, + .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(), @@ -337,8 +360,14 @@ impl BlockTemplateProtocol<'_> { Ok(NewBlockTemplateData { template, miner_data }) } - /// Check if the height is more than the actual tip. So if still makes sense to compute block for that height. - async fn check_expected_tip(&mut self, height: u64) -> Result { + /// Check if the height and parent hash is still as expected, so that it still makes sense to compute the block for + /// that height. + async fn check_expected_tip_and_parent( + &mut self, + height: u64, + best_block_hash: &[u8], + parent_hash: &[u8], + ) -> Result { let tip = self .base_node_client .clone() @@ -346,7 +375,31 @@ impl BlockTemplateProtocol<'_> { .await? .into_inner(); let tip_height = tip.metadata.as_ref().map(|m| m.best_block_height).unwrap_or(0); + let tip_hash = tip + .metadata + .as_ref() + .map(|m| m.best_block_hash.clone()) + .unwrap_or_default(); + let tip_parent_hash = tip.parent_hash; + if tip_hash != best_block_hash { + warn!( + target: LOG_TARGET, + "Base node received next block (hash={}) that has invalidated the block template (hash={})", + tip_parent_hash.to_hex(), + parent_hash.to_vec().to_hex() + ); + return Ok(false); + } + if tip_parent_hash != parent_hash { + warn!( + target: LOG_TARGET, + "Base node received next block (parent hash={}) that has invalidated the block template (parent hash={})", + tip_parent_hash.to_hex(), + parent_hash.to_vec().to_hex() + ); + return Ok(false); + } if height <= tip_height { warn!( target: LOG_TARGET, @@ -427,7 +480,7 @@ fn add_monero_data( let aux_chain_hashes = AuxChainHashes::try_from(vec![monero::Hash::from_slice(merge_mining_hash.as_slice())])?; let tari_target_difficulty = miner_data.tari_target_difficulty; - let pool_target_difficulty = miner_data.p2pool_target_difficulty.as_ref().map(|val| val.difficulty); + let p2pool_target_difficulty = miner_data.p2pool_target_difficulty.as_ref().map(|val| val.difficulty); let block_template_data = BlockTemplateDataBuilder::new() .tari_block( tari_block_result @@ -438,7 +491,7 @@ fn add_monero_data( .monero_seed(monero_mining_data.seed_hash) .monero_difficulty(monero_mining_data.difficulty) .tari_target_difficulty(tari_target_difficulty) - .p2pool_target_difficulty(pool_target_difficulty) + .p2pool_target_difficulty(p2pool_target_difficulty) .tari_merge_mining_hash(merge_mining_hash) .aux_hashes(aux_chain_hashes.clone()) .build()?; @@ -458,7 +511,7 @@ fn add_monero_data( let mining_difficulty = cmp::min( monero_mining_data.difficulty, - if let Some(val) = pool_target_difficulty { + if let Some(val) = p2pool_target_difficulty { cmp::min(val, tari_target_difficulty) } else { tari_target_difficulty @@ -466,8 +519,9 @@ fn add_monero_data( ); info!( target: LOG_TARGET, - "Difficulties: Minotari ({}), Monero({}), Selected({})", + "Difficulties: Minotari ({}), P2Pool ({:?}), Monero ({}), Selected ({})", tari_target_difficulty, + p2pool_target_difficulty, monero_mining_data.difficulty, mining_difficulty ); diff --git a/applications/minotari_merge_mining_proxy/src/proxy.rs b/applications/minotari_merge_mining_proxy/src/proxy.rs index 8bcf0bf9d8..60c7a30b8b 100644 --- a/applications/minotari_merge_mining_proxy/src/proxy.rs +++ b/applications/minotari_merge_mining_proxy/src/proxy.rs @@ -187,7 +187,7 @@ impl InnerService { let mut base_node_client = self.base_node_client.clone(); trace!(target: LOG_TARGET, "Successful connection to base node GRPC"); - let result = + let tari_tip_info = base_node_client .get_tip_info(grpc::Empty {}) .await @@ -195,19 +195,19 @@ impl InnerService { status: err, details: "get_tip_info failed".to_string(), })?; - let height = result + let height = tari_tip_info .get_ref() .metadata .as_ref() .map(|meta| meta.best_block_height) .ok_or(MmProxyError::GrpcResponseMissingField("base node metadata"))?; - if result.get_ref().initial_sync_achieved != self.initial_sync_achieved.load(Ordering::SeqCst) { + if tari_tip_info.get_ref().initial_sync_achieved != self.initial_sync_achieved.load(Ordering::SeqCst) { self.initial_sync_achieved - .store(result.get_ref().initial_sync_achieved, Ordering::SeqCst); + .store(tari_tip_info.get_ref().initial_sync_achieved, Ordering::SeqCst); debug!( target: LOG_TARGET, "Minotari base node initial sync status change to {}", - result.get_ref().initial_sync_achieved + tari_tip_info.get_ref().initial_sync_achieved ); } @@ -296,7 +296,7 @@ impl InnerService { .map_err(MmProxyError::ConversionError)?; let mut base_node_client = self.base_node_client.clone(); let p2pool_client = self.p2pool_client.clone(); - let achieved_target = if self.config.check_tari_difficulty_before_submit { + let achieved_or_tari_target = if self.config.check_tari_difficulty_before_submit { let timer = Instant::now(); let diff = randomx_difficulty( &tari_header, @@ -313,9 +313,9 @@ impl InnerService { let height = tari_header_mut.height; info!( target: LOG_TARGET, - "Checking if we must submit block #{}. Difficulties - achieved target: {}, tari target: {}, pool target: {:?}", + "Checking if we must submit block #{}. Difficulties - achieved: {}, tari target: {}, pool target: {:?}", height, - achieved_target, + achieved_or_tari_target, block_data.template.tari_target_difficulty, block_data.template.p2pool_target_difficulty, ); @@ -324,82 +324,66 @@ impl InnerService { return Err(MmProxyError::LogicalError("No node to submit block to".to_string())); } - let submit_to_base_node = achieved_target >= block_data.template.tari_target_difficulty; + let submit_to_base_node = achieved_or_tari_target >= block_data.template.tari_target_difficulty; let submit_to_p2pool = p2pool_client.is_some() && - achieved_target >= block_data.template.p2pool_target_difficulty.expect("has value"); - - let mut bn_time = Duration::from_secs(0); - let mut p2p_time = Duration::from_secs(0); - let (resp_bn, resp_p2p) = match (submit_to_base_node, submit_to_p2pool) { - (true, true) => { - let bn_timer = Instant::now(); - let response_bn = Some( - base_node_client - .submit_block(block_data.template.tari_block.clone()) - .await, - ); - bn_time = bn_timer.elapsed(); + achieved_or_tari_target >= block_data.template.p2pool_target_difficulty.expect("has value"); + + let (mut bn_time, mut p2p_time) = (Duration::from_secs(0), Duration::from_secs(0)); + let (mut resp_bn, mut resp_p2p) = (None, None); + if submit_to_base_node { + debug!(target: LOG_TARGET, "Submitting to base node"); + let bn_timer = Instant::now(); + resp_bn = Some( + base_node_client + .submit_block(block_data.template.tari_block.clone()) + .await, + ); + bn_time = bn_timer.elapsed(); + debug!( + target: LOG_TARGET, + "Submitted block #{} to Minotari node in {:.2?} (SubmitBlock)", + height, + bn_time, + ); + } + if submit_to_p2pool { + if let Some(mut client) = p2pool_client { + debug!(target: LOG_TARGET, "Submitting to p2pool"); let p2p_timer = Instant::now(); - let response_p2p = if let Some(mut client) = p2pool_client { - info!(target: LOG_TARGET, "Submitting to p2pool"); - Some( - client - .submit_block(SubmitBlockRequest { - block: Some(block_data.template.tari_block), - wallet_payment_address: self.wallet_payment_address.to_hex(), - achieved_difficulty: if self.config.check_tari_difficulty_before_submit { - Some(Difficulty { - difficulty: achieved_target, - }) - } else { - None - }, - }) - .await, - ) - } else { - None - }; - p2p_time = p2p_timer.elapsed(); - (response_bn, response_p2p) - }, - (true, false) => { - let bn_timer = Instant::now(); - let response_bn = Some( - base_node_client - .submit_block(block_data.template.tari_block.clone()) + resp_p2p = Some( + client + .submit_block(SubmitBlockRequest { + block: Some(block_data.template.tari_block), + wallet_payment_address: self.wallet_payment_address.to_hex(), + achieved_difficulty: if self.config.check_tari_difficulty_before_submit { + Some(Difficulty { + difficulty: achieved_or_tari_target, + }) + } else { + None + }, + }) .await, ); - bn_time = bn_timer.elapsed(); - (response_bn, None) - }, - (false, true) => { - let p2p_timer = Instant::now(); - let response_p2p = if let Some(mut client) = p2pool_client { - info!(target: LOG_TARGET, "Submitting to p2pool"); - Some( - client - .submit_block(SubmitBlockRequest { - block: Some(block_data.template.tari_block), - wallet_payment_address: self.wallet_payment_address.to_hex(), - achieved_difficulty: if self.config.check_tari_difficulty_before_submit { - Some(Difficulty { - difficulty: achieved_target, - }) - } else { - None - }, - }) - .await, - ) - } else { - None - }; p2p_time = p2p_timer.elapsed(); - (None, response_p2p) - }, - (false, false) => return Err(MmProxyError::LogicalError("No node to submit block to".to_string())), - }; + debug!( + target: LOG_TARGET, + "Submitted block #{} to p2pool in {:.2?} (SubmitBlock)", + height, + p2p_time, + ); + } else { + error!(target: LOG_TARGET, "p2pool mode enabled, but no p2pool node to submit block to"); + } + } + + if let (Some(Ok(resp_1)), Some(Ok(resp_2))) = (&resp_bn, &resp_p2p) { + if resp_1.get_ref() != resp_2.get_ref() { + return Err(MmProxyError::LogicalError( + "Base node and p2pool node did not agree on block submission".to_string(), + )); + } + } if let Some(resp) = resp_bn { match resp { @@ -414,12 +398,6 @@ impl InnerService { json_resp, json!({"id": TARI_CHAIN_ID, "block_hash": resp.block_hash.to_hex()}), ); - debug!( - target: LOG_TARGET, - "Submitted block #{} to Minotari node in {:.2?} (SubmitBlock)", - height, - bn_time, - ); } else { // self-select related, do not change. json_resp = json_rpc::default_block_accept_response(request["id"].as_i64()); @@ -458,12 +436,6 @@ impl InnerService { if let Some(resp) = resp_p2p { match resp { Ok(_) => { - debug!( - target: LOG_TARGET, - "Submitted block #{} to p2pool in {:.2?} (SubmitBlock)", - height, - p2p_time, - ); self.block_templates.remove_final_block_template(&hash).await; }, Err(err) => { @@ -471,7 +443,7 @@ impl InnerService { target: LOG_TARGET, "Problem submitting block #{} to p2pool, responded in {:.2?} (SubmitBlock): {}", height, - bn_time, + p2p_time, err ); }, @@ -826,6 +798,7 @@ impl InnerService { builder = builder.basic_auth(&self.config.monerod_username, Some(&self.config.monerod_password)); } + let timer = Instant::now(); debug!( target: LOG_TARGET, "[monerod] request: {} {}", @@ -898,9 +871,11 @@ impl InnerService { }; debug!( target: LOG_TARGET, - "[monerod] response: status = {}, monerod_rpc = {}", + "[monerod] response: status = {}, monerod_rpc = {} in {:.2?} for {}", json_response.status(), - rpc_status + rpc_status, + timer.elapsed(), + monerod_uri, ); Ok((request, json_response)) } diff --git a/applications/minotari_miner/src/run_miner.rs b/applications/minotari_miner/src/run_miner.rs index 3044375c60..cccf565f5a 100644 --- a/applications/minotari_miner/src/run_miner.rs +++ b/applications/minotari_miner/src/run_miner.rs @@ -502,34 +502,31 @@ async fn submit_block( wallet_payment_address: &TariAddress, difficulty: u64, ) -> Result { - match (submit_to_base_node, config.sha_p2pool_enabled) { - (true, false) => Ok(base_node_client - .submit_block(block) - .await - .map_err(MinerError::GrpcStatus)? - .into_inner()), - (true, true) => { - let response = base_node_client + let height = block.header.clone().unwrap_or_default().height; + let (mut resp_bn, mut resp_p2p) = (None, None); + if submit_to_base_node { + debug!(target: LOG_TARGET, "Submitting to base node"); + let bn_timer = Instant::now(); + resp_bn = Some( + base_node_client .submit_block(block.clone()) .await .map_err(MinerError::GrpcStatus)? - .into_inner(); - if let Some(client) = sha_p2pool_client { - return Ok(client - .submit_block(SubmitBlockRequest { - block: Some(block), - wallet_payment_address: wallet_payment_address.to_hex(), - achieved_difficulty: Some(Difficulty { difficulty }), - }) - .await - .map_err(MinerError::GrpcStatus)? - .into_inner()); - } - Ok(response) - }, - (false, true) => { - if let Some(client) = sha_p2pool_client { - Ok(client + .into_inner(), + ); + debug!( + target: LOG_TARGET, + "Submitted block #{} to Minotari node in {:.2?} (SubmitBlock)", + height, + bn_timer.elapsed(), + ); + } + if config.sha_p2pool_enabled { + if let Some(client) = sha_p2pool_client { + let p2p_timer = Instant::now(); + debug!(target: LOG_TARGET, "Submitting to p2pool"); + resp_p2p = Some( + client .submit_block(SubmitBlockRequest { block: Some(block), wallet_payment_address: wallet_payment_address.to_hex(), @@ -537,12 +534,33 @@ async fn submit_block( }) .await .map_err(MinerError::GrpcStatus)? - .into_inner()) - } else { - Err(MinerError::LogicalError("No node to submit block to".to_string())) + .into_inner(), + ); + debug!( + target: LOG_TARGET, + "Submitted block #{} to p2pool in {:.2?} (SubmitBlock)", + height, + p2p_timer.elapsed(), + ); + } else { + error!(target: LOG_TARGET, "p2pool mode enabled, but no p2pool node to submit block to"); + } + } + + match (resp_bn, resp_p2p) { + (Some(resp_1), Some(resp_2)) => { + if resp_1 != resp_2 { + return Err(MinerError::LogicalError( + "Base node and p2pool node did not agree on block submission".to_string(), + )); } + Ok(resp_1) }, - (false, false) => Err(MinerError::LogicalError("No node to submit block to".to_string())), + (Some(resp), None) => Ok(resp), + (None, Some(resp)) => Ok(resp), + (None, None) => Err(MinerError::LogicalError( + "No p2pool node or base node to submit block to".to_string(), + )), } } diff --git a/applications/minotari_node/src/grpc/base_node_grpc_server.rs b/applications/minotari_node/src/grpc/base_node_grpc_server.rs index 30cdb83e02..a3c822431d 100644 --- a/applications/minotari_node/src/grpc/base_node_grpc_server.rs +++ b/applications/minotari_node/src/grpc/base_node_grpc_server.rs @@ -1647,6 +1647,19 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { .await .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))?; + let parent_hash = if let Some(tip_header) = handler + .get_header_by_hash(*meta.best_block_hash()) + .await + .map_err(|e| obscure_error_if_true(report_error_flag, Status::internal(e.to_string())))? + { + tip_header.header().prev_hash + } else { + return Err(obscure_error_if_true( + report_error_flag, + Status::not_found("Tip header not found".to_string()), + )); + }; + // Determine if we are bootstrapped let status_watch = self.state_machine_handle.get_status_info_watch(); let state: tari_rpc::BaseNodeState = (&status_watch.borrow().state_info).into(); @@ -1654,6 +1667,7 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { metadata: Some(meta.into()), initial_sync_achieved: status_watch.borrow().bootstrapped, base_node_state: state.into(), + parent_hash: parent_hash.to_vec(), }; trace!(target: LOG_TARGET, "Sending MetaData response to client");