Skip to content

Commit d39ff29

Browse files
authored
Merge pull request #5803 from stacks-network/feat/add-version-to-block-proposal
feat: add versioned and backwards-compatible server version to block proposal
2 parents 4e859a4 + 5a76b47 commit d39ff29

File tree

9 files changed

+208
-8
lines changed

9 files changed

+208
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
99

1010
## Added
1111

12+
- 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))
1213
- Add `vrf_seed` to the `/v3/sortitions` rpc endpoint
1314

1415
### Changed

libsigner/src/events.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use blockstack_lib::net::api::postblock_proposal::{
3131
};
3232
use blockstack_lib::net::stackerdb::MINER_SLOT_COUNT;
3333
use blockstack_lib::util_lib::boot::boot_code_id;
34+
use blockstack_lib::version_string;
3435
use clarity::vm::types::serialization::SerializationError;
3536
use clarity::vm::types::QualifiedContractIdentifier;
3637
use serde::{Deserialize, Serialize};
@@ -45,11 +46,13 @@ use stacks_common::types::chainstate::{
4546
};
4647
use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
4748
use stacks_common::util::HexError;
49+
use stacks_common::versions::STACKS_NODE_VERSION;
4850
use tiny_http::{
4951
Method as HttpMethod, Request as HttpRequest, Response as HttpResponse, Server as HttpServer,
5052
};
5153

5254
use crate::http::{decode_http_body, decode_http_request};
55+
use crate::v0::messages::BLOCK_RESPONSE_DATA_MAX_SIZE;
5356
use crate::EventError;
5457

5558
/// Define the trait for the event processor
@@ -69,24 +72,113 @@ pub struct BlockProposal {
6972
pub burn_height: u64,
7073
/// The reward cycle the block is mined during
7174
pub reward_cycle: u64,
75+
/// Versioned and backwards-compatible block proposal data
76+
pub block_proposal_data: BlockProposalData,
7277
}
7378

7479
impl StacksMessageCodec for BlockProposal {
7580
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
7681
self.block.consensus_serialize(fd)?;
7782
self.burn_height.consensus_serialize(fd)?;
7883
self.reward_cycle.consensus_serialize(fd)?;
84+
self.block_proposal_data.consensus_serialize(fd)?;
7985
Ok(())
8086
}
8187

