Skip to content

Commit 5002ef6

Browse files
committed
add miner inactivity handling to local state machine, lots of test assertions
1 parent 3a25b27 commit 5002ef6

File tree

8 files changed

+785
-441
lines changed

8 files changed

+785
-441
lines changed

libsigner/src/events.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use blockstack_lib::net::api::postblock_proposal::{
3232
use blockstack_lib::net::stackerdb::MINER_SLOT_COUNT;
3333
use blockstack_lib::util_lib::boot::boot_code_id;
3434
use blockstack_lib::version_string;
35+
use clarity::types::chainstate::StacksBlockId;
3536
use clarity::vm::types::serialization::SerializationError;
3637
use clarity::vm::types::QualifiedContractIdentifier;
3738
use serde::{Deserialize, Serialize};
@@ -202,13 +203,20 @@ pub enum SignerEvent<T: SignerEventTrait> {
202203
burn_height: u64,
203204
/// the burn hash for the newly processed burn block
204205
burn_header_hash: BurnchainHeaderHash,
206+
/// the consensus hash for the newly processed burn block
207+
consensus_hash: ConsensusHash,
205208
/// the time at which this event was received by the signer's event processor
206209
received_time: SystemTime,
207210
},
208211
/// A new processed Stacks block was received from the node with the given block hash
209212
NewBlock {
210-
/// The block header hash for the newly processed stacks block
211-
block_hash: Sha512Trunc256Sum,
213+
/// The stacks block ID (or index block hash) of the new block
214+
block_id: StacksBlockId,
215+
/// The consensus hash of the block (either the tenure it was produced during for Stacks 3.0
216+
/// or the burn block that won the sortition in Stacks 2.0)
217+
consensus_hash: ConsensusHash,
218+
/// The signer sighash for the newly processed stacks block
219+
signer_sighash: Sha512Trunc256Sum,
212220
/// The block height for the newly processed stacks block
213221
block_height: u64,
214222
},
@@ -556,6 +564,7 @@ struct BurnBlockEvent {
556564
reward_recipients: Vec<serde_json::Value>,
557565
reward_slot_holders: Vec<String>,
558566
burn_amount: u64,
567+
consensus_hash: String,
559568
}
560569

561570
impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
@@ -571,34 +580,65 @@ impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
571580
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
572581
})?;
573582

583+
let consensus_hash = burn_block_event
584+
.consensus_hash
585+
.get(2..)
586+
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
587+
.and_then(|hex| {
588+
ConsensusHash::from_hex(hex)
589+
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
590+
})?;
591+
574592
Ok(SignerEvent::NewBurnBlock {
575593
burn_height: burn_block_event.burn_block_height,
576594
received_time: SystemTime::now(),
577595
burn_header_hash,
596+
consensus_hash,
578597
})
579598
}
580599
}
581600

582601
#[derive(Debug, Deserialize)]
583602
struct BlockEvent {
584-
block_hash: String,
603+
index_block_hash: String,
604+
signer_signature_hash: String,
605+
consensus_hash: String,
585606
block_height: u64,
586607
}
587608

588609
impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
589610
type Error = EventError;
590611

591612
fn try_from(block_event: BlockEvent) -> Result<Self, Self::Error> {
592-
let block_hash: Sha512Trunc256Sum = block_event
593-
.block_hash
613+
let signer_sighash = block_event
614+
.signer_signature_hash
594615
.get(2..)
595616
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
596617
.and_then(|hex| {
597618
Sha512Trunc256Sum::from_hex(hex)
598619
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
599620
})?;
621+
let consensus_hash = block_event
622+
.consensus_hash
623+
.get(2..)
624+
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
625+
.and_then(|hex| {
626+
ConsensusHash::from_hex(hex)
627+
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
628+
})?;
629+
let block_id = block_event
630+
.index_block_hash
631+
.get(2..)
632+
.ok_or_else(|| EventError::Deserialize("Hex string should be 0x prefixed".into()))
633+
.and_then(|hex| {
634+
StacksBlockId::from_hex(hex)
635+
.map_err(|e| EventError::Deserialize(format!("Invalid hex string: {e}")))
636+
})?;
637+
600638
Ok(SignerEvent::NewBlock {
601-
block_hash,
639+
block_id,
640+
signer_sighash,
641+
consensus_hash,
602642
block_height: block_event.block_height,
603643
})
604644
}

