Skip to content

Commit fb682c3

Browse files
committed
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into feat/miner-continues-tenure-if-tenure-empty
2 parents 71eadc4 + ef96476 commit fb682c3

File tree

19 files changed

+737
-97
lines changed

19 files changed

+737
-97
lines changed

.github/workflows/pr-differences-mutants.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
name: PR Differences Mutants
22

33
on:
4-
pull_request:
5-
types:
6-
- opened
7-
- reopened
8-
- synchronize
9-
- ready_for_review
10-
paths:
11-
- '**.rs'
4+
# Disabling PR mutants (issue #5806)
5+
# pull_request:
6+
# types:
7+
# - opened
8+
# - reopened
9+
# - synchronize
10+
# - ready_for_review
11+
# paths:
12+
# - '**.rs'
1213
workflow_dispatch:
1314

1415
concurrency:

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1313

1414
### Fixed
1515

16+
- Error responses to /v2/transactions/fees are once again expressed as JSON ([#4145](https://github.com/stacks-network/stacks-core/issues/4145)).
17+
1618
## [3.1.0.0.5]
1719

1820
### Added

stacks-signer/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
77

88
## [Unreleased]
99

10-
### Added
10+
## Added
11+
12+
- Introduced the `reorg_attempts_activity_timeout_ms` configuration option for signers which is used to determine the length of time after the last block of a tenure is confirmed that an incoming miner's attempts to reorg it are considered valid miner activity.
1113

1214
### Changed
1315

16+
- Increase default `block_proposal_timeout_ms` from 10 minutes to 4 hours. Until #5729 is implemented, there is no value in rejecting a late block from a miner, since a late block is better than no block at all.
17+
18+
- Signers no longer view any block proposal by a miner in their DB as indicative of valid miner activity.
19+
1420
## [3.1.0.0.5.0]
1521

1622
### Added

stacks-signer/src/chainstate.rs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,25 @@ impl SortitionState {
8989
if self.miner_status != SortitionMinerStatus::Valid {
9090
return Ok(false);
9191
}
92-
// if we've already seen a proposed block from this miner. It cannot have timed out.
93-
let has_blocks = signer_db.has_proposed_block_in_tenure(&self.consensus_hash)?;
94-
if has_blocks {
92+
// if we've already signed a block in this tenure, the miner can't have timed out.
93+
let has_block = signer_db.has_signed_block_in_tenure(&self.consensus_hash)?;
94+
if has_block {
9595
return Ok(false);
9696
}
9797
let Some(received_ts) = signer_db.get_burn_block_receive_time(&self.burn_block_hash)?
9898
else {
9999
return Ok(false);
100100
};
101101
let received_time = UNIX_EPOCH + Duration::from_secs(received_ts);
102-
let Ok(elapsed) = std::time::SystemTime::now().duration_since(received_time) else {
102+
let last_activity = signer_db
103+
.get_last_activity_time(&self.consensus_hash)?
104+
.map(|time| UNIX_EPOCH + Duration::from_secs(time))
105+
.unwrap_or(received_time);
106+
107+
let Ok(elapsed) = std::time::SystemTime::now().duration_since(last_activity) else {
103108
return Ok(false);
104109
};
105-
if elapsed > timeout {
106-
return Ok(true);
107-
}
108-
Ok(false)
110+
Ok(elapsed > timeout)
109111
}
110112
}
111113

@@ -122,6 +124,9 @@ pub struct ProposalEvalConfig {
122124
pub tenure_last_block_proposal_timeout: Duration,
123125
/// How much idle time must pass before allowing a tenure extend
124126
pub tenure_idle_timeout: Duration,
127+
/// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by
128+
/// the new miner to reorg it as valid towards miner activity
129+
pub reorg_attempts_activity_timeout: Duration,
125130
}
126131

127132
impl From<&SignerConfig> for ProposalEvalConfig {
@@ -131,6 +136,7 @@ impl From<&SignerConfig> for ProposalEvalConfig {
131136
block_proposal_timeout: value.block_proposal_timeout,
132137
tenure_last_block_proposal_timeout: value.tenure_last_block_proposal_timeout,
133138
tenure_idle_timeout: value.tenure_idle_timeout,
139+
reorg_attempts_activity_timeout: value.reorg_attempts_activity_timeout,
134140
}
135141
}
136142
}
@@ -545,8 +551,10 @@ impl SortitionsView {
545551
signer_db: &mut SignerDb,
546552
client: &StacksClient,
547553
tenure_last_block_proposal_timeout: Duration,
554+
reorg_attempts_activity_timeout: Duration,
548555
) -> Result<bool, ClientError> {
549556
// If the tenure change block confirms the expected parent block, it should confirm at least one more block than the last accepted block in the parent tenure.
557+
// NOTE: returns the locally accepted block if it is not timed out, otherwise it will return the last globally accepted block.
550558
let last_block_info = Self::get_tenure_last_block_info(
551559
&tenure_change.prev_tenure_consensus_hash,
552560
signer_db,
@@ -566,6 +574,21 @@ impl SortitionsView {
566574
"proposed_chain_length" => block.header.chain_length,
567575
"expected_at_least" => info.block.header.chain_length + 1,
568576
);
577+
if info.signed_group.map_or(true, |signed_time| {
578+
signed_time + reorg_attempts_activity_timeout.as_secs() > get_epoch_time_secs()
579+
}) {
580+
// Note if there is no signed_group time, this is a locally accepted block (i.e. tenure_last_block_proposal_timeout has not been exceeded).
581+
// Treat any attempt to reorg a locally accepted block as valid miner activity.
582+
// If the call returns a globally accepted block, check its globally accepted time against a quarter of the block_proposal_timeout
583+
// to give the miner some extra buffer time to wait for its chain tip to advance
584+
// The miner may just be slow, so count this invalid block proposal towards valid miner activity.
585+
if let Err(e) = signer_db.update_last_activity_time(
586+
&tenure_change.tenure_consensus_hash,
587+
get_epoch_time_secs(),
588+
) {
589+
warn!("Failed to update last activity time: {e}");
590+
}
591+
}
569592
return Ok(false);
570593
}
571594
}
@@ -631,6 +654,7 @@ impl SortitionsView {
631654
signer_db,
632655
client,
633656
self.config.tenure_last_block_proposal_timeout,
657+
self.config.reorg_attempts_activity_timeout,
634658
)?;
635659
if !confirms_expected_parent {
636660
return Ok(false);

stacks-signer/src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ pub(crate) mod tests {
417417
block_proposal_validation_timeout: config.block_proposal_validation_timeout,
418418
tenure_idle_timeout: config.tenure_idle_timeout,
419419
block_proposal_max_age_secs: config.block_proposal_max_age_secs,
420+
reorg_attempts_activity_timeout: config.reorg_attempts_activity_timeout,
420421
}
421422
}
422423

stacks-signer/src/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60;
4141
const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30;
4242
const DEFAULT_DRY_RUN: bool = false;
4343
const TENURE_IDLE_TIMEOUT_SECS: u64 = 120;
44+
const DEFAULT_REORG_ATTEMPTS_ACTIVITY_TIMEOUT_MS: u64 = 200_000;
4445

4546
#[derive(thiserror::Error, Debug)]
4647
/// An error occurred parsing the provided configuration
@@ -163,6 +164,9 @@ pub struct SignerConfig {
163164
pub tenure_idle_timeout: Duration,
164165
/// The maximum age of a block proposal in seconds that will be processed by the signer
165166
pub block_proposal_max_age_secs: u64,
167+
/// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by
168+
/// the new miner to reorg it as valid towards miner activity
169+
pub reorg_attempts_activity_timeout: Duration,
166170
/// The running mode for the signer (dry-run or normal)
167171
pub signer_mode: SignerConfigMode,
168172
}
@@ -205,6 +209,9 @@ pub struct GlobalConfig {
205209
pub tenure_idle_timeout: Duration,
206210
/// The maximum age of a block proposal that will be processed by the signer
207211
pub block_proposal_max_age_secs: u64,
212+
/// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by
213+
/// the new miner to reorg it as valid towards miner activity
214+
pub reorg_attempts_activity_timeout: Duration,
208215
/// Is this signer binary going to be running in dry-run mode?
209216
pub dry_run: bool,
210217
}
@@ -246,6 +253,9 @@ struct RawConfigFile {
246253
pub tenure_idle_timeout_secs: Option<u64>,
247254
/// The maximum age of a block proposal (in secs) that will be processed by the signer.
248255
pub block_proposal_max_age_secs: Option<u64>,
256+
/// Time (in millisecs) following a block's global acceptance that a signer will consider an attempt by a miner
257+
/// to reorg the block as valid towards miner activity
258+
pub reorg_attempts_activity_timeout_ms: Option<u64>,
249259
/// Is this signer binary going to be running in dry-run mode?
250260
pub dry_run: Option<bool>,
251261
}
@@ -349,6 +359,12 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
349359
.block_proposal_max_age_secs
350360
.unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS);
351361

362+
let reorg_attempts_activity_timeout = Duration::from_millis(
363+
raw_data
364+
.reorg_attempts_activity_timeout_ms
365+
.unwrap_or(DEFAULT_REORG_ATTEMPTS_ACTIVITY_TIMEOUT_MS),
366+
);
367+
352368
let dry_run = raw_data.dry_run.unwrap_or(DEFAULT_DRY_RUN);
353369

354370
Ok(Self {
@@ -368,6 +384,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
368384
block_proposal_validation_timeout,
369385
tenure_idle_timeout,
370386
block_proposal_max_age_secs,
387+
reorg_attempts_activity_timeout,
371388
dry_run,
372389
})
373390
}

