Skip to content

Commit 0022c19

Browse files
authored
Merge pull request #6020 from hstove/feat/fork-detection-state-machine
state machine updates for tx replay
2 parents 0ca28a0 + 745ce55 commit 0022c19

File tree

13 files changed

+805
-81
lines changed

13 files changed

+805
-81
lines changed

libsigner/src/events.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ pub enum SignerEvent<T: SignerEventTrait> {
214214
consensus_hash: ConsensusHash,
215215
/// the time at which this event was received by the signer's event processor
216216
received_time: SystemTime,
217+
/// the parent burn block hash for the newly processed burn block
218+
parent_burn_block_hash: BurnchainHeaderHash,
217219
},
218220
/// A new processed Stacks block was received from the node with the given block hash
219221
NewBlock {
@@ -585,6 +587,8 @@ struct BurnBlockEvent {
585587
burn_amount: u64,
586588
#[serde(with = "prefix_hex")]
587589
consensus_hash: ConsensusHash,
590+
#[serde(with = "prefix_hex")]
591+
parent_burn_block_hash: BurnchainHeaderHash,
588592
}
589593

590594
impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
@@ -596,6 +600,7 @@ impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
596600
received_time: SystemTime::now(),
597601
burn_header_hash: burn_block_event.burn_block_hash,
598602
consensus_hash: burn_block_event.consensus_hash,
603+
parent_burn_block_hash: burn_block_event.parent_burn_block_hash,
599604
})
600605
}
601606
}

stacks-signer/src/client/stacks_client.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,22 @@ impl StacksClient {
452452
})
453453
}
454454

