Skip to content

Commit 471649a

Browse files
authored
Merge branch 'develop' into fix/clippy-ci-unnecessary-fold
2 parents 64b8f36 + ab90a9d commit 471649a

File tree

23 files changed

+802
-84
lines changed

23 files changed

+802
-84
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ jobs:
132132
- tests::signer::v0::block_commit_delay
133133
- tests::signer::v0::continue_after_fast_block_no_sortition
134134
- tests::signer::v0::block_validation_response_timeout
135+
- tests::signer::v0::block_validation_pending_table
136+
- tests::signer::v0::new_tenure_while_validating_previous_scenario
135137
- tests::signer::v0::tenure_extend_after_bad_commit
136138
- tests::signer::v0::block_proposal_max_age_rejections
137139
- tests::signer::v0::global_acceptance_depends_on_block_announcement

libsigner/src/v0/messages.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,14 @@ impl BlockResponse {
688688
}
689689
}
690690

691+
/// The signer signature hash for the block response
692+
pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum {
693+
match self {
694+
BlockResponse::Accepted(accepted) => accepted.signer_signature_hash,
695+
BlockResponse::Rejected(rejection) => rejection.signer_signature_hash,
696+
}
697+
}
698+
691699
/// Get the block accept data from the block response
692700
pub fn as_block_accepted(&self) -> Option<&BlockAccepted> {
693701
match self {

stacks-signer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1010
## Added
1111

1212
- Introduced the `block_proposal_max_age_secs` configuration option for signers, enabling them to automatically ignore block proposals that exceed the specified age in seconds.
13+
- When a new block proposal is received while the signer is waiting for an existing proposal to be validated, the signer will wait until the existing block is done validating before submitting the new one for validating. ([#5453](https://github.com/stacks-network/stacks-core/pull/5453))
1314

1415
## Changed
1516
- Improvements to the stale signer cleanup logic: deletes the prior signer if it has no remaining unprocessed blocks in its database

stacks-signer/src/chainstate.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,8 @@ impl SortitionsView {
589589
signer_db.block_lookup(&nakamoto_tip.signer_signature_hash())
590590
{
591591
if block_info.state != BlockState::GloballyAccepted {
592-
if let Err(e) = block_info.mark_globally_accepted() {
593-
warn!("Failed to update block info in db: {e}");
592+
if let Err(e) = signer_db.mark_block_globally_accepted(&mut block_info) {
593+
warn!("Failed to mark block as globally accepted: {e}");
594594
} else if let Err(e) = signer_db.insert_block(&block_info) {
595595
warn!("Failed to update block info in db: {e}");
596596
}

stacks-signer/src/signerdb.rs

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use blockstack_lib::util_lib::db::{
2424
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
2525
Error as DBError,
2626
};
27+
#[cfg(any(test, feature = "testing"))]
28+
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
2729
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
2830
use libsigner::BlockProposal;
2931
use rusqlite::functions::FunctionFlags;
@@ -209,7 +211,7 @@ impl BlockInfo {
209211

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

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

347+
static CREATE_INDEXES_6: &str = r#"
348+
CREATE INDEX IF NOT EXISTS block_validations_pending_on_added_time ON block_validations_pending(added_time ASC);
349+
"#;
350+
345351
static CREATE_SIGNER_STATE_TABLE: &str = "
346352
CREATE TABLE IF NOT EXISTS signer_states (
347353
reward_cycle INTEGER PRIMARY KEY,
@@ -436,23 +442,23 @@ INSERT INTO temp_blocks (
436442
broadcasted,
437443
stacks_height,
438444
burn_block_height,
439-
valid,
445+
valid,
440446
state,
441-
signed_group,
447+
signed_group,
442448
signed_self,
443449
proposed_time,
444450
validation_time_ms,
445451
tenure_change
446452
)
447-
SELECT
453+
SELECT
448454
signer_signature_hash,
449455
reward_cycle,
450456
block_info,
451457
consensus_hash,
452458
signed_over,
453459
broadcasted,
454460
stacks_height,
455-
burn_block_height,
461+
burn_block_height,
456462
json_extract(block_info, '$.valid') AS valid,
457463
json_extract(block_info, '$.state') AS state,
458464
json_extract(block_info, '$.signed_group') AS signed_group,
@@ -466,6 +472,14 @@ DROP TABLE blocks;
466472
467473
ALTER TABLE temp_blocks RENAME TO blocks;"#;
468474

475+
static CREATE_BLOCK_VALIDATION_PENDING_TABLE: &str = r#"
476+
CREATE TABLE IF NOT EXISTS block_validations_pending (
477+
signer_signature_hash TEXT NOT NULL,
478+
-- the time at which the block was added to the pending table
479+
added_time INTEGER NOT NULL,
480+
PRIMARY KEY (signer_signature_hash)
481+
) STRICT;"#;
482+
469483
static SCHEMA_1: &[&str] = &[
470484
DROP_SCHEMA_0,
471485
CREATE_DB_CONFIG,
@@ -514,9 +528,15 @@ static SCHEMA_5: &[&str] = &[
514528
"INSERT INTO db_config (version) VALUES (5);",
515529
];
516530

531+
static SCHEMA_6: &[&str] = &[
532+
CREATE_BLOCK_VALIDATION_PENDING_TABLE,
533+
CREATE_INDEXES_6,
534+
"INSERT OR REPLACE INTO db_config (version) VALUES (6);",
535+
];
536+
517537
impl SignerDb {
518538
/// The current schema version used in this build of the signer binary.
519-
pub const SCHEMA_VERSION: u32 = 5;
539+
pub const SCHEMA_VERSION: u32 = 6;
520540

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

639+
/// Migrate from schema 5 to schema 6
640+
fn schema_6_migration(tx: &Transaction) -> Result<(), DBError> {
641+
if Self::get_schema_version(tx)? >= 6 {
642+
// no migration necessary
643+
return Ok(());
644+
}
645+
646+
for statement in SCHEMA_6.iter() {
647+
tx.execute_batch(statement)?;
648+
}
649+
650+
Ok(())
651+
}
652+
619653
/// Register custom scalar functions used by the database
620654
fn register_scalar_functions(&self) -> Result<(), DBError> {
621655
// Register helper function for determining if a block is a tenure change transaction
@@ -654,7 +688,8 @@ impl SignerDb {
654688
2 => Self::schema_3_migration(&sql_tx)?,
655689
3 => Self::schema_4_migration(&sql_tx)?,
656690
4 => Self::schema_5_migration(&sql_tx)?,
657-
5 => break,
691+
5 => Self::schema_6_migration(&sql_tx)?,
692+
6 => break,
658693
x => return Err(DBError::Other(format!(
659694
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
660695
Self::SCHEMA_VERSION,
@@ -960,6 +995,43 @@ impl SignerDb {
960995
Ok(Some(broadcasted))
961996
}
962997

998+
/// Get a pending block validation, sorted by the time at which it was added to the pending table.
999+
/// If found, remove it from the pending table.
1000+
pub fn get_and_remove_pending_block_validation(
1001+
&self,
1002+
) -> Result<Option<Sha512Trunc256Sum>, DBError> {
1003+
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";
1004+
let args = params![];
1005+
let mut stmt = self.db.prepare(qry)?;
1006+
let sighash: Option<String> = stmt.query_row(args, |row| row.get(0)).optional()?;
1007+
Ok(sighash.and_then(|sighash| Sha512Trunc256Sum::from_hex(&sighash).ok()))
1008+
}
1009+
1010+
/// Remove a pending block validation
1011+
pub fn remove_pending_block_validation(
1012+
&self,
1013+
sighash: &Sha512Trunc256Sum,
1014+
) -> Result<(), DBError> {
1015+
self.db.execute(
1016+
"DELETE FROM block_validations_pending WHERE signer_signature_hash = ?1",
1017+
params![sighash.to_string()],
1018+
)?;
1019+
Ok(())
1020+
}
1021+
1022+
/// Insert a pending block validation
1023+
pub fn insert_pending_block_validation(
1024+
&self,
1025+
sighash: &Sha512Trunc256Sum,
1026+
ts: u64,
1027+
) -> Result<(), DBError> {
1028+
self.db.execute(
1029+
"INSERT INTO block_validations_pending (signer_signature_hash, added_time) VALUES (?1, ?2)",
1030+
params![sighash.to_string(), u64_to_sql(ts)?],
1031+
)?;
1032+
Ok(())
1033+
}
1034+
9631035
/// Return the start time (epoch time in seconds) and the processing time in milliseconds of the tenure (idenfitied by consensus_hash).
9641036
fn get_tenure_times(&self, tenure: &ConsensusHash) -> Result<(u64, u64), DBError> {
9651037
let query = "SELECT tenure_change, proposed_time, validation_time_ms FROM blocks WHERE consensus_hash = ?1 AND state = ?2 ORDER BY stacks_height DESC";
@@ -1022,6 +1094,26 @@ impl SignerDb {
10221094
);
10231095
tenure_extend_timestamp
10241096
}
1097+
1098+
/// Mark a block as globally accepted. This removes the block from the pending
1099+
/// validations table. This does **not** update the block's state in SignerDb.
1100+
pub fn mark_block_globally_accepted(&self, block_info: &mut BlockInfo) -> Result<(), DBError> {
1101+
block_info
1102+
.mark_globally_accepted()
1103+
.map_err(DBError::Other)?;
1104+
self.remove_pending_block_validation(&block_info.signer_signature_hash())?;
1105+
Ok(())
1106+
}
1107+
1108+
/// Mark a block as globally rejected. This removes the block from the pending
1109+
/// validations table. This does **not** update the block's state in SignerDb.
1110+
pub fn mark_block_globally_rejected(&self, block_info: &mut BlockInfo) -> Result<(), DBError> {
1111+
block_info
1112+
.mark_globally_rejected()
1113+
.map_err(DBError::Other)?;
1114+
self.remove_pending_block_validation(&block_info.signer_signature_hash())?;
1115+
Ok(())
1116+
}
10251117
}
10261118

10271119
fn try_deserialize<T>(s: Option<String>) -> Result<Option<T>, DBError>
@@ -1034,6 +1126,50 @@ where
10341126
.map_err(DBError::SerializationError)
10351127
}
10361128

1129+
/// For tests, a struct to represent a pending block validation
1130+
#[cfg(any(test, feature = "testing"))]
1131+
pub struct PendingBlockValidation {
1132+
/// The signer signature hash of the block
1133+
pub signer_signature_hash: Sha512Trunc256Sum,
1134+
/// The time at which the block was added to the pending table
1135+
pub added_time: u64,
1136+
}
1137+
1138+
#[cfg(any(test, feature = "testing"))]
1139+
impl FromRow<PendingBlockValidation> for PendingBlockValidation {
1140+
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
1141+
let signer_signature_hash = Sha512Trunc256Sum::from_column(row, "signer_signature_hash")?;
1142+
let added_time = row.get_unwrap(1);
1143+
Ok(PendingBlockValidation {
1144+
signer_signature_hash,
1145+
added_time,
1146+
})
1147+
}
1148+
}
1149+
1150+
#[cfg(any(test, feature = "testing"))]
1151+
impl SignerDb {
1152+
/// For tests, fetch all pending block validations
1153+
pub fn get_all_pending_block_validations(
1154+
&self,
1155+
) -> Result<Vec<PendingBlockValidation>, DBError> {
1156+
let qry = "SELECT signer_signature_hash, added_time FROM block_validations_pending ORDER BY added_time ASC";
1157+
query_rows(&self.db, qry, params![])
1158+
}
1159+
1160+
/// For tests, check if a pending block validation exists
1161+
pub fn has_pending_block_validation(
1162+
&self,
1163+
sighash: &Sha512Trunc256Sum,
1164+
) -> Result<bool, DBError> {
1165+
let qry = "SELECT signer_signature_hash FROM block_validations_pending WHERE signer_signature_hash = ?1";
1166+
let args = params![sighash.to_string()];
1167+
let sighash_opt: Option<String> = query_row(&self.db, qry, args)?;
1168+
Ok(sighash_opt.is_some())
1169+
}
1170+
}
1171+
1172+
/// Tests for SignerDb
10371173
#[cfg(test)]
10381174
mod tests {
10391175
use std::fs;
@@ -1734,4 +1870,38 @@ mod tests {
17341870
< block_infos[0].proposed_time
17351871
);
17361872
}
1873+
1874+
#[test]
1875+
fn test_get_and_remove_pending_block_validation() {
1876+
let db_path = tmp_db_path();
1877+
let db = SignerDb::new(db_path).expect("Failed to create signer db");
1878+
1879+
let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
1880+
assert!(pending_hash.is_none());
1881+
1882+
db.insert_pending_block_validation(&Sha512Trunc256Sum([0x01; 32]), 1000)
1883+
.unwrap();
1884+
db.insert_pending_block_validation(&Sha512Trunc256Sum([0x02; 32]), 2000)
1885+
.unwrap();
1886+
db.insert_pending_block_validation(&Sha512Trunc256Sum([0x03; 32]), 3000)
1887+
.unwrap();
1888+
1889+
let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
1890+
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x01; 32])));
1891+
1892+
let pendings = db.get_all_pending_block_validations().unwrap();
1893+
assert_eq!(pendings.len(), 2);
1894+
1895+
let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
1896+
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x02; 32])));
1897+
1898+
let pendings = db.get_all_pending_block_validations().unwrap();
1899+
assert_eq!(pendings.len(), 1);
1900+
1901+
let pending_hash = db.get_and_remove_pending_block_validation().unwrap();
1902+
assert_eq!(pending_hash, Some(Sha512Trunc256Sum([0x03; 32])));
1903+
1904+
let pendings = db.get_all_pending_block_validations().unwrap();
1905+
assert_eq!(pendings.len(), 0);
1906+
}
17371907
}

0 commit comments

Comments
 (0)