8288
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
8389
let block = NakamotoBlock::consensus_deserialize(fd)?;
8490
let burn_height = u64::consensus_deserialize(fd)?;
8591
let reward_cycle = u64::consensus_deserialize(fd)?;
92+
let block_proposal_data = BlockProposalData::consensus_deserialize(fd)?;
8693
Ok(BlockProposal {
8794
block,
8895
burn_height,
8996
reward_cycle,
97+
block_proposal_data,
98+
})
99+
}
100+
}
101+
102+
/// The latest version of the block response data
103+
pub const BLOCK_PROPOSAL_DATA_VERSION: u8 = 2;
104+
105+
/// Versioned, backwards-compatible struct for block response data
106+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
107+
pub struct BlockProposalData {
108+
/// The version of the block proposal data
109+
pub version: u8,
110+
/// The miner's server version
111+
pub server_version: String,
112+
/// When deserializing future versions,
113+
/// there may be extra bytes that we don't know about
114+
pub unknown_bytes: Vec<u8>,
115+
}
116+
117+
impl BlockProposalData {
118+
/// Create a new BlockProposalData for the provided server version and unknown bytes
119+
pub fn new(server_version: String) -> Self {
120+
Self {
121+
version: BLOCK_PROPOSAL_DATA_VERSION,
122+
server_version,
123+
unknown_bytes: vec![],
124+
}
125+
}
126+
127+
/// Create a new BlockProposalData with the current build's version
128+
pub fn from_current_version() -> Self {
129+
let server_version = version_string(
130+
"stacks-node",
131+
option_env!("STACKS_NODE_VERSION").or(Some(STACKS_NODE_VERSION)),
132+
);
133+
Self::new(server_version)
134+
}
135+
136+
/// Create an empty BlockProposalData
137+
pub fn empty() -> Self {
138+
Self::new(String::new())
139+
}
140+
141+
/// Serialize the "inner" block response data. Used to determine the bytes length of the serialized block response data
142+
fn inner_consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
143+
write_next(fd, &self.server_version.as_bytes().to_vec())?;
144+
fd.write_all(&self.unknown_bytes)
145+
.map_err(CodecError::WriteError)?;
146+
Ok(())
147+
}
148+
}
149+
150+
impl StacksMessageCodec for BlockProposalData {
151+
/// Serialize the block response data.
152+
/// When creating a new version of the block response data, we are only ever
153+
/// appending new bytes to the end of the struct. When serializing, we use
154+
/// `bytes_len` to ensure that older versions of the code can read through the
155+
/// end of the serialized bytes.
156+
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
157+
write_next(fd, &self.version)?;
158+
let mut inner_bytes = vec![];
159+
self.inner_consensus_serialize(&mut inner_bytes)?;
160+
write_next(fd, &inner_bytes)?;
161+
Ok(())
162+
}
163+
164+
/// Deserialize the block response data in a backwards-compatible manner.
165+
/// When creating a new version of the block response data, we are only ever
166+
/// appending new bytes to the end of the struct. When deserializing, we use
167+
/// `bytes_len` to ensure that we read through the end of the serialized bytes.
168+
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
169+
let Ok(version) = read_next(fd) else {
170+
return Ok(Self::empty());
171+
};
172+
let inner_bytes: Vec<u8> = read_next_at_most(fd, BLOCK_RESPONSE_DATA_MAX_SIZE)?;
173+
let mut inner_reader = inner_bytes.as_slice();
174+
let server_version: Vec<u8> = read_next(&mut inner_reader)?;
175+
let server_version = String::from_utf8(server_version).map_err(|e| {
176+
CodecError::DeserializeError(format!("Failed to decode server version: {:?}", &e))
177+
})?;
178+
Ok(Self {
179+
version,
180+
server_version,
181+
unknown_bytes: inner_reader.to_vec(),
90182
})
91183
}
92184
}
@@ -534,6 +626,8 @@ pub fn get_signers_db_signer_set_message_id(name: &str) -> Option<(u32, u32)> {
534626

535627
#[cfg(test)]
536628
mod tests {
629+
use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader;
630+
537631
use super::*;
538632

539633
#[test]
@@ -551,4 +645,100 @@ mod tests {
551645
let name = "signer--2";
552646
assert!(get_signers_db_signer_set_message_id(name).is_none());
553647
}
648+
649+
// Older version of BlockProposal to ensure backwards compatibility
650+
651+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
652+
/// BlockProposal sent to signers
653+
pub struct BlockProposalOld {
654+
/// The block itself
655+
pub block: NakamotoBlock,
656+
/// The burn height the block is mined during
657+
pub burn_height: u64,
658+
/// The reward cycle the block is mined during
659+
pub reward_cycle: u64,
660+
}
661+
662+
impl StacksMessageCodec for BlockProposalOld {
663+
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
664+
self.block.consensus_serialize(fd)?;
665+
self.burn_height.consensus_serialize(fd)?;
666+
self.reward_cycle.consensus_serialize(fd)?;
667+
Ok(())
668+
}
669+
670+
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
671+
let block = NakamotoBlock::consensus_deserialize(fd)?;
672+
let burn_height = u64::consensus_deserialize(fd)?;
673+
let reward_cycle = u64::consensus_deserialize(fd)?;
674+
Ok(BlockProposalOld {
675+
block,
676+
burn_height,
677+
reward_cycle,
678+
})
679+
}
680+
}
681+
682+
#[test]
683+
/// Test that the old version of the code can deserialize the new
684+
/// version without crashing.
685+
fn test_old_deserialization_works() {
686+
let header = NakamotoBlockHeader::empty();
687+
let block = NakamotoBlock {
688+
header,
689+
txs: vec![],
690+
};
691+
let new_block_proposal = BlockProposal {
692+
block: block.clone(),
693+
burn_height: 1,
694+
reward_cycle: 2,
695+
block_proposal_data: BlockProposalData::from_current_version(),
696+
};
697+
let mut bytes = vec![];
698+
new_block_proposal.consensus_serialize(&mut bytes).unwrap();
699+
let old_block_proposal =
700+
BlockProposalOld::consensus_deserialize(&mut bytes.as_slice()).unwrap();
701+
assert_eq!(old_block_proposal.block, block);
702+
assert_eq!(
703+
old_block_proposal.burn_height,
704+
new_block_proposal.burn_height
705+
);
706+
assert_eq!(
707+
old_block_proposal.reward_cycle,
708+
new_block_proposal.reward_cycle
709+
);
710+
}
711+
712+
#[test]
713+
/// Test that the old version of the code can be serialized
714+
/// and then deserialized into the new version.
715+
fn test_old_proposal_can_deserialize() {
716+
let header = NakamotoBlockHeader::empty();
717+
let block = NakamotoBlock {
718+
header,
719+
txs: vec![],
720+
};
721+
let old_block_proposal = BlockProposalOld {
722+
block: block.clone(),
723+
burn_height: 1,
724+
reward_cycle: 2,
725+
};
726+
let mut bytes = vec![];
727+
old_block_proposal.consensus_serialize(&mut bytes).unwrap();
728+
let new_block_proposal =
729+
BlockProposal::consensus_deserialize(&mut bytes.as_slice()).unwrap();
730+
assert_eq!(new_block_proposal.block, block);
731+
assert_eq!(
732+
new_block_proposal.burn_height,
733+
old_block_proposal.burn_height
734+
);
735+
assert_eq!(
736+
new_block_proposal.reward_cycle,
737+
old_block_proposal.reward_cycle
738+
);
739+
assert_eq!(
740+
new_block_proposal.block_proposal_data.server_version,
741+
String::new()
742+
);
743+
}
554744
}

