Skip to content

state machine updates for tx replay #6020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
da31b7d
feat: detect fork in signer state machine
hstove Apr 11, 2025
acb935e
feat: add dropped blocks to tenure forking info API
hstove Apr 11, 2025
eeb5b1e
wip: integration test for tx replay state machine
hstove Apr 11, 2025
eb80530
Merge remote-tracking branch 'fork/feat/replay-signer-validation' int…
hstove Apr 17, 2025
4a7319f
fix: cargo check errors after merge
hstove Apr 17, 2025
f323760
feat: construct replay set after fork
hstove Apr 21, 2025
16ed89c
Merge remote-tracking branch 'origin/develop' into feat/fork-detectio…
hstove Apr 23, 2025
5b70d4f
fix: cleanup comments, remove unused field
hstove Apr 23, 2025
9c44080
feat: add parent_burn_block_hash to /new_burn_block payload
hstove Apr 25, 2025
9c90c0b
fix: trait impl in test
hstove Apr 25, 2025
1d28e3c
feat: update event-dispatcher.md docs
hstove Apr 28, 2025
8d7f7a8
feat: insert parent_burn_block_hash in signerdb
hstove Apr 28, 2025
2bea12c
fix: use parent_burn_block_hash to 'crawl' forked blocks
hstove Apr 28, 2025
2e67706
Add StateMachineUpdateContent::V1 which inculdes a vector of replay_t…
jferrant Apr 29, 2025
2abb5cd
feat: clear tx_replay_set when a new block is mined
hstove Apr 30, 2025
8836dba
Merge remote-tracking branch 'origin/develop' into feat/fork-detectio…
hstove Apr 30, 2025
0bad518
crc: split fork handling into its own fn in signer_state
hstove May 1, 2025
b1d05e4
Merge branch 'develop' into feat/fork-detection-state-machine
hstove May 1, 2025
8159a93
Merge remote-tracking branch 'origin/develop' into feat/fork-detectio…
hstove May 5, 2025
3150433
fix: remove dup implementation from merge
hstove May 5, 2025
2b134d8
fix: test cleanup/fix
hstove May 6, 2025
12e6f4c
feat: new test scenario, fix ordering of txs
hstove May 6, 2025
91eaf81
Merge branch 'develop' into feat/fork-detection-state-machine
hstove May 6, 2025
9acbd65
crc: add index to nakamoto_staging_blocks
hstove May 9, 2025
5a3aa0e
crc: code cleanup in signer_state
hstove May 9, 2025
e8b50f7
Merge remote-tracking branch 'origin/develop' into feat/fork-detectio…
hstove May 9, 2025
745ce55
Merge remote-tracking branch 'origin/develop' into feat/fork-detectio…
hstove May 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libsigner/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ pub enum SignerEvent<T: SignerEventTrait> {
consensus_hash: ConsensusHash,
/// the time at which this event was received by the signer's event processor
received_time: SystemTime,
/// the parent burn block hash for the newly processed burn block
parent_burn_block_hash: BurnchainHeaderHash,
},
/// A new processed Stacks block was received from the node with the given block hash
NewBlock {
Expand Down Expand Up @@ -585,6 +587,8 @@ struct BurnBlockEvent {
burn_amount: u64,
#[serde(with = "prefix_hex")]
consensus_hash: ConsensusHash,
#[serde(with = "prefix_hex")]
parent_burn_block_hash: BurnchainHeaderHash,
}

impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
Expand All @@ -596,6 +600,7 @@ impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
received_time: SystemTime::now(),
burn_header_hash: burn_block_event.burn_block_hash,
consensus_hash: burn_block_event.consensus_hash,
parent_burn_block_hash: burn_block_event.parent_burn_block_hash,
})
}
}
Expand Down
24 changes: 24 additions & 0 deletions stacks-signer/src/client/stacks_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,22 @@ impl StacksClient {
})
}