stacks-signer/src/runloop.rs

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -500,37 +500,6 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug>
500500
"Running one pass for the signer. state={:?}, event={event:?}",
501501
self.state
502502
);
503-
// This is the only event that we respond to from the outer signer runloop
504-
if let Some(SignerEvent::StatusCheck) = event {
505-
info!("Signer status check requested: {:?}.", self.state);
506-
if let Err(e) = res.send(vec![StateInfo {
507-
runloop_state: self.state,
508-
reward_cycle_info: self.current_reward_cycle_info,
509-
running_signers: self
510-
.stacks_signers
511-
.values()
512-
.map(|s| s.reward_cycle())
513-
.collect(),
514-
signer_state_machines: self
515-
.stacks_signers
516-
.iter()
517-
.map(|(reward_cycle, signer)| {
518-
let ConfiguredSigner::RegisteredSigner(ref signer) = signer else {
519-
return (*reward_cycle, None);
520-
};
521-
(
522-
*reward_cycle,
523-
Some(signer.get_local_state_machine().clone()),
524-
)
525-
})
526-
.collect(),
527-
}
528-
.into()])
529-
{
530-
error!("Failed to send status check result: {e}.");
531-
}
532-
}
533-
534503
if self.state == State::Uninitialized {
535504
if let Err(e) = self.initialize_runloop() {
536505
error!("Failed to initialize signer runloop: {e}.");
@@ -564,6 +533,38 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug>
564533
current_reward_cycle,
565534
);
566535
}
536+
537+
// This is the only event that we respond to from the outer signer runloop
538+
if let Some(SignerEvent::StatusCheck) = event {
539+
info!("Signer status check requested: {:?}.", self.state);
540+
if let Err(e) = res.send(vec![StateInfo {
541+
runloop_state: self.state,
542+
reward_cycle_info: self.current_reward_cycle_info,
543+
running_signers: self
544+
.stacks_signers
545+
.values()
546+
.map(|s| s.reward_cycle())
547+
.collect(),
548+
signer_state_machines: self
549+
.stacks_signers
550+
.iter()
551+
.map(|(reward_cycle, signer)| {
552+
let ConfiguredSigner::RegisteredSigner(ref signer) = signer else {
553+
return (*reward_cycle, None);
554+
};
555+
(
556+
*reward_cycle,
557+
Some(signer.get_local_state_machine().clone()),
558+
)
559+
})
560+
.collect(),
561+
}
562+
.into()])
563+
{
564+
error!("Failed to send status check result: {e}.");
565+
}
566+
}
567+
567568
if self.state == State::NoRegisteredSigners && event.is_some() {
568569
let next_reward_cycle = current_reward_cycle.saturating_add(1);
569570
info!("Signer is not registered for the current reward cycle ({current_reward_cycle}). Reward set is not yet determined or signer is not registered for the upcoming reward cycle ({next_reward_cycle}).");

stacks-signer/src/signerdb.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,15 @@ ALTER TABLE block_rejection_signer_addrs
510510
ADD COLUMN reject_code INTEGER;
511511
"#;
512512

513+
static ADD_CONSENSUS_HASH: &str = r#"
514+
ALTER TABLE burn_blocks
515+
ADD COLUMN consensus_hash TEXT;
516+
"#;
517+
518+
static ADD_CONSENSUS_HASH_INDEX: &str = r#"
519+
CREATE INDEX IF NOT EXISTS burn_blocks_ch on burn_blocks (consensus_hash);
520+
"#;
521+
513522
static SCHEMA_1: &[&str] = &[
514523
DROP_SCHEMA_0,
515524
CREATE_DB_CONFIG,
@@ -579,9 +588,15 @@ static SCHEMA_9: &[&str] = &[
579588
"INSERT INTO db_config (version) VALUES (9);",
580589
];
581590

591+
static SCHEMA_10: &[&str] = &[
592+
ADD_CONSENSUS_HASH,
593+
ADD_CONSENSUS_HASH_INDEX,
594+
"INSERT INTO db_config (version) VALUES (10);",
595+
];
596+
582597
impl SignerDb {
583598
/// The current schema version used in this build of the signer binary.
584-
pub const SCHEMA_VERSION: u32 = 9;
599+
pub const SCHEMA_VERSION: u32 = 10;
585600

586601
/// Create a new `SignerState` instance.
587602
/// This will create a new SQLite database at the given path
@@ -723,7 +738,7 @@ impl SignerDb {
723738
Ok(())
724739
}
725740

726-
/// Migrate from schema 9 to schema 9
741+
/// Migrate from schema 8 to schema 9
727742
fn schema_9_migration(tx: &Transaction) -> Result<(), DBError> {
728743
if Self::get_schema_version(tx)? >= 9 {
729744
// no migration necessary
@@ -737,6 +752,20 @@ impl SignerDb {
737752
Ok(())
738753
}
739754

755+
/// Migrate from schema 9 to schema 10
756+
fn schema_10_migration(tx: &Transaction) -> Result<(), DBError> {
757+
if Self::get_schema_version(tx)? >= 10 {
758+
// no migration necessary
759+
return Ok(());
760+
}
761+
762+
for statement in SCHEMA_10.iter() {
763+
tx.execute_batch(statement)?;
764+
}
765+
766+
Ok(())
767+
}
768+
740769
/// Register custom scalar functions used by the database
741770
fn register_scalar_functions(&self) -> Result<(), DBError> {
742771
// Register helper function for determining if a block is a tenure change transaction
@@ -779,7 +808,8 @@ impl SignerDb {
779808
6 => Self::schema_7_migration(&sql_tx)?,
780809
7 => Self::schema_8_migration(&sql_tx)?,
781810
8 => Self::schema_9_migration(&sql_tx)?,
782-
9 => break,
811+
9 => Self::schema_10_migration(&sql_tx)?,
812+
10 => break,
783813
x => return Err(DBError::Other(format!(
784814
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
785815
Self::SCHEMA_VERSION,
@@ -911,18 +941,20 @@ impl SignerDb {
911941
pub fn insert_burn_block(
912942
&mut self,
913943
burn_hash: &BurnchainHeaderHash,
944+
consensus_hash: &ConsensusHash,
914945
burn_height: u64,
915946
received_time: &SystemTime,
916947
) -> Result<(), DBError> {
917948
let received_ts = received_time
918949
.duration_since(std::time::UNIX_EPOCH)
919950
.map_err(|e| DBError::Other(format!("Bad system time: {e}")))?
920951
.as_secs();
921-
debug!("Inserting burn block info"; "burn_block_height" => burn_height, "burn_hash" => %burn_hash, "received" => received_ts);
952+
debug!("Inserting burn block info"; "burn_block_height" => burn_height, "burn_hash" => %burn_hash, "received" => received_ts, "ch" => %consensus_hash);
922953
self.db.execute(
923-
"INSERT OR REPLACE INTO burn_blocks (block_hash, block_height, received_time) VALUES (?1, ?2, ?3)",
954+
"INSERT OR REPLACE INTO burn_blocks (block_hash, consensus_hash, block_height, received_time) VALUES (?1, ?2, ?3, ?4)",
924955
params![
925956
burn_hash,
957+
consensus_hash,
926958
u64_to_sql(burn_height)?,
927959
u64_to_sql(received_ts)?,
928960
],
@@ -947,6 +979,23 @@ impl SignerDb {
947979
Ok(Some(receive_time))
948980
}
949981

982+
/// Get timestamp (epoch seconds) at which a burn block was received over the event dispatcheer by this signer
983+
/// if that burn block has been received.
984+
pub fn get_burn_block_receive_time_ch(
985+
&self,
986+
ch: &ConsensusHash,
987+
) -> Result<Option<u64>, DBError> {
988+
let query = "SELECT received_time FROM burn_blocks WHERE consensus_hash = ? LIMIT 1";
989+
let Some(receive_time_i64) = query_row::<i64, _>(&self.db, query, &[ch])? else {
990+
return Ok(None);
991+
};
992+
let receive_time = u64::try_from(receive_time_i64).map_err(|e| {
993+
error!("Failed to parse db received_time as u64: {e}");
994+
DBError::Corruption
995+
})?;
996+
Ok(Some(receive_time))
997+
}
998+
950999
/// Insert or replace a block into the database.
9511000
/// Preserves the `broadcast` column if replacing an existing block.
9521001
pub fn insert_block(&mut self, block_info: &BlockInfo) -> Result<(), DBError> {
@@ -1497,12 +1546,14 @@ mod tests {
14971546
let db_path = tmp_db_path();
14981547
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
14991548
let test_burn_hash = BurnchainHeaderHash([10; 32]);
1549+
let test_consensus_hash = ConsensusHash([13; 20]);
15001550
let stime = SystemTime::now();
15011551
let time_to_epoch = stime
15021552
.duration_since(SystemTime::UNIX_EPOCH)
15031553
.unwrap()
15041554
.as_secs();
1505-
db.insert_burn_block(&test_burn_hash, 10, &stime).unwrap();
1555+
db.insert_burn_block(&test_burn_hash, &test_consensus_hash, 10, &stime)
1556+
.unwrap();
15061557

15071558
let stored_time = db
15081559
.get_burn_block_receive_time(&test_burn_hash)

stacks-signer/src/tests/chainstate.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,12 @@ fn reorg_timing_testing(
251251
let sortition_time = SystemTime::UNIX_EPOCH
252252
+ Duration::from_secs(block_info_1.proposed_time + sortition_timing_secs);
253253
signer_db
254-
.insert_burn_block(&view.cur_sortition.burn_block_hash, 3, &sortition_time)
254+
.insert_burn_block(
255+
&view.cur_sortition.burn_block_hash,
256+
&view.cur_sortition.consensus_hash,
257+
3,
258+
&sortition_time,
259+
)
255260
.unwrap();
256261

257262
let MockServerClient {
@@ -385,10 +390,11 @@ fn check_block_proposal_timeout() {
385390

386391
// Ensure we have a burn height to compare against
387392
let burn_hash = view.cur_sortition.burn_block_hash;
393+
let consensus_hash = view.cur_sortition.consensus_hash;
388394
let burn_height = 1;
389395
let received_time = SystemTime::now();
390396
signer_db
391-
.insert_burn_block(&burn_hash, burn_height, &received_time)
397+
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
392398
.unwrap();
393399

394400
view.check_proposal(
@@ -456,10 +462,11 @@ fn check_sortition_timeout() {
456462
};
457463
// Ensure we have a burn height to compare against
458464
let burn_hash = sortition.burn_block_hash;
465+
let consensus_hash = sortition.consensus_hash;
459466
let burn_height = 1;
460467
let received_time = SystemTime::now();
461468
signer_db
462-
.insert_burn_block(&burn_hash, burn_height, &received_time)
469+
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
463470
.unwrap();
464471

465472
std::thread::sleep(Duration::from_secs(1));

0 commit comments

Comments
 (0)