libsigner/src/libsigner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ use stacks_common::versions::STACKS_SIGNER_VERSION;
5757

5858
pub use crate::error::{EventError, RPCError};
5959
pub use crate::events::{
60-
BlockProposal, EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver,
61-
SignerEventTrait, SignerStopSignaler,
60+
BlockProposal, BlockProposalData, EventReceiver, EventStopSignaler, SignerEvent,
61+
SignerEventReceiver, SignerEventTrait, SignerStopSignaler,
6262
};
6363
pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop};
6464
pub use crate::session::{SignerSession, StackerDBSession};

libsigner/src/tests/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use stacks_common::codec::{
4141
use stacks_common::util::secp256k1::Secp256k1PrivateKey;
4242
use stacks_common::util::sleep_ms;
4343

44-
use crate::events::{SignerEvent, SignerEventTrait};
44+
use crate::events::{BlockProposalData, SignerEvent, SignerEventTrait};
4545
use crate::v0::messages::{BlockRejection, SignerMessage};
4646
use crate::{BlockProposal, Signer, SignerEventReceiver, SignerRunLoop};
4747

@@ -126,6 +126,7 @@ fn test_simple_signer() {
126126
},
127127
burn_height: 2,
128128
reward_cycle: 1,
129+
block_proposal_data: BlockProposalData::empty(),
129130
};
130131
for i in 0..max_events {
131132
let privk = Secp256k1PrivateKey::random();

libsigner/src/v0/messages.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,6 @@ impl BlockResponseData {
828828
/// Serialize the "inner" block response data. Used to determine the bytes length of the serialized block response data
829829
fn inner_consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
830830
write_next(fd, &self.tenure_extend_timestamp)?;
831-
// write_next(fd, &self.unknown_bytes)?;
832831
fd.write_all(&self.unknown_bytes)
833832
.map_err(CodecError::WriteError)?;
834833
Ok(())
@@ -1160,6 +1159,7 @@ mod test {
11601159
use stacks_common::types::chainstate::StacksPrivateKey;
11611160

11621161
use super::{StacksMessageCodecExtensions, *};
1162+
use crate::events::BlockProposalData;
11631163

11641164
#[test]
11651165
fn signer_slots_count_is_sane() {
@@ -1276,6 +1276,7 @@ mod test {
12761276
block,
12771277
burn_height: thread_rng().next_u64(),
12781278
reward_cycle: thread_rng().next_u64(),
1279+
block_proposal_data: BlockProposalData::empty(),
12791280
};
12801281
let signer_message = SignerMessage::BlockProposal(block_proposal);
12811282
let serialized_signer_message = signer_message.serialize_to_vec();

stacks-signer/src/signerdb.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,7 +1231,7 @@ mod tests {
12311231
use clarity::types::chainstate::{StacksBlockId, StacksPrivateKey, StacksPublicKey};
12321232
use clarity::util::hash::Hash160;
12331233
use clarity::util::secp256k1::MessageSignature;
1234-
use libsigner::BlockProposal;
1234+
use libsigner::{BlockProposal, BlockProposalData};
12351235

12361236
use super::*;
12371237
use crate::signerdb::NakamotoBlockVote;
@@ -1254,6 +1254,7 @@ mod tests {
12541254
block,
12551255
burn_height: 7,
12561256
reward_cycle: 42,
1257+
block_proposal_data: BlockProposalData::empty(),
12571258
};
12581259
overrides(&mut block_proposal);
12591260
(BlockInfo::from(block_proposal.clone()), block_proposal)

stacks-signer/src/tests/chainstate.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use blockstack_lib::net::api::get_tenures_fork_info::TenureForkingInfo;
2929
use blockstack_lib::net::api::getsortition::SortitionInfo;
3030
use clarity::types::chainstate::{BurnchainHeaderHash, SortitionId};
3131
use clarity::util::vrf::VRFProof;
32-
use libsigner::BlockProposal;
32+
use libsigner::{BlockProposal, BlockProposalData};
3333
use slog::slog_info;
3434
use stacks_common::bitvec::BitVec;
3535
use stacks_common::consts::CHAIN_ID_TESTNET;
@@ -245,6 +245,7 @@ fn reorg_timing_testing(
245245
},
246246
burn_height: 2,
247247
reward_cycle: 1,
248+
block_proposal_data: BlockProposalData::empty(),
248249
};
249250
let mut header_clone = block_proposal_1.block.header.clone();
250251
let mut block_info_1 = BlockInfo::from(block_proposal_1);
@@ -512,6 +513,7 @@ fn check_sortition_timeout() {
512513
},
513514
burn_height: 2,
514515
reward_cycle: 1,
516+
block_proposal_data: BlockProposalData::empty(),
515517
};
516518

517519
let mut block_info = BlockInfo::from(block_proposal);

testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::thread::JoinHandle;
2121
use std::time::{Duration, Instant};
2222

2323
use libsigner::v0::messages::{MinerSlotID, SignerMessage as SignerMessageV0};
24-
use libsigner::{BlockProposal, SignerSession, StackerDBSession};
24+
use libsigner::{BlockProposal, BlockProposalData, SignerSession, StackerDBSession};
2525
use stacks::burnchains::Burnchain;
2626
use stacks::chainstate::burn::db::sortdb::SortitionDB;
2727
use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash};
@@ -250,6 +250,7 @@ impl SignerCoordinator {
250250
block: block.clone(),
251251
burn_height: election_sortition.block_height,
252252
reward_cycle: reward_cycle_id,
253+
block_proposal_data: BlockProposalData::from_current_version(),
253254
};
254255

255256
let block_proposal_message = SignerMessageV0::BlockProposal(block_proposal);

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use libsigner::v0::messages::{
2626
BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MinerSlotID, RejectCode,
2727
SignerMessage,
2828
};
29-
use libsigner::{BlockProposal, SignerSession, StackerDBSession, VERSION_STRING};
29+
use libsigner::{
30+
BlockProposal, BlockProposalData, SignerSession, StackerDBSession, VERSION_STRING,
31+
};
3032
use serde::Deserialize;
3133
use stacks::address::AddressHashMode;
3234
use stacks::burnchains::Txid;
@@ -407,6 +409,7 @@ impl SignerTest<SpawnedSigner> {
407409
block,
408410
burn_height,
409411
reward_cycle,
412+
block_proposal_data: BlockProposalData::empty(),
410413
});
411414
let miner_sk = self
412415
.running_nodes

0 commit comments

Comments
 (0)