Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6989-miner-diagnostics.added
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 49 additions & 9 deletions libsigner/src/events.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -91,47 +92,66 @@ 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<MinerDiagnosticData>,

/// When deserializing future versions,
/// there may be extra bytes that we don't know about
pub unknown_bytes: Vec<u8>,
}

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<W: Write>(&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(())
Expand Down Expand Up @@ -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(),
})
}
Expand Down Expand Up @@ -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::*;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
}
}
}
145 changes: 145 additions & 0 deletions libsigner/src/tests/block_proposal_data.rs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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<u8>,
}

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<W: Write>(&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<W: Write>(&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<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
let Ok(version) = read_next(fd) else {
return Ok(Self::empty());
};
let inner_bytes: Vec<u8> = read_next_at_most(fd, BLOCK_RESPONSE_DATA_MAX_SIZE)?;
let mut inner_reader = inner_bytes.as_slice();
let server_version: Vec<u8> = 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);
}
3 changes: 2 additions & 1 deletion libsigner/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

mod block_proposal_data;
mod http;
mod signer_state;

Expand Down
64 changes: 64 additions & 0 deletions stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -1307,3 +1309,65 @@ impl<L: Clone> DerefMut for EpochList<L> {
&mut self.0
}
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Copy)]
pub enum MiningReason {
BlockFound = 0,
Extended = 1,
ReadCountExtend = 2,
}

impl TryFrom<u8> for MiningReason {
type Error = CodecError;

fn try_from(value: u8) -> Result<Self, CodecError> {
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<W: Write>(&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<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
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::<u8, _>(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,
})
}
}
Loading