stacks-signer/src/runloop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
315315
block_proposal_validation_timeout: self.config.block_proposal_validation_timeout,
316316
tenure_idle_timeout: self.config.tenure_idle_timeout,
317317
block_proposal_max_age_secs: self.config.block_proposal_max_age_secs,
318+
reorg_attempts_activity_timeout: self.config.reorg_attempts_activity_timeout,
318319
}))
319320
}
320321

stacks-signer/src/signerdb.rs

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ CREATE TABLE IF NOT EXISTS block_validations_pending (
480480
PRIMARY KEY (signer_signature_hash)
481481
) STRICT;"#;
482482

483+
static CREATE_TENURE_ACTIVTY_TABLE: &str = r#"
484+
CREATE TABLE IF NOT EXISTS tenure_activity (
485+
consensus_hash TEXT NOT NULL PRIMARY KEY,
486+
last_activity_time INTEGER NOT NULL
487+
) STRICT;"#;
488+
483489
static SCHEMA_1: &[&str] = &[
484490
DROP_SCHEMA_0,
485491
CREATE_DB_CONFIG,
@@ -534,9 +540,14 @@ static SCHEMA_6: &[&str] = &[
534540
"INSERT OR REPLACE INTO db_config (version) VALUES (6);",
535541
];
536542

543+
static SCHEMA_7: &[&str] = &[
544+
CREATE_TENURE_ACTIVTY_TABLE,
545+
"INSERT OR REPLACE INTO db_config (version) VALUES (7);",
546+
];
547+
537548
impl SignerDb {
538549
/// The current schema version used in this build of the signer binary.
539-
pub const SCHEMA_VERSION: u32 = 6;
550+
pub const SCHEMA_VERSION: u32 = 7;
540551

541552
/// Create a new `SignerState` instance.
542553
/// This will create a new SQLite database at the given path
@@ -650,6 +661,20 @@ impl SignerDb {
650661
Ok(())
651662
}
652663

664+
/// Migrate from schema 6 to schema 7
665+
fn schema_7_migration(tx: &Transaction) -> Result<(), DBError> {
666+
if Self::get_schema_version(tx)? >= 7 {
667+
// no migration necessary
668+
return Ok(());
669+
}
670+
671+
for statement in SCHEMA_7.iter() {
672+
tx.execute_batch(statement)?;
673+
}
674+
675+
Ok(())
676+
}
677+
653678
/// Register custom scalar functions used by the database
654679
fn register_scalar_functions(&self) -> Result<(), DBError> {
655680
// Register helper function for determining if a block is a tenure change transaction
@@ -689,7 +714,8 @@ impl SignerDb {
689714
3 => Self::schema_4_migration(&sql_tx)?,
690715
4 => Self::schema_5_migration(&sql_tx)?,
691716
5 => Self::schema_6_migration(&sql_tx)?,
692-
6 => break,
717+
6 => Self::schema_7_migration(&sql_tx)?,
718+
7 => break,
693719
x => return Err(DBError::Other(format!(
694720
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
695721
Self::SCHEMA_VERSION,
@@ -746,10 +772,10 @@ impl SignerDb {
746772
try_deserialize(result)
747773
}
748774

749-
/// Return whether a block proposal has been stored for a tenure (identified by its consensus hash)
750-
/// Does not consider the block's state.
751-
pub fn has_proposed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result<bool, DBError> {
752-
let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? LIMIT 1";
775+
/// Return whether there was signed block in a tenure (identified by its consensus hash)
776+
pub fn has_signed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result<bool, DBError> {
777+
let query =
778+
"SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 LIMIT 1";
753779
let result: Option<String> = query_row(&self.db, query, [tenure])?;
754780

755781
Ok(result.is_some())
@@ -1112,6 +1138,30 @@ impl SignerDb {
11121138
self.remove_pending_block_validation(&block_info.signer_signature_hash())?;
11131139
Ok(())
11141140
}
1141+
/// Update the tenure (identified by consensus_hash) last activity timestamp
1142+
pub fn update_last_activity_time(
1143+
&mut self,
1144+
tenure: &ConsensusHash,
1145+
last_activity_time: u64,
1146+
) -> Result<(), DBError> {
1147+
debug!("Updating last activity for tenure"; "consensus_hash" => %tenure, "last_activity_time" => last_activity_time);
1148+
self.db.execute("INSERT OR REPLACE INTO tenure_activity (consensus_hash, last_activity_time) VALUES (?1, ?2)", params![tenure, u64_to_sql(last_activity_time)?])?;
1149+
Ok(())
1150+
}
1151+
1152+
/// Get the last activity timestamp for a tenure (identified by consensus_hash)
1153+
pub fn get_last_activity_time(&self, tenure: &ConsensusHash) -> Result<Option<u64>, DBError> {
1154+
let query =
1155+
"SELECT last_activity_time FROM tenure_activity WHERE consensus_hash = ? LIMIT 1";
1156+
let Some(last_activity_time_i64) = query_row::<i64, _>(&self.db, query, &[tenure])? else {
1157+
return Ok(None);
1158+
};
1159+
let last_activity_time = u64::try_from(last_activity_time_i64).map_err(|e| {
1160+
error!("Failed to parse db last_activity_time as u64: {e}");
1161+
DBError::Corruption
1162+
})?;
1163+
Ok(Some(last_activity_time))
1164+
}
11151165
}
11161166

11171167
fn try_deserialize<T>(s: Option<String>) -> Result<Option<T>, DBError>
@@ -1903,7 +1953,7 @@ mod tests {
19031953
}
19041954

19051955
#[test]
1906-
fn has_proposed_block() {
1956+
fn has_signed_block() {
19071957
let db_path = tmp_db_path();
19081958
let consensus_hash_1 = ConsensusHash([0x01; 20]);
19091959
let consensus_hash_2 = ConsensusHash([0x02; 20]);
@@ -1913,16 +1963,59 @@ mod tests {
19131963
b.block.header.chain_length = 1;
19141964
});
19151965

1916-
assert!(!db.has_proposed_block_in_tenure(&consensus_hash_1).unwrap());
1917-
assert!(!db.has_proposed_block_in_tenure(&consensus_hash_2).unwrap());
1966+
assert!(!db.has_signed_block_in_tenure(&consensus_hash_1).unwrap());
1967+
assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap());
19181968

1969+
block_info.signed_over = true;
19191970
db.insert_block(&block_info).unwrap();
19201971

1972+
assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap());
1973+
assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap());
1974+
1975+
block_info.block.header.consensus_hash = consensus_hash_2;
19211976
block_info.block.header.chain_length = 2;
1977+
block_info.signed_over = false;
19221978

19231979
db.insert_block(&block_info).unwrap();
19241980

1925-
assert!(db.has_proposed_block_in_tenure(&consensus_hash_1).unwrap());
1926-
assert!(!db.has_proposed_block_in_tenure(&consensus_hash_2).unwrap());
1981+
assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap());
1982+
assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap());
1983+
1984+
block_info.signed_over = true;
1985+
1986+
db.insert_block(&block_info).unwrap();
1987+
1988+
assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap());
1989+
assert!(db.has_signed_block_in_tenure(&consensus_hash_2).unwrap());
1990+
}
1991+
1992+
#[test]
1993+
fn update_last_activity() {
1994+
let db_path = tmp_db_path();
1995+
let consensus_hash_1 = ConsensusHash([0x01; 20]);
1996+
let consensus_hash_2 = ConsensusHash([0x02; 20]);
1997+
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
1998+
1999+
assert!(db
2000+
.get_last_activity_time(&consensus_hash_1)
2001+
.unwrap()
2002+
.is_none());
2003+
assert!(db
2004+
.get_last_activity_time(&consensus_hash_2)
2005+
.unwrap()
2006+
.is_none());
2007+
2008+
let time = get_epoch_time_secs();
2009+
db.update_last_activity_time(&consensus_hash_1, time)
2010+
.unwrap();
2011+
let retrieved_time = db
2012+
.get_last_activity_time(&consensus_hash_1)
2013+
.unwrap()
2014+
.unwrap();
2015+
assert_eq!(time, retrieved_time);
2016+
assert!(db
2017+
.get_last_activity_time(&consensus_hash_2)
2018+
.unwrap()
2019+
.is_none());
19272020
}
19282021
}

0 commit comments

Comments
 (0)