diff --git a/changelog.d/6989-miner-diagnostics.added b/changelog.d/6989-miner-diagnostics.added new file mode 100644 index 0000000000..c8d5faeadd --- /dev/null +++ b/changelog.d/6989-miner-diagnostics.added @@ -0,0 +1 @@ +Added some diagnostics data to a miner's block proposal StackerDB message. This data contains nothing confidential, only some information about the miner's view of the world. This may help with investigating issues where a miner keeps submitting blocks that are then rejected by signers. \ No newline at end of file diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 74259efdce..4477549724 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -36,6 +36,7 @@ use stacks_common::codec::{ use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, StacksPublicKey, }; +use stacks_common::types::MinerDiagnosticData; use stacks_common::util::hash::{hex_bytes, Sha512Trunc256Sum}; use stacks_common::util::serde_serializers::{prefix_hex, prefix_opt_hex, prefix_string_0x}; use stacks_common::versions::STACKS_NODE_VERSION; @@ -91,15 +92,19 @@ impl StacksMessageCodec for BlockProposal { } /// The latest version of the block response data -pub const BLOCK_PROPOSAL_DATA_VERSION: u8 = 2; +pub const BLOCK_PROPOSAL_DATA_VERSION: u8 = 3; -/// Versioned, backwards-compatible struct for block response data +/// Versioned, backwards-compatible struct for block proposal data #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BlockProposalData { /// The version of the block proposal data pub version: u8, /// The miner's server version pub server_version: String, + + /// Added in version 3 + pub miner_diagnostic_data: Option, + /// When deserializing future versions, /// there may be extra bytes that we don't know about pub unknown_bytes: Vec, @@ -107,31 +112,46 @@ pub struct BlockProposalData { impl BlockProposalData { /// Create a new BlockProposalData for the provided server version and unknown bytes - pub fn new(server_version: String) -> Self { + pub fn new(server_version: String, miner_diagnostic_data: MinerDiagnosticData) -> Self { Self { version: BLOCK_PROPOSAL_DATA_VERSION, server_version, + miner_diagnostic_data: Some(miner_diagnostic_data), unknown_bytes: vec![], } } /// Create a new BlockProposalData with the current build's version - pub fn from_current_version() -> Self { + pub fn from_current_version(miner_diagnostic_data: MinerDiagnosticData) -> Self { let server_version = version_string( "stacks-node", option_env!("STACKS_NODE_VERSION").or(Some(STACKS_NODE_VERSION)), ); - Self::new(server_version) + Self::new(server_version, miner_diagnostic_data) } /// Create an empty BlockProposalData pub fn empty() -> Self { - Self::new(String::new()) + Self { + version: 1, + server_version: String::new(), + miner_diagnostic_data: None, + unknown_bytes: vec![], + } } - /// Serialize the "inner" block response data. Used to determine the bytes length of the serialized block response data + /// Serialize the "inner" block proposal data. Used to determine the bytes length of the serialized block proposal data fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { write_next(fd, &self.server_version.as_bytes().to_vec())?; + if self.version >= 3 { + let miner_diagnostic_data = + self.miner_diagnostic_data + .as_ref() + .ok_or(CodecError::SerializeError( + "BlockProposalData v3+ must have miner diagnostic data".into(), + ))?; + miner_diagnostic_data.consensus_serialize(fd)?; + } fd.write_all(&self.unknown_bytes) .map_err(CodecError::WriteError)?; Ok(()) @@ -166,9 +186,17 @@ impl StacksMessageCodec for BlockProposalData { let server_version = String::from_utf8(server_version).map_err(|e| { CodecError::DeserializeError(format!("Failed to decode server version: {:?}", &e)) })?; + let miner_diagnostic_data = if version >= 3 { + Some(MinerDiagnosticData::consensus_deserialize( + &mut inner_reader, + )?) + } else { + None + }; Ok(Self { version, server_version, + miner_diagnostic_data, unknown_bytes: inner_reader.to_vec(), }) } @@ -669,6 +697,7 @@ pub fn get_signers_db_signer_set_message_id(name: &str) -> Option<(u32, u32)> { #[cfg(test)] mod tests { use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader; + use clarity::types::MiningReason; use super::*; @@ -734,7 +763,7 @@ mod tests { block: block.clone(), burn_height: 1, reward_cycle: 2, - block_proposal_data: BlockProposalData::from_current_version(), + block_proposal_data: BlockProposalData::from_current_version(dummy_diagnostic_data()), }; let mut bytes = vec![]; new_block_proposal.consensus_serialize(&mut bytes).unwrap(); @@ -783,4 +812,15 @@ mod tests { String::new() ); } + + fn dummy_diagnostic_data() -> MinerDiagnosticData { + MinerDiagnosticData { + burnchain_tip_height: 42, + burnchain_tip_consensus_hash: ConsensusHash::from_bytes(&[42u8; 20]).unwrap(), + burnchain_tip_header_hash: BurnchainHeaderHash::from_bytes(&[42u8; 32]).unwrap(), + read_count_extend_timestamp: 1773830677, + tenure_extend_time_stamp: 1773830677, + mining_reason: MiningReason::ReadCountExtend, + } + } } diff --git a/libsigner/src/tests/block_proposal_data.rs b/libsigner/src/tests/block_proposal_data.rs new file mode 100644 index 0000000000..86d9ab66c7 --- /dev/null +++ b/libsigner/src/tests/block_proposal_data.rs @@ -0,0 +1,145 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::io::{Read, Write}; + +use clarity::codec::{ + read_next, read_next_at_most, write_next, Error as CodecError, StacksMessageCodec, +}; +use clarity::types::chainstate::{BurnchainHeaderHash, ConsensusHash}; +use clarity::types::{MinerDiagnosticData, MiningReason}; +use serde::{Deserialize, Serialize}; + +use crate::v0::messages::BLOCK_RESPONSE_DATA_MAX_SIZE; +use crate::BlockProposalData; + +pub const BLOCK_PROPOSAL_DATA_VERSION_V2: u8 = 2; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub struct BlockProposalData_v2 { + pub version: u8, + pub server_version: String, + pub unknown_bytes: Vec, +} + +impl BlockProposalData_v2 { + pub fn new(server_version: String) -> Self { + Self { + version: BLOCK_PROPOSAL_DATA_VERSION_V2, + server_version, + unknown_bytes: vec![], + } + } + + pub fn empty() -> Self { + Self::new(String::new()) + } + + fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &self.server_version.as_bytes().to_vec())?; + fd.write_all(&self.unknown_bytes) + .map_err(CodecError::WriteError)?; + Ok(()) + } +} + +impl StacksMessageCodec for BlockProposalData_v2 { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &self.version)?; + let mut inner_bytes = vec![]; + self.inner_consensus_serialize(&mut inner_bytes)?; + write_next(fd, &inner_bytes)?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let Ok(version) = read_next(fd) else { + return Ok(Self::empty()); + }; + let inner_bytes: Vec = read_next_at_most(fd, BLOCK_RESPONSE_DATA_MAX_SIZE)?; + let mut inner_reader = inner_bytes.as_slice(); + let server_version: Vec = read_next(&mut inner_reader)?; + let server_version = String::from_utf8(server_version).map_err(|e| { + CodecError::DeserializeError(format!("Failed to decode server version: {:?}", &e)) + })?; + Ok(Self { + version, + server_version, + unknown_bytes: inner_reader.to_vec(), + }) + } +} + +/// Asserts that current version BlockProposalData can be deserialized and reserialized +/// by an older version without losing any information. +#[test] +fn block_proposal_data_serialization_roundtrip_v2() { + let original_data = BlockProposalData::new( + "myversion".into(), + MinerDiagnosticData { + burnchain_tip_height: 67, + burnchain_tip_consensus_hash: ConsensusHash::from_bytes(&[0xabu8; 20]).unwrap(), + burnchain_tip_header_hash: BurnchainHeaderHash::from_bytes(&[99u8; 32]).unwrap(), + read_count_extend_timestamp: 1764576000, + tenure_extend_time_stamp: 1804989566, + mining_reason: MiningReason::Extended, + }, + ); + + let serialized = original_data.serialize_to_vec(); + + let v2_deserialized = BlockProposalData_v2::consensus_deserialize(&mut &serialized[..]) + .expect("BlockProposalData v2 should deserialize v3 data without error"); + + assert_eq!(v2_deserialized.server_version, "myversion"); + assert_eq!(v2_deserialized.version, original_data.version); + assert!(!v2_deserialized.unknown_bytes.is_empty()); + + let v2_serialized = v2_deserialized.serialize_to_vec(); + + let roundtripped = BlockProposalData::consensus_deserialize(&mut &v2_serialized[..]).expect( + "current BlockProposalData should deserialize data from older serializers without error", + ); + + assert_eq!(original_data, roundtripped); +} + +/// Asserts that we can successfully deserialize block proposal data that was serialized +/// by an older version, and re-serialize it identically. +#[test] +fn block_proposal_data_backwards_compatible() { + let original_data = BlockProposalData_v2::new("1.2.3.4".into()); + + let serialized = original_data.serialize_to_vec(); + + let deserialized = BlockProposalData::consensus_deserialize(&mut &serialized[..]) + .expect("current BlockProposalData should deserialize v2 data without error"); + + assert_eq!(deserialized.server_version, "1.2.3.4"); + assert_eq!(deserialized.version, original_data.version); + assert!(deserialized.unknown_bytes.is_empty()); + assert!(deserialized.miner_diagnostic_data.is_none()); + + let re_serialized = deserialized.serialize_to_vec(); + + assert_eq!(serialized, re_serialized); + + let roundtripped = BlockProposalData_v2::consensus_deserialize(&mut &re_serialized[..]) + .expect("BlockProposalData v2 should deserialize round-tripped data without error"); + + assert_eq!(original_data, roundtripped); +} diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 02eece8236..a323eeb42a 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +mod block_proposal_data; mod http; mod signer_state; diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index b39788ae2a..bfbcdfaae0 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -16,6 +16,7 @@ use std::cmp::Ordering; use std::fmt; +use std::io::{Read, Write}; use std::ops::{Deref, DerefMut, Index, IndexMut}; use std::str::FromStr; use std::sync::LazyLock; @@ -29,6 +30,7 @@ use crate::address::{ C32_ADDRESS_VERSION_MAINNET_MULTISIG, C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_MULTISIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG, }; +use crate::codec::{read_next, write_next, Error as CodecError, StacksMessageCodec}; use crate::consts::{ MICROSTACKS_PER_STACKS, PEER_VERSION_EPOCH_1_0, PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1, PEER_VERSION_EPOCH_2_2, @@ -1307,3 +1309,65 @@ impl DerefMut for EpochList { &mut self.0 } } + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Copy)] +pub enum MiningReason { + BlockFound = 0, + Extended = 1, + ReadCountExtend = 2, +} + +impl TryFrom for MiningReason { + type Error = CodecError; + + fn try_from(value: u8) -> Result { + match value { + x if x == MiningReason::BlockFound as u8 => Ok(MiningReason::BlockFound), + x if x == MiningReason::Extended as u8 => Ok(MiningReason::Extended), + x if x == MiningReason::ReadCountExtend as u8 => Ok(MiningReason::ReadCountExtend), + _ => Err(CodecError::DeserializeError(format!( + "unknown mining reason {value}" + ))), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MinerDiagnosticData { + pub burnchain_tip_height: u64, + pub burnchain_tip_consensus_hash: chainstate::ConsensusHash, + pub burnchain_tip_header_hash: chainstate::BurnchainHeaderHash, + pub tenure_extend_time_stamp: u64, + pub read_count_extend_timestamp: u64, + pub mining_reason: MiningReason, +} + +impl StacksMessageCodec for MinerDiagnosticData { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &self.burnchain_tip_height)?; + write_next(fd, &self.burnchain_tip_consensus_hash)?; + write_next(fd, &self.burnchain_tip_header_hash)?; + write_next(fd, &self.tenure_extend_time_stamp)?; + write_next(fd, &self.read_count_extend_timestamp)?; + write_next(fd, &(self.mining_reason as u8))?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let burnchain_tip_height = read_next(fd)?; + let burnchain_tip_consensus_hash = read_next(fd)?; + let burnchain_tip_header_hash = read_next(fd)?; + let tenure_extend_time_stamp = read_next(fd)?; + let read_count_extend_timestamp = read_next(fd)?; + let mining_reason = read_next::(fd)?.try_into()?; + + Ok(MinerDiagnosticData { + burnchain_tip_height, + burnchain_tip_consensus_hash, + burnchain_tip_header_hash, + tenure_extend_time_stamp, + read_count_extend_timestamp, + mining_reason, + }) + } +} diff --git a/stacks-node/src/nakamoto_node/miner.rs b/stacks-node/src/nakamoto_node/miner.rs index d5292b4faf..beb4316c08 100644 --- a/stacks-node/src/nakamoto_node/miner.rs +++ b/stacks-node/src/nakamoto_node/miner.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -47,6 +47,7 @@ use stacks::net::p2p::NetworkHandle; use stacks::net::stackerdb::StackerDBs; use stacks::net::{NakamotoBlocksData, StacksMessageType}; use stacks::types::chainstate::BlockHeaderHash; +use stacks::types::{MinerDiagnosticData, MiningReason}; use stacks::util::get_epoch_time_secs; use stacks::util::secp256k1::MessageSignature; #[cfg(test)] @@ -239,6 +240,16 @@ impl std::fmt::Display for MinerReason { } } +impl Into for MinerReason { + fn into(self) -> MiningReason { + match self { + Self::BlockFound { .. } => MiningReason::BlockFound, + Self::Extended { .. } => MiningReason::Extended, + Self::ReadCountExtend { .. } => MiningReason::ReadCountExtend, + } + } +} + pub struct BlockMinerThread { /// node config struct config: Config, @@ -958,6 +969,22 @@ impl BlockMinerThread { "Failed to open chainstate DB. Cannot mine! {e:?}" )) })?; + + let burn_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).map_err(|e| { + NakamotoNodeError::SigningCoordinatorFailure(format!( + "Failed to open sortition DB. Cannot mine! {e:?}" + )) + })?; + + let diagnostics = MinerDiagnosticData { + burnchain_tip_height: burn_tip.block_height, + burnchain_tip_consensus_hash: burn_tip.consensus_hash, + burnchain_tip_header_hash: burn_tip.burn_header_hash, + tenure_extend_time_stamp: coordinator.get_tenure_extend_timestamp(), + read_count_extend_timestamp: coordinator.get_read_count_extend_timestamp(), + mining_reason: self.reason.clone().into(), + }; + coordinator.propose_block( new_block, &self.burnchain, @@ -967,6 +994,7 @@ impl BlockMinerThread { &self.globals.counters, &self.burn_election_block, &self.miner_db, + diagnostics, ) } diff --git a/stacks-node/src/nakamoto_node/signer_coordinator.rs b/stacks-node/src/nakamoto_node/signer_coordinator.rs index 30c5e0d43b..6f8b30fd60 100644 --- a/stacks-node/src/nakamoto_node/signer_coordinator.rs +++ b/stacks-node/src/nakamoto_node/signer_coordinator.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Stacks Open Internet Foundation +// Copyright (C) 2024-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -34,6 +34,7 @@ use stacks::codec::StacksMessageCodec; use stacks::libstackerdb::StackerDBChunkData; use stacks::net::stackerdb::StackerDBs; use stacks::types::chainstate::{StacksBlockId, StacksPrivateKey, StacksPublicKey}; +use stacks::types::MinerDiagnosticData; use stacks::util::hash::Sha512Trunc256Sum; use stacks::util::secp256k1::MessageSignature; use stacks::util_lib::boot::boot_code_id; @@ -281,6 +282,7 @@ impl SignerCoordinator { counters: &Counters, election_sortition: &BlockSnapshot, miner_db: &MinerDB, + miner_diagnostic_data: MinerDiagnosticData, ) -> Result, NakamotoNodeError> { // Add this block to the block status map. self.stackerdb_comms.insert_block(&block.header); @@ -293,7 +295,7 @@ impl SignerCoordinator { block: block.clone(), burn_height: election_sortition.block_height, reward_cycle: reward_cycle_id, - block_proposal_data: BlockProposalData::from_current_version(), + block_proposal_data: BlockProposalData::from_current_version(miner_diagnostic_data), }; let block_proposal_message = SignerMessageV0::BlockProposal(block_proposal); diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 8e6ef77298..6ddd9d0239 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -92,6 +92,7 @@ use stacks::net::api::postblock_proposal::{ BlockValidateReject, BlockValidateResponse, NakamotoBlockProposal, ValidateRejectCode, }; use stacks::types::chainstate::{ConsensusHash, StacksBlockId}; +use stacks::types::{MinerDiagnosticData, MiningReason}; use stacks::util::hash::hex_bytes; use stacks::util_lib::boot::boot_code_id; use stacks::util_lib::signed_structured_data::pox4::{ @@ -509,7 +510,7 @@ pub fn blind_signer_multinode( pub fn get_latest_block_proposal( conf: &Config, sortdb: &SortitionDB, -) -> Result<(NakamotoBlock, StacksPublicKey), String> { +) -> Result<(NakamotoBlock, StacksPublicKey, Option), String> { let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); let (stackerdb_conf, miner_info) = NakamotoChainState::make_miners_stackerdb_config(sortdb, &tip) @@ -529,38 +530,48 @@ pub fn get_latest_block_proposal( .enumerate() .zip(miner_ranges) .filter_map(|((miner_ix, (miner_addr, _)), miner_slot_id)| { - let proposed_block = { + let (proposed_block, miner_diagnostic_data) = { let message: SignerMessageV0 = miners_stackerdb.get_latest(miner_slot_id.start).ok()??; let SignerMessageV0::BlockProposal(block_proposal) = message else { warn!("Expected a block proposal. Got {message:?}"); return None; }; - block_proposal.block + ( + block_proposal.block, + block_proposal.block_proposal_data.miner_diagnostic_data, + ) }; - Some((proposed_block, miner_addr, miner_ix == latest_miner)) + Some(( + proposed_block, + miner_addr, + miner_ix == latest_miner, + miner_diagnostic_data, + )) }) .collect(); - proposed_blocks.sort_by(|(block_a, _, is_latest_a), (block_b, _, is_latest_b)| { - let res = block_a - .header - .chain_length - .cmp(&block_b.header.chain_length); - if res != std::cmp::Ordering::Equal { - return res; - } - // the heights are tied, tie break with the latest miner - if *is_latest_a { - return std::cmp::Ordering::Greater; - } - if *is_latest_b { - return std::cmp::Ordering::Less; - } - std::cmp::Ordering::Equal - }); + proposed_blocks.sort_by( + |(block_a, _, is_latest_a, _), (block_b, _, is_latest_b, _)| { + let res = block_a + .header + .chain_length + .cmp(&block_b.header.chain_length); + if res != std::cmp::Ordering::Equal { + return res; + } + // the heights are tied, tie break with the latest miner + if *is_latest_a { + return std::cmp::Ordering::Greater; + } + if *is_latest_b { + return std::cmp::Ordering::Less; + } + std::cmp::Ordering::Equal + }, + ); - for (b, _, is_latest) in proposed_blocks.iter() { + for (b, _, is_latest, _) in proposed_blocks.iter() { info!("Consider block"; "signer_signature_hash" => %b.header.signer_signature_hash(), "is_latest_sortition" => is_latest, @@ -568,7 +579,7 @@ pub fn get_latest_block_proposal( ); } - let Some((proposed_block, miner_addr, _)) = proposed_blocks.pop() else { + let Some((proposed_block, miner_addr, _, miner_diagnostic_data)) = proposed_blocks.pop() else { return Err("No block proposals found".into()); }; @@ -586,7 +597,7 @@ pub fn get_latest_block_proposal( )); } - Ok((proposed_block, pubkey)) + Ok((proposed_block, pubkey, miner_diagnostic_data)) } pub fn read_and_sign_block_proposal( @@ -3611,10 +3622,10 @@ fn miner_writes_proposed_block_to_stackerdb() { next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); let sortdb = naka_conf.get_burnchain().open_sortition_db(true).unwrap(); + let burn_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()); - let proposed_block = get_latest_block_proposal(&naka_conf, &sortdb) - .expect("Expected to find a proposed block in the StackerDB") - .0; + let (proposed_block, _, miner_diagnostic_data) = get_latest_block_proposal(&naka_conf, &sortdb) + .expect("Expected to find a proposed block in the StackerDB"); let proposed_block_hash = format!("0x{}", proposed_block.header.block_hash()); let mut proposed_zero_block = proposed_block.clone(); @@ -3655,6 +3666,19 @@ fn miner_writes_proposed_block_to_stackerdb() { proposed_zero_block_hash, "Observed miner hash should match the proposed block read from StackerDB (after zeroing signatures)" ); + + let miner_diagnostic_data = miner_diagnostic_data + .expect("miner should have attached diagnostics data to block proposal"); + + assert_eq!( + miner_diagnostic_data.mining_reason, + MiningReason::BlockFound, + ); + + assert_eq!( + miner_diagnostic_data.burnchain_tip_height, + burn_tip.unwrap().block_height, + ); } #[test]