@@ -31,6 +31,7 @@ use blockstack_lib::net::api::postblock_proposal::{
31
31
} ;
32
32
use blockstack_lib:: net:: stackerdb:: MINER_SLOT_COUNT ;
33
33
use blockstack_lib:: util_lib:: boot:: boot_code_id;
34
+ use blockstack_lib:: version_string;
34
35
use clarity:: vm:: types:: serialization:: SerializationError ;
35
36
use clarity:: vm:: types:: QualifiedContractIdentifier ;
36
37
use serde:: { Deserialize , Serialize } ;
@@ -45,11 +46,13 @@ use stacks_common::types::chainstate::{
45
46
} ;
46
47
use stacks_common:: util:: hash:: { Hash160 , Sha512Trunc256Sum } ;
47
48
use stacks_common:: util:: HexError ;
49
+ use stacks_common:: versions:: STACKS_NODE_VERSION ;
48
50
use tiny_http:: {
49
51
Method as HttpMethod , Request as HttpRequest , Response as HttpResponse , Server as HttpServer ,
50
52
} ;
51
53
52
54
use crate :: http:: { decode_http_body, decode_http_request} ;
55
+ use crate :: v0:: messages:: BLOCK_RESPONSE_DATA_MAX_SIZE ;
53
56
use crate :: EventError ;
54
57
55
58
/// Define the trait for the event processor
@@ -69,24 +72,113 @@ pub struct BlockProposal {
69
72
pub burn_height : u64 ,
70
73
/// The reward cycle the block is mined during
71
74
pub reward_cycle : u64 ,
75
+ /// Versioned and backwards-compatible block proposal data
76
+ pub block_proposal_data : BlockProposalData ,
72
77
}
73
78
74
79
impl StacksMessageCodec for BlockProposal {
75
80
fn consensus_serialize < W : Write > ( & self , fd : & mut W ) -> Result < ( ) , CodecError > {
76
81
self . block . consensus_serialize ( fd) ?;
77
82
self . burn_height . consensus_serialize ( fd) ?;
78
83
self . reward_cycle . consensus_serialize ( fd) ?;
84
+ self . block_proposal_data . consensus_serialize ( fd) ?;
79
85
Ok ( ( ) )
80
86
}
81
87
82
88
fn consensus_deserialize < R : Read > ( fd : & mut R ) -> Result < Self , CodecError > {
83
89
let block = NakamotoBlock :: consensus_deserialize ( fd) ?;
84
90
let burn_height = u64:: consensus_deserialize ( fd) ?;
85
91
let reward_cycle = u64:: consensus_deserialize ( fd) ?;
92
+ let block_proposal_data = BlockProposalData :: consensus_deserialize ( fd) ?;
86
93
Ok ( BlockProposal {
87
94
block,
88
95
burn_height,
89
96
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 ( ) ,
90
182
} )
91
183
}
92
184
}
@@ -534,6 +626,8 @@ pub fn get_signers_db_signer_set_message_id(name: &str) -> Option<(u32, u32)> {
534
626
535
627
#[ cfg( test) ]
536
628
mod tests {
629
+ use blockstack_lib:: chainstate:: nakamoto:: NakamotoBlockHeader ;
630
+
537
631
use super :: * ;
538
632
539
633
#[ test]
@@ -551,4 +645,100 @@ mod tests {
551
645
let name = "signer--2" ;
552
646
assert ! ( get_signers_db_signer_set_message_id( name) . is_none( ) ) ;
553
647
}
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
+ }
554
744
}
0 commit comments