Skip to content

Commit ce5d2a3

Browse files
committed
feat: mark a miner as invalid once signers have rejected its reorg attempt
1 parent bf79d64 commit ce5d2a3

File tree

3 files changed

+128
-23
lines changed

3 files changed

+128
-23
lines changed

stacks-signer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1818
- Signers no longer view any block proposal by a miner in their DB as indicative of valid miner activity.
1919
- Various index improvements to the signer's database to improve performance.
2020
- Add new reject codes to the signer response for better visibility into why a block was rejected.
21+
- When allowing a reorg within the `reorg_attempts_activity_timeout_ms`, the signer will now watch the responses from other signers and if >30% of them reject this reorg attempt, then the signer will mark the miner as invalid, reject further attempts to reorg and allowing the previous miner to extend their tenure.
2122

2223
## [3.1.0.0.5.0]
2324

stacks-signer/src/signerdb.rs

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use blockstack_lib::util_lib::db::{
2727
#[cfg(any(test, feature = "testing"))]
2828
use blockstack_lib::util_lib::db::{FromColumn, FromRow};
2929
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
30+
use clarity::types::Address;
31+
use libsigner::v0::messages::{RejectReason, RejectReasonPrefix};
3032
use libsigner::BlockProposal;
3133
use rusqlite::functions::FunctionFlags;
3234
use rusqlite::{
@@ -500,6 +502,11 @@ CREATE TABLE IF NOT EXISTS tenure_activity (
500502
last_activity_time INTEGER NOT NULL
501503
) STRICT;"#;
502504

505+
static ADD_REJECT_CODE: &str = r#"
506+
ALTER TABLE block_rejection_signer_addrs
507+
ADD COLUMN reject_code INTEGER;
508+
"#;
509+
503510
static SCHEMA_1: &[&str] = &[
504511
DROP_SCHEMA_0,
505512
CREATE_DB_CONFIG,
@@ -564,9 +571,14 @@ static SCHEMA_8: &[&str] = &[
564571
"INSERT INTO db_config (version) VALUES (8);",
565572
];
566573

574+
static SCHEMA_9: &[&str] = &[
575+
ADD_REJECT_CODE,
576+
"INSERT INTO db_config (version) VALUES (9);",
577+
];
578+
567579
impl SignerDb {
568580
/// The current schema version used in this build of the signer binary.
569-
pub const SCHEMA_VERSION: u32 = 8;
581+
pub const SCHEMA_VERSION: u32 = 9;
570582

571583
/// Create a new `SignerState` instance.
572584
/// This will create a new SQLite database at the given path
@@ -708,6 +720,20 @@ impl SignerDb {
708720
Ok(())
709721
}
710722

723+
/// Migrate from schema 9 to schema 9
724+
fn schema_9_migration(tx: &Transaction) -> Result<(), DBError> {
725+
if Self::get_schema_version(tx)? >= 9 {
726+
// no migration necessary
727+
return Ok(());
728+
}
729+
730+
for statement in SCHEMA_9.iter() {
731+
tx.execute_batch(statement)?;
732+
}
733+
734+
Ok(())
735+
}
736+
711737
/// Register custom scalar functions used by the database
712738
fn register_scalar_functions(&self) -> Result<(), DBError> {
713739
// Register helper function for determining if a block is a tenure change transaction
@@ -749,7 +775,8 @@ impl SignerDb {
749775
5 => Self::schema_6_migration(&sql_tx)?,
750776
6 => Self::schema_7_migration(&sql_tx)?,
751777
7 => Self::schema_8_migration(&sql_tx)?,
752-
8 => break,
778+
8 => Self::schema_9_migration(&sql_tx)?,
779+
9 => break,
753780
x => return Err(DBError::Other(format!(
754781
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
755782
Self::SCHEMA_VERSION,
@@ -998,27 +1025,58 @@ impl SignerDb {
9981025
&self,
9991026
block_sighash: &Sha512Trunc256Sum,
10001027
addr: &StacksAddress,
1028+
reject_reason: &RejectReason,
10011029
) -> Result<(), DBError> {
1002-
let qry = "INSERT OR REPLACE INTO block_rejection_signer_addrs (signer_signature_hash, signer_addr) VALUES (?1, ?2);";
1003-
let args = params![block_sighash, addr.to_string(),];
1030+
let qry = "INSERT OR REPLACE INTO block_rejection_signer_addrs (signer_signature_hash, signer_addr, reject_code) VALUES (?1, ?2, ?3);";
1031+
let args = params![
1032+
block_sighash,
1033+
addr.to_string(),
1034+
RejectReasonPrefix::from(reject_reason) as i64
1035+
];
10041036

10051037
debug!("Inserting block rejection.";
1006-
"block_sighash" => %block_sighash,
1007-
"signer_address" => %addr);
1038+
"block_sighash" => %block_sighash,
1039+
"signer_address" => %addr,
1040+
"reject_reason" => %reject_reason
1041+
);
10081042

10091043
self.db.execute(qry, args)?;
10101044
Ok(())
10111045
}
10121046

1013-
/// Get all signer addresses that rejected the block
1047+
/// Get all signer addresses that rejected the block (and their reject codes)
10141048
pub fn get_block_rejection_signer_addrs(
10151049
&self,
10161050
block_sighash: &Sha512Trunc256Sum,
1017-
) -> Result<Vec<StacksAddress>, DBError> {
1051+
) -> Result<Vec<(StacksAddress, RejectReasonPrefix)>, DBError> {
10181052
let qry =
1019-
"SELECT signer_addr FROM block_rejection_signer_addrs WHERE signer_signature_hash = ?1";
1053+
"SELECT signer_addr, reject_code FROM block_rejection_signer_addrs WHERE signer_signature_hash = ?1";
10201054
let args = params![block_sighash];
1021-
query_rows(&self.db, qry, args)
1055+
let mut stmt = self.db.prepare(qry)?;
1056+
1057+
let rows = stmt.query_map(args, |row| {
1058+
let addr: String = row.get(0)?;
1059+
let addr = StacksAddress::from_string(&addr).ok_or(SqliteError::InvalidColumnType(
1060+
0,
1061+
"signer_addr".into(),
1062+
rusqlite::types::Type::Text,
1063+
))?;
1064+
let reject_code: i64 = row.get(1)?;
1065+
1066+
let reject_code = u8::try_from(reject_code)
1067+
.map_err(|_| {
1068+
SqliteError::InvalidColumnType(
1069+
1,
1070+
"reject_code".into(),
1071+
rusqlite::types::Type::Integer,
1072+
)
1073+
})
1074+
.map(RejectReasonPrefix::from)?;
1075+
1076+
Ok((addr, reject_code))
1077+
})?;
1078+
1079+
rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.into())
10221080
}
10231081

10241082
/// Mark a block as having been broadcasted and therefore GloballyAccepted

stacks-signer/src/v0/signer.rs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use clarity::util::hash::{MerkleHashFunc, Sha512Trunc256Sum};
2828
use clarity::util::secp256k1::Secp256k1PublicKey;
2929
use libsigner::v0::messages::{
3030
BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MockProposal, MockSignature,
31-
RejectReason, SignerMessage,
31+
RejectReason, RejectReasonPrefix, SignerMessage,
3232
};
3333
use libsigner::{BlockProposal, SignerEvent};
3434
use slog::{slog_debug, slog_error, slog_info, slog_warn};
@@ -37,7 +37,7 @@ use stacks_common::util::get_epoch_time_secs;
3737
use stacks_common::util::secp256k1::MessageSignature;
3838
use stacks_common::{debug, error, info, warn};
3939

40-
use crate::chainstate::{ProposalEvalConfig, SortitionsView};
40+
use crate::chainstate::{ProposalEvalConfig, SortitionMinerStatus, SortitionsView};
4141
use crate::client::{ClientError, SignerSlotID, StackerDB, StacksClient};
4242
use crate::config::{SignerConfig, SignerConfigMode};
4343
use crate::runloop::SignerResult;
@@ -165,7 +165,11 @@ impl SignerTrait<SignerMessage> for Signer {
165165
match event {
166166
SignerEvent::BlockValidationResponse(block_validate_response) => {
167167
debug!("{self}: Received a block proposal result from the stacks node...");
168-
self.handle_block_validate_response(stacks_client, block_validate_response)
168+
self.handle_block_validate_response(
169+
stacks_client,
170+
block_validate_response,
171+
sortition_state,
172+
)
169173
}
170174
SignerEvent::SignerMessages(_signer_set, messages) => {
171175
debug!(
@@ -177,7 +181,7 @@ impl SignerTrait<SignerMessage> for Signer {
177181
let SignerMessage::BlockResponse(block_response) = message else {
178182
continue;
179183
};
180-
self.handle_block_response(stacks_client, block_response);
184+
self.handle_block_response(stacks_client, block_response, sortition_state);
181185
}
182186
}
183187
SignerEvent::MinerMessages(messages, miner_pubkey) => {
@@ -618,13 +622,14 @@ impl Signer {
618622
&mut self,
619623
stacks_client: &StacksClient,
620624
block_response: &BlockResponse,
625+
sortition_state: &mut Option<SortitionsView>,
621626
) {
622627
match block_response {
623628
BlockResponse::Accepted(accepted) => {
624629
self.handle_block_signature(stacks_client, accepted);
625630
}
626631
BlockResponse::Rejected(block_rejection) => {
627-
self.handle_block_rejection(block_rejection);
632+
self.handle_block_rejection(block_rejection, sortition_state);
628633
}
629634
};
630635
}
@@ -793,6 +798,7 @@ impl Signer {
793798
fn handle_block_validate_reject(
794799
&mut self,
795800
block_validate_reject: &BlockValidateReject,
801+
sortition_state: &mut Option<SortitionsView>,
796802
) -> Option<BlockResponse> {
797803
crate::monitoring::actions::increment_block_validation_responses(false);
798804
let signer_signature_hash = block_validate_reject.signer_signature_hash;
@@ -833,7 +839,7 @@ impl Signer {
833839
self.signer_db
834840
.insert_block(&block_info)
835841
.unwrap_or_else(|e| self.handle_insert_block_error(e));
836-
self.handle_block_rejection(&block_rejection);
842+
self.handle_block_rejection(&block_rejection, sortition_state);
837843
Some(BlockResponse::Rejected(block_rejection))
838844
}
839845

@@ -842,6 +848,7 @@ impl Signer {
842848
&mut self,
843849
stacks_client: &StacksClient,
844850
block_validate_response: &BlockValidateResponse,
851+
sortition_state: &mut Option<SortitionsView>,
845852
) {
846853
info!("{self}: Received a block validate response: {block_validate_response:?}");
847854
let block_response = match block_validate_response {
@@ -852,7 +859,7 @@ impl Signer {
852859
self.handle_block_validate_ok(stacks_client, block_validate_ok)
853860
}
854861
BlockValidateResponse::Reject(block_validate_reject) => {
855-
self.handle_block_validate_reject(block_validate_reject)
862+
self.handle_block_validate_reject(block_validate_reject, sortition_state)
856863
}
857864
};
858865
// Remove this block validation from the pending table
@@ -994,6 +1001,21 @@ impl Signer {
9941001
})
9951002
}
9961003

1004+
/// Compute the rejection weight for the given reject code, given a list of signatures
1005+
fn compute_reject_code_signing_weight<'a>(
1006+
&self,
1007+
addrs: impl Iterator<Item = &'a (StacksAddress, RejectReasonPrefix)>,
1008+
reject_code: RejectReasonPrefix,
1009+
) -> u32 {
1010+
addrs.filter(|(_, code)| *code == reject_code).fold(
1011+
0u32,
1012+
|signing_weight, (stacker_address, _)| {
1013+
let stacker_weight = self.signer_weights.get(stacker_address).unwrap_or(&0);
1014+
signing_weight.saturating_add(*stacker_weight)
1015+
},
1016+
)
1017+
}
1018+
9971019
/// Compute the total signing weight
9981020
fn compute_signature_total_weight(&self) -> u32 {
9991021
self.signer_weights
@@ -1002,7 +1024,11 @@ impl Signer {
10021024
}
10031025

10041026
/// Handle an observed rejection from another signer
1005-
fn handle_block_rejection(&mut self, rejection: &BlockRejection) {
1027+
fn handle_block_rejection(
1028+
&mut self,
1029+
rejection: &BlockRejection,
1030+
sortition_state: &mut Option<SortitionsView>,
1031+
) {
10061032
debug!("{self}: Received a block-reject signature: {rejection:?}");
10071033

10081034
let block_hash = &rejection.signer_signature_hash;
@@ -1045,10 +1071,11 @@ impl Signer {
10451071
}
10461072

10471073
// signature is valid! store it
1048-
if let Err(e) = self
1049-
.signer_db
1050-
.add_block_rejection_signer_addr(block_hash, &signer_address)
1051-
{
1074+
if let Err(e) = self.signer_db.add_block_rejection_signer_addr(
1075+
block_hash,
1076+
&signer_address,
1077+
&rejection.response_data.reject_reason,
1078+
) {
10521079
warn!("{self}: Failed to save block rejection signature: {e:?}",);
10531080
}
10541081

@@ -1061,7 +1088,8 @@ impl Signer {
10611088
return;
10621089
}
10631090
};
1064-
let total_reject_weight = self.compute_signature_signing_weight(rejection_addrs.iter());
1091+
let total_reject_weight =
1092+
self.compute_signature_signing_weight(rejection_addrs.iter().map(|(addr, _)| addr));
10651093
let total_weight = self.compute_signature_total_weight();
10661094

10671095
let min_weight = NakamotoBlockHeader::compute_voting_weight_threshold(total_weight)
@@ -1089,6 +1117,24 @@ impl Signer {
10891117
// Consensus reached! No longer bother tracking its validation submission to the node as we are too late to participate in the decision anyway.
10901118
self.submitted_block_proposal = None;
10911119
}
1120+
1121+
// If 30% of the signers have rejected the block due to an invalid
1122+
// reorg, mark the miner as invalid.
1123+
let total_reorg_reject_weight = self.compute_reject_code_signing_weight(
1124+
rejection_addrs.iter(),
1125+
RejectReasonPrefix::ReorgNotAllowed,
1126+
);
1127+
if total_reorg_reject_weight.saturating_add(min_weight) > total_weight {
1128+
// Mark the miner as invalid
1129+
if let Some(sortition_state) = sortition_state {
1130+
let ch = block_info.block.header.consensus_hash;
1131+
if sortition_state.cur_sortition.consensus_hash == ch {
1132+
info!("{self}: Marking miner as invalid for attempted reorg");
1133+
sortition_state.cur_sortition.miner_status =
1134+
SortitionMinerStatus::InvalidatedBeforeFirstBlock;
1135+
}
1136+
}
1137+
}
10921138
}
10931139

10941140
/// Handle an observed signature from another signer

0 commit comments

Comments
 (0)