455+
/// Get the sortition info for a given consensus hash
456+
pub fn get_sortition_by_consensus_hash(
457+
&self,
458+
consensus_hash: &ConsensusHash,
459+
) -> Result<SortitionInfo, ClientError> {
460+
let path = self.sortition_by_consensus_hash_path(consensus_hash);
461+
let response = self.stacks_node_client.get(&path).send()?;
462+
if !response.status().is_success() {
463+
return Err(ClientError::RequestFailure(response.status()));
464+
}
465+
let sortition_info = response.json::<Vec<SortitionInfo>>()?;
466+
sortition_info.first().cloned().ok_or_else(|| {
467+
ClientError::InvalidResponse("No sortition info found for given consensus hash".into())
468+
})
469+
}
470+
455471
/// Get the current peer info data from the stacks node
456472
pub fn get_peer_info(&self) -> Result<PeerInfo, ClientError> {
457473
debug!("StacksClient: Getting peer info");
@@ -725,6 +741,14 @@ impl StacksClient {
725741
format!("{}{RPC_SORTITION_INFO_PATH}", self.http_origin)
726742
}
727743

744+
fn sortition_by_consensus_hash_path(&self, consensus_hash: &ConsensusHash) -> String {
745+
format!(
746+
"{}{RPC_SORTITION_INFO_PATH}/consensus/{}",
747+
self.http_origin,
748+
consensus_hash.to_hex()
749+
)
750+
}
751+
728752
fn tenure_forking_info_path(&self, start: &ConsensusHash, stop: &ConsensusHash) -> String {
729753
format!(
730754
"{}{RPC_TENURE_FORKING_INFO_PATH}/{}/{}",

stacks-signer/src/signerdb.rs

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ use std::time::{Duration, SystemTime};
2121

2222
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
2323
use blockstack_lib::chainstate::stacks::TransactionPayload;
24+
#[cfg(any(test, feature = "testing"))]
25+
use blockstack_lib::util_lib::db::FromColumn;
2426
use blockstack_lib::util_lib::db::{
2527
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
26-
Error as DBError,
28+
Error as DBError, FromRow,
2729
};
28-
#[cfg(any(test, feature = "testing"))]
29-
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
3030
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
3131
use clarity::types::Address;
3232
use libsigner::v0::messages::{RejectReason, RejectReasonPrefix, StateMachineUpdate};
@@ -72,6 +72,34 @@ impl StacksMessageCodec for NakamotoBlockVote {
7272
}
7373
}
7474

75+
#[derive(Serialize, Deserialize, Debug, PartialEq)]
76+
/// Struct for storing information about a burn block
77+
pub struct BurnBlockInfo {
78+
/// The hash of the burn block
79+
pub block_hash: BurnchainHeaderHash,
80+
/// The height of the burn block
81+
pub block_height: u64,
82+
/// The consensus hash of the burn block
83+
pub consensus_hash: ConsensusHash,
84+
/// The hash of the parent burn block
85+
pub parent_burn_block_hash: BurnchainHeaderHash,
86+
}
87+
88+
impl FromRow<BurnBlockInfo> for BurnBlockInfo {
89+
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
90+
let block_hash: BurnchainHeaderHash = row.get(0)?;
91+
let block_height: u64 = row.get(1)?;
92+
let consensus_hash: ConsensusHash = row.get(2)?;
93+
let parent_burn_block_hash: BurnchainHeaderHash = row.get(3)?;
94+
Ok(BurnBlockInfo {
95+
block_hash,
96+
block_height,
97+
consensus_hash,
98+
parent_burn_block_hash,
99+
})
100+
}
101+
}
102+
75103
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
76104
/// Store extra version-specific info in `BlockInfo`
77105
pub enum ExtraBlockInfo {
@@ -566,6 +594,15 @@ CREATE TABLE IF NOT EXISTS signer_state_machine_updates (
566594
PRIMARY KEY (signer_addr, reward_cycle)
567595
) STRICT;"#;
568596

597+
static ADD_PARENT_BURN_BLOCK_HASH: &str = r#"
598+
ALTER TABLE burn_blocks
599+
ADD COLUMN parent_burn_block_hash TEXT;
600+
"#;
601+
602+
static ADD_PARENT_BURN_BLOCK_HASH_INDEX: &str = r#"
603+
CREATE INDEX IF NOT EXISTS burn_blocks_parent_burn_block_hash_idx on burn_blocks (parent_burn_block_hash);
604+
"#;
605+
569606
static SCHEMA_1: &[&str] = &[
570607
DROP_SCHEMA_0,
571608
CREATE_DB_CONFIG,
@@ -652,6 +689,12 @@ static SCHEMA_12: &[&str] = &[
652689
"INSERT OR REPLACE INTO db_config (version) VALUES (12);",
653690
];
654691

692+
static SCHEMA_13: &[&str] = &[
693+
ADD_PARENT_BURN_BLOCK_HASH,
694+
ADD_PARENT_BURN_BLOCK_HASH_INDEX,
695+
"INSERT INTO db_config (version) VALUES (13);",
696+
];
697+
655698
impl SignerDb {
656699
/// The current schema version used in this build of the signer binary.
657700
pub const SCHEMA_VERSION: u32 = 12;
@@ -852,6 +895,20 @@ impl SignerDb {
852895
Ok(())
853896
}
854897

898+
/// Migrate from schema 12 to schema 13
899+
fn schema_13_migration(tx: &Transaction) -> Result<(), DBError> {
900+
if Self::get_schema_version(tx)? >= 13 {
901+
// no migration necessary
902+
return Ok(());
903+
}
904+
905+
for statement in SCHEMA_13.iter() {
906+
tx.execute_batch(statement)?;
907+
}
908+
909+
Ok(())
910+
}
911+
855912
/// Register custom scalar functions used by the database
856913
fn register_scalar_functions(&self) -> Result<(), DBError> {
857914
// Register helper function for determining if a block is a tenure change transaction
@@ -897,7 +954,8 @@ impl SignerDb {
897954
9 => Self::schema_10_migration(&sql_tx)?,
898955
10 => Self::schema_11_migration(&sql_tx)?,
899956
11 => Self::schema_12_migration(&sql_tx)?,
900-
12 => break,
957+
12 => Self::schema_13_migration(&sql_tx)?,
958+
13 => break,
901959
x => return Err(DBError::Other(format!(
902960
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
903961
Self::SCHEMA_VERSION,
@@ -1032,19 +1090,27 @@ impl SignerDb {
10321090
consensus_hash: &ConsensusHash,
10331091
burn_height: u64,
10341092
received_time: &SystemTime,
1093+
parent_burn_block_hash: &BurnchainHeaderHash,
10351094
) -> Result<(), DBError> {
10361095
let received_ts = received_time
10371096
.duration_since(std::time::UNIX_EPOCH)
10381097
.map_err(|e| DBError::Other(format!("Bad system time: {e}")))?
10391098
.as_secs();
1040-
debug!("Inserting burn block info"; "burn_block_height" => burn_height, "burn_hash" => %burn_hash, "received" => received_ts, "ch" => %consensus_hash);
1099+
debug!("Inserting burn block info";
1100+
"burn_block_height" => burn_height,
1101+
"burn_hash" => %burn_hash,
1102+
"received" => received_ts,
1103+
"ch" => %consensus_hash,
1104+
"parent_burn_block_hash" => %parent_burn_block_hash
1105+
);
10411106
self.db.execute(
1042-
"INSERT OR REPLACE INTO burn_blocks (block_hash, consensus_hash, block_height, received_time) VALUES (?1, ?2, ?3, ?4)",
1107+
"INSERT OR REPLACE INTO burn_blocks (block_hash, consensus_hash, block_height, received_time, parent_burn_block_hash) VALUES (?1, ?2, ?3, ?4, ?5)",
10431108
params![
10441109
burn_hash,
10451110
consensus_hash,
10461111
u64_to_sql(burn_height)?,
10471112
u64_to_sql(received_ts)?,
1113+
parent_burn_block_hash,
10481114
],
10491115
)?;
10501116
Ok(())
@@ -1084,6 +1150,26 @@ impl SignerDb {
10841150
Ok(Some(receive_time))
10851151
}
10861152

1153+
/// Lookup the burn block for a given burn block hash.
1154+
pub fn get_burn_block_by_hash(
1155+
&self,
1156+
burn_block_hash: &BurnchainHeaderHash,
1157+
) -> Result<BurnBlockInfo, DBError> {
1158+
let query =
1159+
"SELECT block_hash, block_height, consensus_hash, parent_burn_block_hash FROM burn_blocks WHERE block_hash = ?";
1160+
let args = params![burn_block_hash];
1161+
1162+
query_row(&self.db, query, args)?.ok_or(DBError::NotFoundError)
1163+
}
1164+
1165+
/// Lookup the burn block for a given consensus hash.
1166+
pub fn get_burn_block_by_ch(&self, ch: &ConsensusHash) -> Result<BurnBlockInfo, DBError> {
1167+
let query = "SELECT block_hash, block_height, consensus_hash, parent_burn_block_hash FROM burn_blocks WHERE consensus_hash = ?";
1168+
let args = params![ch];
1169+
1170+
query_row(&self.db, query, args)?.ok_or(DBError::NotFoundError)
1171+
}
1172+
10871173
/// Insert or replace a block into the database.
10881174
/// Preserves the `broadcast` column if replacing an existing block.
10891175
pub fn insert_block(&mut self, block_info: &BlockInfo) -> Result<(), DBError> {
@@ -1717,8 +1803,14 @@ pub mod tests {
17171803
.duration_since(SystemTime::UNIX_EPOCH)
17181804
.unwrap()
17191805
.as_secs();
1720-
db.insert_burn_block(&test_burn_hash, &test_consensus_hash, 10, &stime)
1721-
.unwrap();
1806+
db.insert_burn_block(
1807+
&test_burn_hash,
1808+
&test_consensus_hash,
1809+
10,
1810+
&stime,
1811+
&test_burn_hash,
1812+
)
1813+
.unwrap();
17221814

17231815
let stored_time = db
17241816
.get_burn_block_receive_time(&test_burn_hash)

stacks-signer/src/tests/chainstate.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ fn reorg_timing_testing(
210210
consensus_hash: last_sortition.consensus_hash,
211211
was_sortition: true,
212212
first_block_mined: Some(StacksBlockId([1; 32])),
213+
nakamoto_blocks: None,
213214
},
214215
TenureForkingInfo {
215216
burn_block_hash: BurnchainHeaderHash([128; 32]),
@@ -219,6 +220,7 @@ fn reorg_timing_testing(
219220
consensus_hash: view.cur_sortition.parent_tenure_id,
220221
was_sortition: true,
221222
first_block_mined: Some(StacksBlockId([2; 32])),
223+
nakamoto_blocks: None,
222224
},
223225
];
224226

@@ -256,6 +258,7 @@ fn reorg_timing_testing(
256258
&view.cur_sortition.consensus_hash,
257259
3,
258260
&sortition_time,
261+
&view.last_sortition.as_ref().unwrap().burn_block_hash,
259262
)
260263
.unwrap();
261264

@@ -394,7 +397,13 @@ fn check_block_proposal_timeout() {
394397
let burn_height = 1;
395398
let received_time = SystemTime::now();
396399
signer_db
397-
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
400+
.insert_burn_block(
401+
&burn_hash,
402+
&consensus_hash,
403+
burn_height,
404+
&received_time,
405+
&view.last_sortition.as_ref().unwrap().burn_block_hash,
406+
)
398407
.unwrap();
399408

400409
view.check_proposal(
@@ -466,7 +475,13 @@ fn check_sortition_timeout() {
466475
let burn_height = 1;
467476
let received_time = SystemTime::now();
468477
signer_db
469-
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
478+
.insert_burn_block(
479+
&burn_hash,
480+
&consensus_hash,
481+
burn_height,
482+
&received_time,
483+
&BurnchainHeaderHash([0; 32]),
484+
)
470485
.unwrap();
471486

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

stacks-signer/src/v0/signer.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::client::{ClientError, SignerSlotID, StackerDB, StacksClient};
5151
use crate::config::{SignerConfig, SignerConfigMode};
5252
use crate::runloop::SignerResult;
5353
use crate::signerdb::{BlockInfo, BlockState, SignerDb};
54+
use crate::v0::signer_state::NewBurnBlock;
5455
use crate::Signer as SignerTrait;
5556

5657
/// A global variable that can be used to make signers repeat their proposal
@@ -486,6 +487,7 @@ impl Signer {
486487
burn_header_hash,
487488
consensus_hash,
488489
received_time,
490+
parent_burn_block_hash,
489491
} => {
490492
info!("{self}: Received a new burn block event for block height {burn_height}");
491493
self.signer_db
@@ -494,6 +496,7 @@ impl Signer {
494496
consensus_hash,
495497
*burn_height,
496498
received_time,
499+
parent_burn_block_hash,
497500
)
498501
.unwrap_or_else(|e| {
499502
error!(
@@ -505,7 +508,10 @@ impl Signer {
505508
panic!("{self} Failed to write burn block event to signerdb: {e}");
506509
});
507510
self.local_state_machine
508-
.bitcoin_block_arrival(&self.signer_db, stacks_client, &self.proposal_config, Some(*burn_height))
511+
.bitcoin_block_arrival(&self.signer_db, stacks_client, &self.proposal_config, Some(NewBurnBlock {
512+
burn_block_height: *burn_height,
513+
consensus_hash: *consensus_hash,
514+
}))
509515
.unwrap_or_else(|e| error!("{self}: failed to update local state machine for latest bitcoin block arrival"; "err" => ?e));
510516
*sortition_state = None;
511517
}

0 commit comments

Comments
 (0)