Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add versioned and backwards-compatible server version to block proposal #5803

Merged
merged 6 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
192 changes: 192 additions & 0 deletions libsigner/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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
Expand All @@ -69,24 +72,114 @@ 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 {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
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(())
}

fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
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<u8>,
}

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<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
write_next(fd, &self.server_version.as_bytes().to_vec())?;
// write_next(fd, &self.unknown_bytes)?;
hstove marked this conversation as resolved.
Show resolved Hide resolved
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<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(())
}

/// 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<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(),
})
}
}
Expand Down Expand Up @@ -534,6 +627,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]
Expand All @@ -551,4 +646,101 @@ 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<W: Write>(&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<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
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
);
// assert_eq!();
hstove marked this conversation as resolved.
Show resolved Hide resolved
}

#[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()
);
}
}
4 changes: 2 additions & 2 deletions libsigner/src/libsigner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
3 changes: 2 additions & 1 deletion libsigner/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,7 @@ mod test {
use stacks_common::types::chainstate::StacksPrivateKey;

use super::{StacksMessageCodecExtensions, *};
use crate::events::BlockProposalData;

#[test]
fn signer_slots_count_is_sane() {
Expand Down Expand Up @@ -1276,6 +1277,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();
Expand Down
3 changes: 2 additions & 1 deletion stacks-signer/src/signerdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,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;
Expand All @@ -1204,6 +1204,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)
Expand Down
4 changes: 3 additions & 1 deletion stacks-signer/src/tests/chainstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -244,6 +244,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);
Expand Down Expand Up @@ -511,6 +512,7 @@ fn check_sortition_timeout() {
},
burn_height: 2,
reward_cycle: 1,
block_proposal_data: BlockProposalData::empty(),
};

let mut block_info = BlockInfo::from(block_proposal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion testnet/stacks-node/src/tests/signer/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -405,6 +407,7 @@ impl SignerTest<SpawnedSigner> {
block,
burn_height,
reward_cycle,
block_proposal_data: BlockProposalData::empty(),
});
let miner_sk = self
.running_nodes
Expand Down
Loading