diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a844b431..6d721c79f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ### Added +- The `BlockProposal` StackerDB message serialization struct now includes a `server_version` string, which represents the version of the node that the miner is using. ([#5803](https://github.com/stacks-network/stacks-core/pull/5803)) - Add `vrf_seed` to the `/v3/sortitions` rpc endpoint ### Changed diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 52a77e2bb8..68050639e5 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -31,6 +31,7 @@ use blockstack_lib::net::api::postblock_proposal::{ }; use blockstack_lib::net::stackerdb::MINER_SLOT_COUNT; use blockstack_lib::util_lib::boot::boot_code_id; +use blockstack_lib::version_string; use clarity::vm::types::serialization::SerializationError; use clarity::vm::types::QualifiedContractIdentifier; use serde::{Deserialize, Serialize}; @@ -45,11 +46,13 @@ use stacks_common::types::chainstate::{ }; use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum}; use stacks_common::util::HexError; +use stacks_common::versions::STACKS_NODE_VERSION; use tiny_http::{ Method as HttpMethod, Request as HttpRequest, Response as HttpResponse, Server as HttpServer, }; use crate::http::{decode_http_body, decode_http_request}; +use crate::v0::messages::BLOCK_RESPONSE_DATA_MAX_SIZE; use crate::EventError; /// Define the trait for the event processor @@ -69,6 +72,8 @@ pub struct BlockProposal { pub burn_height: u64, /// The reward cycle the block is mined during pub reward_cycle: u64, + /// Versioned and backwards-compatible block proposal data + pub block_proposal_data: BlockProposalData, } impl StacksMessageCodec for BlockProposal { @@ -76,6 +81,7 @@ impl StacksMessageCodec for BlockProposal { self.block.consensus_serialize(fd)?; self.burn_height.consensus_serialize(fd)?; self.reward_cycle.consensus_serialize(fd)?; + self.block_proposal_data.consensus_serialize(fd)?; Ok(()) } @@ -83,10 +89,96 @@ impl StacksMessageCodec for BlockProposal { let block = NakamotoBlock::consensus_deserialize(fd)?; let burn_height = u64::consensus_deserialize(fd)?; let reward_cycle = u64::consensus_deserialize(fd)?; + let block_proposal_data = BlockProposalData::consensus_deserialize(fd)?; Ok(BlockProposal { block, burn_height, reward_cycle, + block_proposal_data, + }) + } +} + +/// The latest version of the block response data +pub const BLOCK_PROPOSAL_DATA_VERSION: u8 = 2; + +/// Versioned, backwards-compatible struct for block response 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, + /// When deserializing future versions, + /// there may be extra bytes that we don't know about + pub unknown_bytes: Vec, +} + +impl BlockProposalData { + /// Create a new BlockProposalData for the provided server version and unknown bytes + pub fn new(server_version: String) -> Self { + Self { + version: BLOCK_PROPOSAL_DATA_VERSION, + server_version, + unknown_bytes: vec![], + } + } + + /// Create a new BlockProposalData with the current build's version + pub fn from_current_version() -> Self { + let server_version = version_string( + "stacks-node", + option_env!("STACKS_NODE_VERSION").or(Some(STACKS_NODE_VERSION)), + ); + Self::new(server_version) + } + + /// Create an empty BlockProposalData + pub fn empty() -> Self { + Self::new(String::new()) + } + + /// Serialize the "inner" block response data. Used to determine the bytes length of the serialized block response data + 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 { + /// Serialize the block response data. + /// When creating a new version of the block response data, we are only ever + /// appending new bytes to the end of the struct. When serializing, we use + /// `bytes_len` to ensure that older versions of the code can read through the + /// end of the serialized bytes. + 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(()) + } + + /// Deserialize the block response data in a backwards-compatible manner. + /// When creating a new version of the block response data, we are only ever + /// appending new bytes to the end of the struct. When deserializing, we use + /// `bytes_len` to ensure that we read through the end of the serialized bytes. + 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(), }) } } @@ -534,6 +626,8 @@ 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 super::*; #[test] @@ -551,4 +645,100 @@ mod tests { let name = "signer--2"; assert!(get_signers_db_signer_set_message_id(name).is_none()); } + + // Older version of BlockProposal to ensure backwards compatibility + + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + /// BlockProposal sent to signers + pub struct BlockProposalOld { + /// The block itself + pub block: NakamotoBlock, + /// The burn height the block is mined during + pub burn_height: u64, + /// The reward cycle the block is mined during + pub reward_cycle: u64, + } + + impl StacksMessageCodec for BlockProposalOld { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + self.block.consensus_serialize(fd)?; + self.burn_height.consensus_serialize(fd)?; + self.reward_cycle.consensus_serialize(fd)?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let block = NakamotoBlock::consensus_deserialize(fd)?; + let burn_height = u64::consensus_deserialize(fd)?; + let reward_cycle = u64::consensus_deserialize(fd)?; + Ok(BlockProposalOld { + block, + burn_height, + reward_cycle, + }) + } + } + + #[test] + /// Test that the old version of the code can deserialize the new + /// version without crashing. + fn test_old_deserialization_works() { + let header = NakamotoBlockHeader::empty(); + let block = NakamotoBlock { + header, + txs: vec![], + }; + let new_block_proposal = BlockProposal { + block: block.clone(), + burn_height: 1, + reward_cycle: 2, + block_proposal_data: BlockProposalData::from_current_version(), + }; + let mut bytes = vec![]; + new_block_proposal.consensus_serialize(&mut bytes).unwrap(); + let old_block_proposal = + BlockProposalOld::consensus_deserialize(&mut bytes.as_slice()).unwrap(); + assert_eq!(old_block_proposal.block, block); + assert_eq!( + old_block_proposal.burn_height, + new_block_proposal.burn_height + ); + assert_eq!( + old_block_proposal.reward_cycle, + new_block_proposal.reward_cycle + ); + } + + #[test] + /// Test that the old version of the code can be serialized + /// and then deserialized into the new version. + fn test_old_proposal_can_deserialize() { + let header = NakamotoBlockHeader::empty(); + let block = NakamotoBlock { + header, + txs: vec![], + }; + let old_block_proposal = BlockProposalOld { + block: block.clone(), + burn_height: 1, + reward_cycle: 2, + }; + let mut bytes = vec![]; + old_block_proposal.consensus_serialize(&mut bytes).unwrap(); + let new_block_proposal = + BlockProposal::consensus_deserialize(&mut bytes.as_slice()).unwrap(); + assert_eq!(new_block_proposal.block, block); + assert_eq!( + new_block_proposal.burn_height, + old_block_proposal.burn_height + ); + assert_eq!( + new_block_proposal.reward_cycle, + old_block_proposal.reward_cycle + ); + assert_eq!( + new_block_proposal.block_proposal_data.server_version, + String::new() + ); + } } diff --git a/libsigner/src/libsigner.rs b/libsigner/src/libsigner.rs index 90cee5e4c3..44f9c4f414 100644 --- a/libsigner/src/libsigner.rs +++ b/libsigner/src/libsigner.rs @@ -57,8 +57,8 @@ use stacks_common::versions::STACKS_SIGNER_VERSION; pub use crate::error::{EventError, RPCError}; pub use crate::events::{ - BlockProposal, EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, - SignerEventTrait, SignerStopSignaler, + BlockProposal, BlockProposalData, EventReceiver, EventStopSignaler, SignerEvent, + SignerEventReceiver, SignerEventTrait, SignerStopSignaler, }; pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop}; pub use crate::session::{SignerSession, StackerDBSession}; diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 9c04eb09ad..b94eb7608a 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -41,7 +41,7 @@ use stacks_common::codec::{ use stacks_common::util::secp256k1::Secp256k1PrivateKey; use stacks_common::util::sleep_ms; -use crate::events::{SignerEvent, SignerEventTrait}; +use crate::events::{BlockProposalData, SignerEvent, SignerEventTrait}; use crate::v0::messages::{BlockRejection, SignerMessage}; use crate::{BlockProposal, Signer, SignerEventReceiver, SignerRunLoop}; @@ -126,6 +126,7 @@ fn test_simple_signer() { }, burn_height: 2, reward_cycle: 1, + block_proposal_data: BlockProposalData::empty(), }; for i in 0..max_events { let privk = Secp256k1PrivateKey::random(); diff --git a/libsigner/src/v0/messages.rs b/libsigner/src/v0/messages.rs index 7d2daf560a..12a279e1fe 100644 --- a/libsigner/src/v0/messages.rs +++ b/libsigner/src/v0/messages.rs @@ -828,7 +828,6 @@ impl BlockResponseData { /// Serialize the "inner" block response data. Used to determine the bytes length of the serialized block response data fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { write_next(fd, &self.tenure_extend_timestamp)?; - // write_next(fd, &self.unknown_bytes)?; fd.write_all(&self.unknown_bytes) .map_err(CodecError::WriteError)?; Ok(()) @@ -1160,6 +1159,7 @@ mod test { use stacks_common::types::chainstate::StacksPrivateKey; use super::{StacksMessageCodecExtensions, *}; + use crate::events::BlockProposalData; #[test] fn signer_slots_count_is_sane() { @@ -1276,6 +1276,7 @@ mod test { block, burn_height: thread_rng().next_u64(), reward_cycle: thread_rng().next_u64(), + block_proposal_data: BlockProposalData::empty(), }; let signer_message = SignerMessage::BlockProposal(block_proposal); let serialized_signer_message = signer_message.serialize_to_vec(); diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index 8cd4f93ed6..9a91a1c219 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -1231,7 +1231,7 @@ mod tests { use clarity::types::chainstate::{StacksBlockId, StacksPrivateKey, StacksPublicKey}; use clarity::util::hash::Hash160; use clarity::util::secp256k1::MessageSignature; - use libsigner::BlockProposal; + use libsigner::{BlockProposal, BlockProposalData}; use super::*; use crate::signerdb::NakamotoBlockVote; @@ -1254,6 +1254,7 @@ mod tests { block, burn_height: 7, reward_cycle: 42, + block_proposal_data: BlockProposalData::empty(), }; overrides(&mut block_proposal); (BlockInfo::from(block_proposal.clone()), block_proposal) diff --git a/stacks-signer/src/tests/chainstate.rs b/stacks-signer/src/tests/chainstate.rs index b99536c724..b39da1304d 100644 --- a/stacks-signer/src/tests/chainstate.rs +++ b/stacks-signer/src/tests/chainstate.rs @@ -29,7 +29,7 @@ use blockstack_lib::net::api::get_tenures_fork_info::TenureForkingInfo; use blockstack_lib::net::api::getsortition::SortitionInfo; use clarity::types::chainstate::{BurnchainHeaderHash, SortitionId}; use clarity::util::vrf::VRFProof; -use libsigner::BlockProposal; +use libsigner::{BlockProposal, BlockProposalData}; use slog::slog_info; use stacks_common::bitvec::BitVec; use stacks_common::consts::CHAIN_ID_TESTNET; @@ -245,6 +245,7 @@ fn reorg_timing_testing( }, burn_height: 2, reward_cycle: 1, + block_proposal_data: BlockProposalData::empty(), }; let mut header_clone = block_proposal_1.block.header.clone(); let mut block_info_1 = BlockInfo::from(block_proposal_1); @@ -512,6 +513,7 @@ fn check_sortition_timeout() { }, burn_height: 2, reward_cycle: 1, + block_proposal_data: BlockProposalData::empty(), }; let mut block_info = BlockInfo::from(block_proposal); diff --git a/testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs b/testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs index 0dcbfa04ad..69a240fd1e 100644 --- a/testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs +++ b/testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs @@ -21,7 +21,7 @@ use std::thread::JoinHandle; use std::time::{Duration, Instant}; use libsigner::v0::messages::{MinerSlotID, SignerMessage as SignerMessageV0}; -use libsigner::{BlockProposal, SignerSession, StackerDBSession}; +use libsigner::{BlockProposal, BlockProposalData, SignerSession, StackerDBSession}; use stacks::burnchains::Burnchain; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; @@ -250,6 +250,7 @@ impl SignerCoordinator { block: block.clone(), burn_height: election_sortition.block_height, reward_cycle: reward_cycle_id, + block_proposal_data: BlockProposalData::from_current_version(), }; let block_proposal_message = SignerMessageV0::BlockProposal(block_proposal); diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index 7388349ca6..d74ddb0aac 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -26,7 +26,9 @@ use libsigner::v0::messages::{ BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MinerSlotID, RejectCode, SignerMessage, }; -use libsigner::{BlockProposal, SignerSession, StackerDBSession, VERSION_STRING}; +use libsigner::{ + BlockProposal, BlockProposalData, SignerSession, StackerDBSession, VERSION_STRING, +}; use serde::Deserialize; use stacks::address::AddressHashMode; use stacks::burnchains::Txid; @@ -407,6 +409,7 @@ impl SignerTest { block, burn_height, reward_cycle, + block_proposal_data: BlockProposalData::empty(), }); let miner_sk = self .running_nodes