13
13
// You should have received a copy of the GNU General Public License
14
14
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
15
16
+ use std:: time:: Duration ;
17
+
16
18
use blockstack_lib:: chainstate:: nakamoto:: NakamotoBlock ;
17
19
use blockstack_lib:: chainstate:: stacks:: TenureChangePayload ;
18
20
use blockstack_lib:: net:: api:: getsortition:: SortitionInfo ;
21
+ use blockstack_lib:: util_lib:: db:: Error as DBError ;
22
+ use clarity:: types:: chainstate:: BurnchainHeaderHash ;
19
23
use slog:: { slog_info, slog_warn} ;
20
24
use stacks_common:: types:: chainstate:: { ConsensusHash , StacksPublicKey } ;
21
25
use stacks_common:: util:: hash:: Hash160 ;
22
26
use stacks_common:: { info, warn} ;
23
27
24
28
use crate :: client:: { ClientError , StacksClient } ;
29
+ use crate :: config:: SignerConfig ;
25
30
use crate :: signerdb:: SignerDb ;
26
31
32
+ #[ derive( thiserror:: Error , Debug ) ]
33
+ /// Error type for the signer chainstate module
34
+ pub enum SignerChainstateError {
35
+ /// Error resulting from database interactions
36
+ #[ error( "Database error: {0}" ) ]
37
+ DBError ( #[ from] DBError ) ,
38
+ /// Error resulting from crate::client interactions
39
+ #[ error( "Client error: {0}" ) ]
40
+ ClientError ( #[ from] ClientError ) ,
41
+ }
42
+
27
43
/// Captures this signer's current view of a sortition's miner.
28
44
#[ derive( PartialEq , Eq , Debug ) ]
29
45
pub enum SortitionMinerStatus {
@@ -56,6 +72,26 @@ pub struct SortitionState {
56
72
pub consensus_hash : ConsensusHash ,
57
73
/// what is this signer's view of the this sortition's miner? did they misbehave?
58
74
pub miner_status : SortitionMinerStatus ,
75
+ /// the timestamp in the burn block that performed this sortition
76
+ pub burn_header_timestamp : u64 ,
77
+ /// the burn header hash of the burn block that performed this sortition
78
+ pub burn_block_hash : BurnchainHeaderHash ,
79
+ }
80
+
81
+ /// Captures the configuration settings used by the signer when evaluating block proposals.
82
+ #[ derive( Debug , Clone ) ]
83
+ pub struct ProposalEvalConfig {
84
+ /// How much time must pass between the first block proposal in a tenure and the next bitcoin block
85
+ /// before a subsequent miner isn't allowed to reorg the tenure
86
+ pub first_proposal_burn_block_timing : Duration ,
87
+ }
88
+
89
+ impl From < & SignerConfig > for ProposalEvalConfig {
90
+ fn from ( value : & SignerConfig ) -> Self {
91
+ Self {
92
+ first_proposal_burn_block_timing : value. first_proposal_burn_block_timing . clone ( ) ,
93
+ }
94
+ }
59
95
}
60
96
61
97
/// The signer's current view of the stacks chain's sortition
@@ -68,6 +104,8 @@ pub struct SortitionsView {
68
104
pub cur_sortition : SortitionState ,
69
105
/// the hash at which the sortitions view was fetched
70
106
pub latest_consensus_hash : ConsensusHash ,
107
+ /// configuration settings for evaluating proposals
108
+ pub config : ProposalEvalConfig ,
71
109
}
72
110
73
111
impl TryFrom < SortitionInfo > for SortitionState {
@@ -85,6 +123,8 @@ impl TryFrom<SortitionInfo> for SortitionState {
85
123
parent_tenure_id : value
86
124
. stacks_parent_ch
87
125
. ok_or_else ( || ClientError :: UnexpectedSortitionInfo ) ?,
126
+ burn_header_timestamp : value. burn_header_timestamp ,
127
+ burn_block_hash : value. burn_block_hash ,
88
128
miner_status : SortitionMinerStatus :: Valid ,
89
129
} )
90
130
}
@@ -112,7 +152,7 @@ impl SortitionsView {
112
152
signer_db : & SignerDb ,
113
153
block : & NakamotoBlock ,
114
154
block_pk : & StacksPublicKey ,
115
- ) -> Result < bool , ClientError > {
155
+ ) -> Result < bool , SignerChainstateError > {
116
156
let bitvec_all_1s = block. header . pox_treatment . iter ( ) . all ( |entry| entry) ;
117
157
if !bitvec_all_1s {
118
158
warn ! (
@@ -203,8 +243,13 @@ impl SortitionsView {
203
243
return Ok ( false ) ;
204
244
}
205
245
// now, we have to check if the parent tenure was a valid choice.
206
- let is_valid_parent_tenure =
207
- Self :: check_parent_tenure_choice ( proposed_by. state ( ) , block, client) ?;
246
+ let is_valid_parent_tenure = Self :: check_parent_tenure_choice (
247
+ proposed_by. state ( ) ,
248
+ block,
249
+ signer_db,
250
+ client,
251
+ & self . config . first_proposal_burn_block_timing ,
252
+ ) ?;
208
253
if !is_valid_parent_tenure {
209
254
return Ok ( false ) ;
210
255
}
@@ -251,8 +296,10 @@ impl SortitionsView {
251
296
fn check_parent_tenure_choice (
252
297
sortition_state : & SortitionState ,
253
298
block : & NakamotoBlock ,
299
+ signer_db : & SignerDb ,
254
300
client : & StacksClient ,
255
- ) -> Result < bool , ClientError > {
301
+ first_proposal_burn_block_timing : & Duration ,
302
+ ) -> Result < bool , SignerChainstateError > {
256
303
// if the parent tenure is the last sortition, it is a valid choice.
257
304
// if the parent tenure is a reorg, then all of the reorged sortitions
258
305
// must either have produced zero blocks _or_ produced their first block
@@ -264,6 +311,9 @@ impl SortitionsView {
264
311
"Most recent miner's tenure does not build off the prior sortition, checking if this is valid behavior" ;
265
312
"proposed_block_consensus_hash" => %block. header. consensus_hash,
266
313
"proposed_block_signer_sighash" => %block. header. signer_signature_hash( ) ,
314
+ "sortition_state.consensus_hash" => %sortition_state. consensus_hash,
315
+ "sortition_state.prior_sortition" => %sortition_state. prior_sortition,
316
+ "sortition_state.parent_tenure_id" => %sortition_state. parent_tenure_id,
267
317
) ;
268
318
269
319
let tenures_reorged = client. get_tenure_forking_info (
@@ -277,9 +327,67 @@ impl SortitionsView {
277
327
) ;
278
328
return Ok ( false ) ;
279
329
}
330
+
331
+ // this value *should* always be some, but try to do the best we can if it isn't
332
+ let sortition_state_received_time =
333
+ signer_db. get_burn_block_receive_time ( & sortition_state. burn_block_hash ) ?;
334
+
280
335
for tenure in tenures_reorged. iter ( ) {
336
+ if tenure. consensus_hash == sortition_state. parent_tenure_id {
337
+ // this was a built-upon tenure, no need to check this tenure as part of the reorg.
338
+ continue ;
339
+ }
340
+
281
341
if tenure. first_block_mined . is_some ( ) {
282
- // TODO: must check if the first block was poorly timed.
342
+ let Some ( local_block_info) =
343
+ signer_db. get_first_signed_block_in_tenure ( & tenure. consensus_hash ) ?
344
+ else {
345
+ warn ! (
346
+ "Miner is not building off of most recent tenure, but a tenure they attempted to reorg has already mined blocks, and there is no local knowledge for that tenure's block timing." ;
347
+ "proposed_block_consensus_hash" => %block. header. consensus_hash,
348
+ "proposed_block_signer_sighash" => %block. header. signer_signature_hash( ) ,
349
+ "parent_tenure" => %sortition_state. parent_tenure_id,
350
+ "last_sortition" => %sortition_state. prior_sortition,
351
+ "violating_tenure_id" => %tenure. consensus_hash,
352
+ "violating_tenure_first_block_id" => ?tenure. first_block_mined,
353
+ ) ;
354
+ return Ok ( false ) ;
355
+ } ;
356
+
357
+ let checked_proposal_timing = if let Some ( sortition_state_received_time) =
358
+ sortition_state_received_time
359
+ {
360
+ // how long was there between when the proposal was received and the next sortition started?
361
+ let proposal_to_sortition = if let Some ( signed_at) =
362
+ local_block_info. signed_self
363
+ {
364
+ sortition_state_received_time. saturating_sub ( signed_at)
365
+ } else {
366
+ info ! ( "We did not sign over the reorged tenure's first block, considering it as a late-arriving proposal" ) ;
367
+ 0
368
+ } ;
369
+ if Duration :: from_secs ( proposal_to_sortition)
370
+ <= * first_proposal_burn_block_timing
371
+ {
372
+ info ! (
373
+ "Miner is not building off of most recent tenure. A tenure they reorg has already mined blocks, but the block was poorly timed, allowing the reorg." ;
374
+ "proposed_block_consensus_hash" => %block. header. consensus_hash,
375
+ "proposed_block_signer_sighash" => %block. header. signer_signature_hash( ) ,
376
+ "parent_tenure" => %sortition_state. parent_tenure_id,
377
+ "last_sortition" => %sortition_state. prior_sortition,
378
+ "violating_tenure_id" => %tenure. consensus_hash,
379
+ "violating_tenure_first_block_id" => ?tenure. first_block_mined,
380
+ "violating_tenure_proposed_time" => local_block_info. proposed_time,
381
+ "new_tenure_received_time" => sortition_state_received_time,
382
+ "new_tenure_burn_timestamp" => sortition_state. burn_header_timestamp,
383
+ ) ;
384
+ continue ;
385
+ }
386
+ true
387
+ } else {
388
+ false
389
+ } ;
390
+
283
391
warn ! (
284
392
"Miner is not building off of most recent tenure, but a tenure they attempted to reorg has already mined blocks." ;
285
393
"proposed_block_consensus_hash" => %block. header. consensus_hash,
@@ -288,6 +396,7 @@ impl SortitionsView {
288
396
"last_sortition" => %sortition_state. prior_sortition,
289
397
"violating_tenure_id" => %tenure. consensus_hash,
290
398
"violating_tenure_first_block_id" => ?tenure. first_block_mined,
399
+ "checked_proposal_timing" => checked_proposal_timing,
291
400
) ;
292
401
return Ok ( false ) ;
293
402
}
@@ -346,7 +455,10 @@ impl SortitionsView {
346
455
}
347
456
348
457
/// Fetch a new view of the recent sortitions
349
- pub fn fetch_view ( client : & StacksClient ) -> Result < Self , ClientError > {
458
+ pub fn fetch_view (
459
+ config : ProposalEvalConfig ,
460
+ client : & StacksClient ,
461
+ ) -> Result < Self , ClientError > {
350
462
let latest_state = client. get_latest_sortition ( ) ?;
351
463
let latest_ch = latest_state. consensus_hash ;
352
464
@@ -383,6 +495,7 @@ impl SortitionsView {
383
495
cur_sortition,
384
496
last_sortition,
385
497
latest_consensus_hash,
498
+ config,
386
499
} )
387
500
}
388
501
}
0 commit comments