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

Merged
merged 28 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3b2726e
feat: prevent multiple block proposal evals
hstove Nov 12, 2024
4178fb6
fix: comments, cleanup
hstove Nov 12, 2024
a9c7794
Merge remote-tracking branch 'origin/develop' into feat/retry-pending…
hstove Dec 18, 2024
014f44b
feat: integration test for retry pending block validations
hstove Dec 18, 2024
5384045
Merge branch 'develop' into feat/retry-pending-block-proposals
hstove Dec 18, 2024
0dc1524
fix: move logic for removing/retrying pending block responses
hstove Dec 18, 2024
5f8f9eb
Merge remote-tracking branch 'origin/feat/retry-pending-block-proposa…
hstove Dec 18, 2024
949088f
when marking block as global accepted/rejected, remove pending valida…
hstove Dec 18, 2024
90b6fb3
fix: dont remove pending validation in tests
hstove Dec 18, 2024
63ae626
Merge remote-tracking branch 'origin/develop' into feat/retry-pending…
hstove Dec 19, 2024
2e30240
fix: don't hold mutex while sleeping in test injection
hstove Dec 19, 2024
e522058
feat: use TestFlag for validation delay
hstove Dec 19, 2024
b3f9c35
Merge branch 'develop' into feat/retry-pending-block-proposals
hstove Dec 20, 2024
cf345bb
fix: bump sister block timeout
hstove Dec 20, 2024
77ef010
fix: bump timeout in locally_rejected_blocks_overridden_by_global_acc…
hstove Dec 21, 2024
0c90997
Merge remote-tracking branch 'origin/develop' into feat/retry-pending…
hstove Jan 8, 2025
ae7c822
fix: delete and return pending row in one statement
hstove Jan 8, 2025
5f020df
Merge branch 'develop' into feat/retry-pending-block-proposals
hstove Jan 9, 2025
d667a4e
chore: add explicit ASC order in index
hstove Jan 13, 2025
91c38d0
Merge remote-tracking branch 'origin/develop' into feat/retry-pending…
hstove Jan 13, 2025
b897380
fix: remove unused import post-merge
hstove Jan 13, 2025
8a072d1
fix: crc feedback
hstove Jan 13, 2025
2fd4e78
feat: test for handling pending block proposal at tenure change
hstove Jan 13, 2025
fe20e24
crc: comment around `mark_block_globally_accepted`
hstove Jan 13, 2025
beb6fba
crc: add test to bitcoin-tests
hstove Jan 14, 2025
3123738
crc: remove unused function
hstove Jan 14, 2025
d22b6e0
Merge branch 'develop' into feat/retry-pending-block-proposals
hstove Jan 14, 2025
4d44a6d
chore: changelog
hstove Jan 14, 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
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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 @@ -687,6 +687,14 @@ impl BlockResponse {
}
}

/// 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,
}
}

/// Get the block accept data from the block response
pub fn as_block_accepted(&self) -> Option<&BlockAccepted> {
match self {
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
211 changes: 203 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 ASC);
"#;

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 @@ -960,6 +995,68 @@ impl SignerDb {
Ok(Some(broadcasted))
}

/// Get the current state of a given block in the database
pub fn get_block_state(
hstove marked this conversation as resolved.
Show resolved Hide resolved
&self,
block_sighash: &Sha512Trunc256Sum,
) -> Result<Option<BlockState>, DBError> {
let qry = "SELECT state FROM blocks WHERE signer_signature_hash = ?1 LIMIT 1";
let args = params![block_sighash];
let state_opt: Option<String> = query_row(&self.db, qry, args)?;
let Some(state) = state_opt else {
return Ok(None);
};
Ok(Some(
BlockState::try_from(state.as_str()).map_err(|_| DBError::Corruption)?,
))
}

/// 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> {
let qry = "DELETE FROM block_validations_pending WHERE signer_signature_hash = (SELECT signer_signature_hash FROM block_validations_pending ORDER BY added_time ASC LIMIT 1) RETURNING signer_signature_hash";
obycode marked this conversation as resolved.
Show resolved Hide resolved
let args = params![];
let mut stmt = self.db.prepare(qry)?;
let sighash: Option<String> = stmt.query_row(args, |row| row.get(0)).optional()?;
Ok(sighash.and_then(|sighash| Sha512Trunc256Sum::from_hex(&sighash).ok()))
}

/// 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> {
hstove marked this conversation as resolved.
Show resolved Hide resolved
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(())
}

/// 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 @@ -1022,6 +1119,26 @@ impl SignerDb {
);
tenure_extend_timestamp
}

/// Mark a block as globally accepted. This removes the block from the pending
/// validations table. This does **not** update the block's state in SignerDb.
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. This removes the block from the pending
/// validations table. This does **not** update the block's state in SignerDb.
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 @@ -1034,6 +1151,50 @@ 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(any(test, feature = "testing"))]
impl SignerDb {
/// For tests, fetch all pending block validations
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
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())
}
}

/// Tests for SignerDb
#[cfg(test)]
mod tests {
use std::fs;
Expand Down Expand Up @@ -1734,4 +1895,38 @@ mod tests {
< block_infos[0].proposed_time
);
}

#[test]
fn test_get_and_remove_pending_block_validation() {
let db_path = tmp_db_path();
let db = SignerDb::new(db_path).expect("Failed to create signer db");

let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
assert!(pending_hash.is_none());

db.insert_pending_block_validation(&Sha512Trunc256Sum([0x01; 32]), 1000)
.unwrap();
db.insert_pending_block_validation(&Sha512Trunc256Sum([0x02; 32]), 2000)
.unwrap();
db.insert_pending_block_validation(&Sha512Trunc256Sum([0x03; 32]), 3000)
.unwrap();

let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x01; 32])));

let pendings = db.get_all_pending_block_validations().unwrap();
assert_eq!(pendings.len(), 2);

let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x02; 32])));

let pendings = db.get_all_pending_block_validations().unwrap();
assert_eq!(pendings.len(), 1);

let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x03; 32])));

let pendings = db.get_all_pending_block_validations().unwrap();
assert_eq!(pendings.len(), 0);
}
}
Loading