Skip to content

Commit 670d185

Browse files
authored
Merge pull request #6307 from jferrant/fix/do-not-hammer-node-in-capitulation
Do not capitulate if signer has recently contributed to a globally accepted block or recently already checked capitulation
2 parents 1f42955 + 7789907 commit 670d185

File tree

8 files changed

+130
-66
lines changed

8 files changed

+130
-66
lines changed

libsigner/src/tests/signer_state.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// You should have received a copy of the GNU General Public License
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
use std::collections::HashMap;
16-
use std::time::SystemTime;
1716

1817
use blockstack_lib::chainstate::stacks::{
1918
StacksTransaction, TokenTransferMemo, TransactionAnchorMode, TransactionAuth,
@@ -239,7 +238,6 @@ fn determine_global_states() {
239238
current_miner: (&current_miner).into(),
240239
active_signer_protocol_version: local_supported_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
241240
tx_replay_set: ReplayTransactionSet::none(),
242-
update_time: SystemTime::now(),
243241
};
244242

245243
global_eval.insert_update(local_address, local_update);
@@ -279,7 +277,6 @@ fn determine_global_states() {
279277
current_miner: (&new_miner).into(),
280278
active_signer_protocol_version: local_supported_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
281279
tx_replay_set: ReplayTransactionSet::none(),
282-
update_time: SystemTime::now(),
283280
};
284281

285282
global_eval.insert_update(local_address, new_update);
@@ -320,7 +317,6 @@ fn determine_global_states_with_tx_replay_set() {
320317
current_miner: (&current_miner).into(),
321318
active_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
322319
tx_replay_set: ReplayTransactionSet::none(),
323-
update_time: SystemTime::now(),
324320
};
325321

326322
let burn_block = ConsensusHash([20u8; 20]);
@@ -358,7 +354,6 @@ fn determine_global_states_with_tx_replay_set() {
358354
current_miner: (&current_miner).into(),
359355
active_signer_protocol_version: local_supported_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
360356
tx_replay_set: ReplayTransactionSet::none(),
361-
update_time: SystemTime::now(),
362357
};
363358

364359
// Let's tip the scales over to the correct burn view
@@ -415,7 +410,6 @@ fn determine_global_states_with_tx_replay_set() {
415410
current_miner: (&current_miner).into(),
416411
active_signer_protocol_version,
417412
tx_replay_set: ReplayTransactionSet::new(vec![tx]),
418-
update_time: SystemTime::now(),
419413
};
420414

421415
assert_eq!(

libsigner/src/v0/signer_state.rs

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
use std::collections::HashMap;
1717
use std::hash::{Hash, Hasher};
18-
use std::time::SystemTime;
1918

2019
use blockstack_lib::chainstate::stacks::StacksTransaction;
2120
use clarity::types::chainstate::StacksAddress;
@@ -120,7 +119,6 @@ impl GlobalStateEvaluator {
120119
active_signer_protocol_version,
121120
// We need to calculate the threshold for the tx_replay_set separately
122121
tx_replay_set: ReplayTransactionSet::none(),
123-
update_time: SystemTime::now(),
124122
};
125123
let key = SignerStateMachineKey(state_machine.clone());
126124
let entry = state_views.entry(key).or_insert_with(|| 0);
@@ -240,7 +238,7 @@ impl Default for ReplayTransactionSet {
240238
/// A signer state machine view. This struct can
241239
/// be used to encode the local signer's view or
242240
/// the global view.
243-
#[derive(Debug, Clone, Serialize, Deserialize)]
241+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244242
pub struct SignerStateMachine {
245243
/// The tip burn block (i.e., the latest bitcoin block) seen by this signer
246244
pub burn_block: ConsensusHash,
@@ -252,42 +250,16 @@ pub struct SignerStateMachine {
252250
pub active_signer_protocol_version: u64,
253251
/// Transaction replay set
254252
pub tx_replay_set: ReplayTransactionSet,
255-
/// The time when this state machine was last updated
256-
pub update_time: SystemTime,
257-
}
258-
259-
impl PartialEq for SignerStateMachine {
260-
fn eq(&self, other: &Self) -> bool {
261-
// update_time is intentionally ignored
262-
self.burn_block == other.burn_block
263-
&& self.burn_block_height == other.burn_block_height
264-
&& self.current_miner == other.current_miner
265-
&& self.active_signer_protocol_version == other.active_signer_protocol_version
266-
&& self.tx_replay_set == other.tx_replay_set
267-
}
268-
}
269-
270-
impl Eq for SignerStateMachine {}
271-
272-
impl Hash for SignerStateMachine {
273-
fn hash<H: Hasher>(&self, state: &mut H) {
274-
// update_time is intentionally ignored
275-
self.burn_block.hash(state);
276-
self.burn_block_height.hash(state);
277-
self.current_miner.hash(state);
278-
self.active_signer_protocol_version.hash(state);
279-
self.tx_replay_set.hash(state);
280-
}
281253
}
282254

283255
#[derive(Debug)]
284256
/// A wrapped SignerStateMachine that implements a very specific hash that enables properly ignoring the
285-
/// tx_replay_set and update_time when evaluating the global signer state machine
257+
/// tx_replay_set when evaluating the global signer state machine
286258
pub struct SignerStateMachineKey(SignerStateMachine);
287259

288260
impl PartialEq for SignerStateMachineKey {
289261
fn eq(&self, other: &Self) -> bool {
290-
// NOTE: tx_replay_set and update_time are intentionally ignored
262+
// NOTE: tx_replay_set is intentionally ignored
291263
self.0.burn_block == other.0.burn_block
292264
&& self.0.burn_block_height == other.0.burn_block_height
293265
&& self.0.current_miner == other.0.current_miner

stacks-node/src/tests/signer/multiversion.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ pub fn signer_state_machine_v3_1_00_13_to_current(
104104
.map(stacks_transaction_v3_1_00_13_to_current)
105105
.collect(),
106106
),
107-
update_time: SystemTime::now(),
108107
}
109108
}
110109

stacks-signer/src/chainstate/tests/v2.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ fn setup_test_environment(
142142
},
143143
active_signer_protocol_version: 0,
144144
tx_replay_set: ReplayTransactionSet::none(),
145-
update_time: SystemTime::now(),
146145
};
147146

148147
let sortitions_view = GlobalStateView {

stacks-signer/src/signerdb.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use std::collections::HashMap;
1818
use std::fmt::Display;
1919
use std::path::Path;
20-
use std::time::{Duration, SystemTime};
20+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
2121

2222
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
2323
use blockstack_lib::chainstate::stacks::TransactionPayload;
@@ -632,6 +632,11 @@ static ADD_BURN_BLOCK_RECEIVED_TIMES_CONSENSUS_HASH_INDEX: &str = r#"
632632
CREATE INDEX IF NOT EXISTS burn_block_updates_received_times_consensus_hash ON burn_block_updates_received_times(burn_block_consensus_hash, received_time ASC);
633633
"#;
634634

635+
// Used by get_last_globally_accepted_block_signed_self
636+
static ADD_BLOCK_SIGNED_SELF_INDEX: &str = r#"
637+
CREATE INDEX idx_blocks_query_opt ON blocks (consensus_hash, state, signed_self, burn_block_height DESC);
638+
"#;
639+
635640
static DROP_BLOCK_SIGNATURES_TABLE: &str = r#"
636641
DROP TABLE IF EXISTS block_signatures;
637642
"#;
@@ -773,6 +778,7 @@ static SCHEMA_15: &[&str] = &[
773778
static SCHEMA_16: &[&str] = &[
774779
CREATE_BURN_BLOCK_UPDATES_RECEIVED_TIME_TABLE,
775780
ADD_BURN_BLOCK_RECEIVED_TIMES_CONSENSUS_HASH_INDEX,
781+
ADD_BLOCK_SIGNED_SELF_INDEX,
776782
DROP_BLOCK_SIGNATURES_TABLE,
777783
CREATE_BLOCK_SIGNATURES_TABLE_V16,
778784
DROP_BLOCK_REJECTION_SIGNER_ADDRS,
@@ -1152,6 +1158,25 @@ impl SignerDb {
11521158
try_deserialize(result)
11531159
}
11541160

1161+
/// Return the last globally accepted block self_signed time in a given tenure (identified by its consensus hash).
1162+
pub fn get_last_globally_accepted_block_signed_self(
1163+
&self,
1164+
tenure: &ConsensusHash,
1165+
) -> Result<Option<SystemTime>, DBError> {
1166+
let query = r#"
1167+
SELECT signed_self
1168+
FROM blocks
1169+
WHERE consensus_hash = ?1
1170+
AND state = ?2
1171+
AND signed_self IS NOT NULL
1172+
ORDER BY burn_block_height DESC
1173+
LIMIT 1;
1174+
"#;
1175+
let args = params![tenure, &BlockState::GloballyAccepted.to_string()];
1176+
let result: Option<u64> = query_row(&self.db, query, args)?;
1177+
Ok(result.map(|signed_self| UNIX_EPOCH + Duration::from_secs(signed_self)))
1178+
}
1179+
11551180
/// Return the canonical tip -- the last globally accepted block.
11561181
pub fn get_canonical_tip(&self) -> Result<Option<BlockInfo>, DBError> {
11571182
let query = "SELECT block_info FROM blocks WHERE state = ?1 ORDER BY stacks_height DESC, signed_group DESC LIMIT 1";
@@ -3296,4 +3321,64 @@ pub mod tests {
32963321
.unwrap()
32973322
.is_none());
32983323
}
3324+
3325+
#[test]
3326+
fn test_get_last_globally_accepted_block_signed_self() {
3327+
let db_path = tmp_db_path();
3328+
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
3329+
3330+
let consensus_hash_1 = ConsensusHash([0x01; 20]);
3331+
let consensus_hash_2 = ConsensusHash([0x02; 20]);
3332+
3333+
// Create blocks with different burn heights and signed_self timestamps (seconds since epoch)
3334+
let (mut block_info_1, _block_proposal) = create_block_override(|b| {
3335+
b.block.header.consensus_hash = consensus_hash_1;
3336+
b.block.header.miner_signature = MessageSignature([0x01; 65]);
3337+
b.block.header.chain_length = 1;
3338+
b.burn_height = 1;
3339+
});
3340+
block_info_1.mark_locally_accepted(false).unwrap();
3341+
let (mut block_info_2, _block_proposal) = create_block_override(|b| {
3342+
b.block.header.consensus_hash = consensus_hash_1;
3343+
b.block.header.miner_signature = MessageSignature([0x02; 65]);
3344+
b.block.header.chain_length = 2;
3345+
b.burn_height = 2;
3346+
});
3347+
block_info_2.mark_locally_accepted(false).unwrap();
3348+
let (mut block_info_3, _block_proposal) = create_block_override(|b| {
3349+
b.block.header.consensus_hash = consensus_hash_2;
3350+
b.block.header.miner_signature = MessageSignature([0x03; 65]);
3351+
b.block.header.chain_length = 3;
3352+
b.burn_height = 3;
3353+
});
3354+
block_info_3.mark_locally_accepted(false).unwrap();
3355+
3356+
// Mark only one of the blocks as globally accepted
3357+
block_info_1.mark_globally_accepted().unwrap();
3358+
3359+
// Insert into db
3360+
db.insert_block(&block_info_1).unwrap();
3361+
db.insert_block(&block_info_2).unwrap();
3362+
db.insert_block(&block_info_3).unwrap();
3363+
3364+
// Query for consensus_hash_1 should return signed_self of block_info_2 (highest burn_height)
3365+
db.get_last_globally_accepted_block_signed_self(&consensus_hash_1)
3366+
.unwrap()
3367+
.expect("Expected a signed_self timestamp");
3368+
3369+
// Query for consensus_hash_2 should return none since we only contributed to a locally signed block
3370+
let result_2 = db
3371+
.get_last_globally_accepted_block_signed_self(&consensus_hash_2)
3372+
.unwrap();
3373+
3374+
assert!(result_2.is_none());
3375+
3376+
// Query for a consensus hash with no blocks should return None
3377+
let consensus_hash_3 = ConsensusHash([0x03; 20]);
3378+
let result_3 = db
3379+
.get_last_globally_accepted_block_signed_self(&consensus_hash_3)
3380+
.unwrap();
3381+
3382+
assert!(result_3.is_none());
3383+
}
32993384
}

stacks-signer/src/tests/signer_state.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ fn check_capitulate_miner_view() {
181181
current_miner: (&new_miner).into(),
182182
tx_replay_set: ReplayTransactionSet::none(),
183183
active_signer_protocol_version,
184-
update_time: SystemTime::now(),
185184
};
186185

187186
let mut local_state_machine = LocalStateMachine::Initialized(signer_state_machine.clone());
@@ -392,7 +391,6 @@ fn check_miner_inactivity_timeout() {
392391
current_miner: inactive_miner,
393392
active_signer_protocol_version: 0,
394393
tx_replay_set: ReplayTransactionSet::none(),
395-
update_time: SystemTime::now(),
396394
};
397395
local_state_machine = LocalStateMachine::Initialized(signer_state.clone());
398396
local_state_machine

stacks-signer/src/v0/signer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ pub struct Signer {
131131
pub tx_replay_scope: ReplayScopeOpt,
132132
/// Time to wait between updating our local state machine view point and capitulating to other signers miner view
133133
pub capitulate_miner_view_timeout: Duration,
134+
/// The last time we capitulated our miner viewpoint
135+
pub last_capitulate_miner_view: SystemTime,
134136
/// The signer supported protocol version. used only in testing
135137
#[cfg(any(test, feature = "testing"))]
136138
pub supported_signer_protocol_version: u64,
@@ -262,6 +264,7 @@ impl SignerTrait<SignerMessage> for Signer {
262264
validate_with_replay_tx: signer_config.validate_with_replay_tx,
263265
tx_replay_scope: None,
264266
capitulate_miner_view_timeout: signer_config.capitulate_miner_view_timeout,
267+
last_capitulate_miner_view: SystemTime::now(),
265268
#[cfg(any(test, feature = "testing"))]
266269
supported_signer_protocol_version: signer_config.supported_signer_protocol_version,
267270
}
@@ -301,6 +304,7 @@ impl SignerTrait<SignerMessage> for Signer {
301304
sortition_state,
302305
self.capitulate_miner_view_timeout,
303306
self.proposal_config.tenure_last_block_proposal_timeout,
307+
&mut self.last_capitulate_miner_view,
304308
);
305309

306310
if prior_state != self.local_state_machine {

0 commit comments

Comments
 (0)