/// Get the sortition info for a given consensus hash
pub fn get_sortition_by_consensus_hash(
&self,
consensus_hash: &ConsensusHash,
) -> Result<SortitionInfo, ClientError> {
let path = self.sortition_by_consensus_hash_path(consensus_hash);
let response = self.stacks_node_client.get(&path).send()?;
if !response.status().is_success() {
return Err(ClientError::RequestFailure(response.status()));
}
let sortition_info = response.json::<Vec<SortitionInfo>>()?;
sortition_info.first().cloned().ok_or_else(|| {
ClientError::InvalidResponse("No sortition info found for given consensus hash".into())
})
}

/// Get the current peer info data from the stacks node
pub fn get_peer_info(&self) -> Result<PeerInfo, ClientError> {
debug!("StacksClient: Getting peer info");
Expand Down Expand Up @@ -725,6 +741,14 @@ impl StacksClient {
format!("{}{RPC_SORTITION_INFO_PATH}", self.http_origin)
}

fn sortition_by_consensus_hash_path(&self, consensus_hash: &ConsensusHash) -> String {
format!(
"{}{RPC_SORTITION_INFO_PATH}/consensus/{}",
self.http_origin,
consensus_hash.to_hex()
)
}

fn tenure_forking_info_path(&self, start: &ConsensusHash, stop: &ConsensusHash) -> String {
format!(
"{}{RPC_TENURE_FORKING_INFO_PATH}/{}/{}",
Expand Down
108 changes: 100 additions & 8 deletions stacks-signer/src/signerdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ use std::time::{Duration, SystemTime};

use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::chainstate::stacks::TransactionPayload;
#[cfg(any(test, feature = "testing"))]
use blockstack_lib::util_lib::db::FromColumn;
use blockstack_lib::util_lib::db::{
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
Error as DBError,
Error as DBError, FromRow,
};
#[cfg(any(test, feature = "testing"))]
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
use clarity::types::Address;
use libsigner::v0::messages::{RejectReason, RejectReasonPrefix, StateMachineUpdate};
Expand Down Expand Up @@ -72,6 +72,34 @@ impl StacksMessageCodec for NakamotoBlockVote {
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
/// Struct for storing information about a burn block
pub struct BurnBlockInfo {
/// The hash of the burn block
pub block_hash: BurnchainHeaderHash,
/// The height of the burn block
pub block_height: u64,
/// The consensus hash of the burn block
pub consensus_hash: ConsensusHash,
/// The hash of the parent burn block
pub parent_burn_block_hash: BurnchainHeaderHash,
}

impl FromRow<BurnBlockInfo> for BurnBlockInfo {
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
let block_hash: BurnchainHeaderHash = row.get(0)?;
let block_height: u64 = row.get(1)?;
let consensus_hash: ConsensusHash = row.get(2)?;
let parent_burn_block_hash: BurnchainHeaderHash = row.get(3)?;
Ok(BurnBlockInfo {
block_hash,
block_height,
consensus_hash,
parent_burn_block_hash,
})
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
/// Store extra version-specific info in `BlockInfo`
pub enum ExtraBlockInfo {
Expand Down Expand Up @@ -566,6 +594,15 @@ CREATE TABLE IF NOT EXISTS signer_state_machine_updates (
PRIMARY KEY (signer_addr, reward_cycle)
) STRICT;"#;

static ADD_PARENT_BURN_BLOCK_HASH: &str = r#"
ALTER TABLE burn_blocks
ADD COLUMN parent_burn_block_hash TEXT;
"#;

static ADD_PARENT_BURN_BLOCK_HASH_INDEX: &str = r#"
CREATE INDEX IF NOT EXISTS burn_blocks_parent_burn_block_hash_idx on burn_blocks (parent_burn_block_hash);
"#;

static SCHEMA_1: &[&str] = &[
DROP_SCHEMA_0,
CREATE_DB_CONFIG,
Expand Down Expand Up @@ -652,6 +689,12 @@ static SCHEMA_12: &[&str] = &[
"INSERT OR REPLACE INTO db_config (version) VALUES (12);",
];

static SCHEMA_13: &[&str] = &[
ADD_PARENT_BURN_BLOCK_HASH,
ADD_PARENT_BURN_BLOCK_HASH_INDEX,
"INSERT INTO db_config (version) VALUES (13);",
];

impl SignerDb {
/// The current schema version used in this build of the signer binary.
pub const SCHEMA_VERSION: u32 = 12;
Expand Down Expand Up @@ -852,6 +895,20 @@ impl SignerDb {
Ok(())
}

/// Migrate from schema 12 to schema 13
fn schema_13_migration(tx: &Transaction) -> Result<(), DBError> {
if Self::get_schema_version(tx)? >= 13 {
// no migration necessary
return Ok(());
}

for statement in SCHEMA_13.iter() {
tx.execute_batch(statement)?;
}

Ok(())
}

/// Register custom scalar functions used by the database
fn register_scalar_functions(&self) -> Result<(), DBError> {
// Register helper function for determining if a block is a tenure change transaction
Expand Down Expand Up @@ -897,7 +954,8 @@ impl SignerDb {
9 => Self::schema_10_migration(&sql_tx)?,
10 => Self::schema_11_migration(&sql_tx)?,
11 => Self::schema_12_migration(&sql_tx)?,
12 => break,
12 => Self::schema_13_migration(&sql_tx)?,
13 => break,
x => return Err(DBError::Other(format!(
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
Self::SCHEMA_VERSION,
Expand Down Expand Up @@ -1032,19 +1090,27 @@ impl SignerDb {
consensus_hash: &ConsensusHash,
burn_height: u64,
received_time: &SystemTime,
parent_burn_block_hash: &BurnchainHeaderHash,
) -> Result<(), DBError> {
let received_ts = received_time
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| DBError::Other(format!("Bad system time: {e}")))?
.as_secs();
debug!("Inserting burn block info"; "burn_block_height" => burn_height, "burn_hash" => %burn_hash, "received" => received_ts, "ch" => %consensus_hash);
debug!("Inserting burn block info";
"burn_block_height" => burn_height,
"burn_hash" => %burn_hash,
"received" => received_ts,
"ch" => %consensus_hash,
"parent_burn_block_hash" => %parent_burn_block_hash
);
self.db.execute(
"INSERT OR REPLACE INTO burn_blocks (block_hash, consensus_hash, block_height, received_time) VALUES (?1, ?2, ?3, ?4)",
"INSERT OR REPLACE INTO burn_blocks (block_hash, consensus_hash, block_height, received_time, parent_burn_block_hash) VALUES (?1, ?2, ?3, ?4, ?5)",
params![
burn_hash,
consensus_hash,
u64_to_sql(burn_height)?,
u64_to_sql(received_ts)?,
parent_burn_block_hash,
],
)?;
Ok(())
Expand Down Expand Up @@ -1084,6 +1150,26 @@ impl SignerDb {
Ok(Some(receive_time))
}

/// Lookup the burn block for a given burn block hash.
pub fn get_burn_block_by_hash(
&self,
burn_block_hash: &BurnchainHeaderHash,
) -> Result<BurnBlockInfo, DBError> {
let query =
"SELECT block_hash, block_height, consensus_hash, parent_burn_block_hash FROM burn_blocks WHERE block_hash = ?";
let args = params![burn_block_hash];

query_row(&self.db, query, args)?.ok_or(DBError::NotFoundError)
}

/// Lookup the burn block for a given consensus hash.
pub fn get_burn_block_by_ch(&self, ch: &ConsensusHash) -> Result<BurnBlockInfo, DBError> {
let query = "SELECT block_hash, block_height, consensus_hash, parent_burn_block_hash FROM burn_blocks WHERE consensus_hash = ?";
let args = params![ch];

query_row(&self.db, query, args)?.ok_or(DBError::NotFoundError)
}

/// Insert or replace a block into the database.
/// Preserves the `broadcast` column if replacing an existing block.
pub fn insert_block(&mut self, block_info: &BlockInfo) -> Result<(), DBError> {
Expand Down Expand Up @@ -1717,8 +1803,14 @@ pub mod tests {
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
db.insert_burn_block(&test_burn_hash, &test_consensus_hash, 10, &stime)
.unwrap();
db.insert_burn_block(
&test_burn_hash,
&test_consensus_hash,
10,
&stime,
&test_burn_hash,
)
.unwrap();

let stored_time = db
.get_burn_block_receive_time(&test_burn_hash)
Expand Down
19 changes: 17 additions & 2 deletions stacks-signer/src/tests/chainstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ fn reorg_timing_testing(
consensus_hash: last_sortition.consensus_hash,
was_sortition: true,
first_block_mined: Some(StacksBlockId([1; 32])),
nakamoto_blocks: None,
},
TenureForkingInfo {
burn_block_hash: BurnchainHeaderHash([128; 32]),
Expand All @@ -219,6 +220,7 @@ fn reorg_timing_testing(
consensus_hash: view.cur_sortition.parent_tenure_id,
was_sortition: true,
first_block_mined: Some(StacksBlockId([2; 32])),
nakamoto_blocks: None,
},
];

Expand Down Expand Up @@ -256,6 +258,7 @@ fn reorg_timing_testing(
&view.cur_sortition.consensus_hash,
3,
&sortition_time,
&view.last_sortition.as_ref().unwrap().burn_block_hash,
)
.unwrap();

Expand Down Expand Up @@ -394,7 +397,13 @@ fn check_block_proposal_timeout() {
let burn_height = 1;
let received_time = SystemTime::now();
signer_db
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
.insert_burn_block(
&burn_hash,
&consensus_hash,
burn_height,
&received_time,
&view.last_sortition.as_ref().unwrap().burn_block_hash,
)
.unwrap();

view.check_proposal(
Expand Down Expand Up @@ -466,7 +475,13 @@ fn check_sortition_timeout() {
let burn_height = 1;
let received_time = SystemTime::now();
signer_db
.insert_burn_block(&burn_hash, &consensus_hash, burn_height, &received_time)
.insert_burn_block(
&burn_hash,
&consensus_hash,
burn_height,
&received_time,
&BurnchainHeaderHash([0; 32]),
)
.unwrap();

std::thread::sleep(Duration::from_secs(1));
Expand Down
8 changes: 7 additions & 1 deletion stacks-signer/src/v0/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use crate::client::{ClientError, SignerSlotID, StackerDB, StacksClient};
use crate::config::{SignerConfig, SignerConfigMode};
use crate::runloop::SignerResult;
use crate::signerdb::{BlockInfo, BlockState, SignerDb};
use crate::v0::signer_state::NewBurnBlock;
use crate::Signer as SignerTrait;

/// A global variable that can be used to make signers repeat their proposal
Expand Down Expand Up @@ -486,6 +487,7 @@ impl Signer {
burn_header_hash,
consensus_hash,
received_time,
parent_burn_block_hash,
} => {
info!("{self}: Received a new burn block event for block height {burn_height}");
self.signer_db
Expand All @@ -494,6 +496,7 @@ impl Signer {
consensus_hash,
*burn_height,
received_time,
parent_burn_block_hash,
)
.unwrap_or_else(|e| {
error!(
Expand All @@ -505,7 +508,10 @@ impl Signer {
panic!("{self} Failed to write burn block event to signerdb: {e}");
});
self.local_state_machine
.bitcoin_block_arrival(&self.signer_db, stacks_client, &self.proposal_config, Some(*burn_height))
.bitcoin_block_arrival(&self.signer_db, stacks_client, &self.proposal_config, Some(NewBurnBlock {
burn_block_height: *burn_height,
consensus_hash: *consensus_hash,
}))
.unwrap_or_else(|e| error!("{self}: failed to update local state machine for latest bitcoin block arrival"; "err" => ?e));
*sortition_state = None;
}
Expand Down
Loading
Loading