Skip to content
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

feat: prevent multiple block proposal evals #5453

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ jobs:
- tests::signer::v0::block_commit_delay
- tests::signer::v0::continue_after_fast_block_no_sortition
- tests::signer::v0::block_validation_response_timeout
- tests::signer::v0::block_validation_pending_table
- tests::signer::v0::tenure_extend_after_bad_commit
- tests::signer::v0::block_proposal_max_age_rejections
- tests::signer::v0::global_acceptance_depends_on_block_announcement
Expand Down
8 changes: 8 additions & 0 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,14 @@ impl BlockResponse {
BlockResponse::Rejected(rejection) => rejection.signer_signature_hash,
}
}

/// The signer signature hash for the block response
pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum {
match self {
BlockResponse::Accepted(accepted) => accepted.signer_signature_hash,
BlockResponse::Rejected(rejection) => rejection.signer_signature_hash,
}
}
}

impl StacksMessageCodec for BlockResponse {
Expand Down
4 changes: 2 additions & 2 deletions stacks-signer/src/chainstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,8 +589,8 @@ impl SortitionsView {
signer_db.block_lookup(&nakamoto_tip.signer_signature_hash())
{
if block_info.state != BlockState::GloballyAccepted {
if let Err(e) = block_info.mark_globally_accepted() {
warn!("Failed to update block info in db: {e}");
if let Err(e) = signer_db.mark_block_globally_accepted(&mut block_info) {
warn!("Failed to mark block as globally accepted: {e}");
} else if let Err(e) = signer_db.insert_block(&block_info) {
warn!("Failed to update block info in db: {e}");
}
Expand Down
157 changes: 149 additions & 8 deletions stacks-signer/src/signerdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use blockstack_lib::util_lib::db::{
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
Error as DBError,
};
#[cfg(any(test, feature = "testing"))]
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
use libsigner::BlockProposal;
use rusqlite::functions::FunctionFlags;
Expand Down Expand Up @@ -209,7 +211,7 @@ impl BlockInfo {

/// Mark this block as valid, signed over, and records a group timestamp in the block info if it wasn't
/// already set.
pub fn mark_globally_accepted(&mut self) -> Result<(), String> {
fn mark_globally_accepted(&mut self) -> Result<(), String> {
self.move_to(BlockState::GloballyAccepted)?;
self.valid = Some(true);
self.signed_over = true;
Expand All @@ -225,7 +227,7 @@ impl BlockInfo {
}

/// Mark the block as globally rejected and invalid
pub fn mark_globally_rejected(&mut self) -> Result<(), String> {
fn mark_globally_rejected(&mut self) -> Result<(), String> {
self.move_to(BlockState::GloballyRejected)?;
self.valid = Some(false);
Ok(())
Expand Down Expand Up @@ -342,6 +344,10 @@ CREATE INDEX IF NOT EXISTS blocks_state ON blocks (state);
CREATE INDEX IF NOT EXISTS blocks_signed_group ON blocks (signed_group);
"#;

static CREATE_INDEXES_6: &str = r#"
CREATE INDEX IF NOT EXISTS block_validations_pending_on_added_time ON block_validations_pending(added_time);
"#;

static CREATE_SIGNER_STATE_TABLE: &str = "
CREATE TABLE IF NOT EXISTS signer_states (
reward_cycle INTEGER PRIMARY KEY,
Expand Down Expand Up @@ -436,23 +442,23 @@ INSERT INTO temp_blocks (
broadcasted,
stacks_height,
burn_block_height,
valid,
valid,
state,
signed_group,
signed_group,
signed_self,
proposed_time,
validation_time_ms,
tenure_change
)
SELECT
SELECT
signer_signature_hash,
reward_cycle,
block_info,
consensus_hash,
signed_over,
broadcasted,
stacks_height,
burn_block_height,
burn_block_height,
json_extract(block_info, '$.valid') AS valid,
json_extract(block_info, '$.state') AS state,
json_extract(block_info, '$.signed_group') AS signed_group,
Expand All @@ -466,6 +472,14 @@ DROP TABLE blocks;

ALTER TABLE temp_blocks RENAME TO blocks;"#;

static CREATE_BLOCK_VALIDATION_PENDING_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS block_validations_pending (
signer_signature_hash TEXT NOT NULL,
-- the time at which the block was added to the pending table
added_time INTEGER NOT NULL,
PRIMARY KEY (signer_signature_hash)
) STRICT;"#;

static SCHEMA_1: &[&str] = &[
DROP_SCHEMA_0,
CREATE_DB_CONFIG,
Expand Down Expand Up @@ -514,9 +528,15 @@ static SCHEMA_5: &[&str] = &[
"INSERT INTO db_config (version) VALUES (5);",
];

static SCHEMA_6: &[&str] = &[
CREATE_BLOCK_VALIDATION_PENDING_TABLE,
CREATE_INDEXES_6,
"INSERT OR REPLACE INTO db_config (version) VALUES (6);",
];

impl SignerDb {
/// The current schema version used in this build of the signer binary.
pub const SCHEMA_VERSION: u32 = 5;
pub const SCHEMA_VERSION: u32 = 6;

/// Create a new `SignerState` instance.
/// This will create a new SQLite database at the given path
Expand Down Expand Up @@ -616,6 +636,20 @@ impl SignerDb {
Ok(())
}

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

for statement in SCHEMA_6.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 @@ -654,7 +688,8 @@ impl SignerDb {
2 => Self::schema_3_migration(&sql_tx)?,
3 => Self::schema_4_migration(&sql_tx)?,
4 => Self::schema_5_migration(&sql_tx)?,
5 => break,
5 => Self::schema_6_migration(&sql_tx)?,
6 => 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 @@ -976,6 +1011,73 @@ impl SignerDb {
))
}

/// Get a pending block validation, sorted by the time at which it was added to the pending table.
/// If found, remove it from the pending table.
pub fn get_and_remove_pending_block_validation(
&self,
) -> Result<Option<Sha512Trunc256Sum>, DBError> {
if let Some(sighash) = self.get_pending_block_validation()? {
self.remove_pending_block_validation(&sighash)?;
return Ok(Some(sighash));
}
Comment on lines +1019 to +1022
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more efficient to do the get and delete in one query, like a DELETE with a RETURNING signer_signature_hash?

Ok(None)
}

/// Get a pending block validation, sorted by the time at which it was added to the pending table.
pub fn get_pending_block_validation(&self) -> Result<Option<Sha512Trunc256Sum>, DBError> {
let qry =
"SELECT signer_signature_hash FROM block_validations_pending ORDER BY added_time ASC";
let args = params![];
let sighash: Option<String> = query_row(&self.db, qry, args)?;
Ok(sighash.and_then(|sighash| Sha512Trunc256Sum::from_hex(&sighash).ok()))
}

/// Remove a pending block validation
pub fn remove_pending_block_validation(
&self,
sighash: &Sha512Trunc256Sum,
) -> Result<(), DBError> {
self.db.execute(
"DELETE FROM block_validations_pending WHERE signer_signature_hash = ?1",
params![sighash.to_string()],
)?;
Ok(())
}

/// Insert a pending block validation
pub fn insert_pending_block_validation(
&self,
sighash: &Sha512Trunc256Sum,
ts: u64,
) -> Result<(), DBError> {
self.db.execute(
"INSERT INTO block_validations_pending (signer_signature_hash, added_time) VALUES (?1, ?2)",
params![sighash.to_string(), u64_to_sql(ts)?],
)?;
Ok(())
}

/// For tests, fetch all pending block validations
#[cfg(any(test, feature = "testing"))]
pub fn get_all_pending_block_validations(
&self,
) -> Result<Vec<PendingBlockValidation>, DBError> {
let qry = "SELECT signer_signature_hash, added_time FROM block_validations_pending ORDER BY added_time ASC";
query_rows(&self.db, qry, params![])
}

/// For tests, check if a pending block validation exists
#[cfg(any(test, feature = "testing"))]
pub fn has_pending_block_validation(
&self,
sighash: &Sha512Trunc256Sum,
) -> Result<bool, DBError> {
let qry = "SELECT signer_signature_hash FROM block_validations_pending WHERE signer_signature_hash = ?1";
let args = params![sighash.to_string()];
let sighash_opt: Option<String> = query_row(&self.db, qry, args)?;
Ok(sighash_opt.is_some())
}

/// Return the start time (epoch time in seconds) and the processing time in milliseconds of the tenure (idenfitied by consensus_hash).
fn get_tenure_times(&self, tenure: &ConsensusHash) -> Result<(u64, u64), DBError> {
let query = "SELECT tenure_change, proposed_time, validation_time_ms FROM blocks WHERE consensus_hash = ?1 AND state = ?2 ORDER BY stacks_height DESC";
Expand Down Expand Up @@ -1038,6 +1140,24 @@ impl SignerDb {
);
tenure_extend_timestamp
}

/// Mark a block as globally accepted
pub fn mark_block_globally_accepted(&self, block_info: &mut BlockInfo) -> Result<(), DBError> {
block_info
.mark_globally_accepted()
.map_err(DBError::Other)?;
self.remove_pending_block_validation(&block_info.signer_signature_hash())?;
Ok(())
}

/// Mark a block as globally rejected
pub fn mark_block_globally_rejected(&self, block_info: &mut BlockInfo) -> Result<(), DBError> {
block_info
.mark_globally_rejected()
.map_err(DBError::Other)?;
self.remove_pending_block_validation(&block_info.signer_signature_hash())?;
Ok(())
}
}

fn try_deserialize<T>(s: Option<String>) -> Result<Option<T>, DBError>
Expand All @@ -1050,6 +1170,27 @@ where
.map_err(DBError::SerializationError)
}

/// For tests, a struct to represent a pending block validation
#[cfg(any(test, feature = "testing"))]
pub struct PendingBlockValidation {
/// The signer signature hash of the block
pub signer_signature_hash: Sha512Trunc256Sum,
/// The time at which the block was added to the pending table
pub added_time: u64,
}

#[cfg(any(test, feature = "testing"))]
impl FromRow<PendingBlockValidation> for PendingBlockValidation {
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
let signer_signature_hash = Sha512Trunc256Sum::from_column(row, "signer_signature_hash")?;
let added_time = row.get_unwrap(1);
Ok(PendingBlockValidation {
signer_signature_hash,
added_time,
})
}
}

#[cfg(test)]
mod tests {
use std::fs;
Expand Down
Loading
Loading