Skip to content

Commit c09f17e

Browse files
authored
Merge pull request #6147 from jferrant/bug/capitulate-viewpoint-equality-bugs
Fix capitulate_miner_view so don't flip flop between multiple miners
2 parents d23c861 + 8341d77 commit c09f17e

File tree

4 files changed

+166
-136
lines changed

4 files changed

+166
-136
lines changed

stacks-signer/src/tests/signer_state.rs

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
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;
1617

1718
use clarity::types::chainstate::{
18-
ConsensusHash, StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey,
19+
BurnchainHeaderHash, ConsensusHash, StacksAddress, StacksBlockId, StacksPrivateKey,
20+
StacksPublicKey,
1921
};
2022
use clarity::util::hash::Hash160;
2123
use clarity::util::secp256k1::MessageSignature;
@@ -32,7 +34,7 @@ use crate::v0::signer_state::LocalStateMachine;
3234
#[test]
3335
fn check_capitulate_miner_view() {
3436
let mut address_weights = HashMap::new();
35-
for _ in 0..5 {
37+
for _ in 0..10 {
3638
let stacks_address = StacksAddress::p2pkh(
3739
false,
3840
&StacksPublicKey::from_private(&StacksPrivateKey::random()),
@@ -43,17 +45,23 @@ fn check_capitulate_miner_view() {
4345
let active_signer_protocol_version = 0;
4446
let local_supported_signer_protocol_version = 0;
4547
let burn_block = ConsensusHash([0x55; 20]);
46-
let burn_block_height = 100;
4748
let parent_tenure_id = ConsensusHash([0x22; 20]);
4849
let parent_tenure_last_block = StacksBlockId([0x33; 32]);
4950
let parent_tenure_last_block_height = 1;
51+
52+
let old_miner_tenure_id = ConsensusHash([0x01; 20]);
53+
let new_miner_tenure_id = ConsensusHash([0x00; 20]);
54+
55+
let burn_block_height = 100;
56+
5057
let old_miner = StateMachineUpdateMinerState::ActiveMiner {
5158
current_miner_pkh: Hash160([0xab; 20]),
52-
tenure_id: ConsensusHash([0x44; 20]),
59+
tenure_id: old_miner_tenure_id,
5360
parent_tenure_id,
5461
parent_tenure_last_block,
5562
parent_tenure_last_block_height,
5663
};
64+
// Make sure the old update still has the newer burn block height
5765
let old_update = StateMachineUpdateMessage::new(
5866
active_signer_protocol_version,
5967
local_supported_signer_protocol_version,
@@ -86,35 +94,21 @@ fn check_capitulate_miner_view() {
8694
StateMachineUpdateContent::V0 {
8795
burn_block,
8896
burn_block_height,
89-
current_miner,
97+
..
9098
},
9199
..
92100
} = local_update.clone()
93101
else {
94102
panic!("Unexpected state machine update message version");
95103
};
96104
// Let's create a new miner view
97-
let new_tenure_id = ConsensusHash([0x00; 20]);
98-
99-
let db_path = tmp_db_path();
100-
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
101-
let (mut block_info_1, _block_proposal) = create_block_override(|b| {
102-
b.block.header.consensus_hash = new_tenure_id;
103-
b.block.header.miner_signature = MessageSignature([0x01; 65]);
104-
b.block.header.chain_length = 1;
105-
b.burn_height = burn_block_height;
106-
});
107-
108-
db.insert_block(&block_info_1).unwrap();
109105
let new_miner = StateMachineUpdateMinerState::ActiveMiner {
110106
current_miner_pkh: Hash160([0x00; 20]),
111-
tenure_id: new_tenure_id,
107+
tenure_id: new_miner_tenure_id,
112108
parent_tenure_id,
113109
parent_tenure_last_block,
114110
parent_tenure_last_block_height,
115111
};
116-
117-
// Let's update only our own view: the evaluator will tell me to revert my viewpoint to the old miner
118112
let new_update = StateMachineUpdateMessage::new(
119113
active_signer_protocol_version,
120114
local_supported_signer_protocol_version,
@@ -126,6 +120,43 @@ fn check_capitulate_miner_view() {
126120
)
127121
.unwrap();
128122

123+
// Update the database to have both the burn blocks corresponding to the tenure ids
124+
// and both tenures have a locally accepted block
125+
let db_path = tmp_db_path();
126+
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
127+
// Make sure both burn block corresponding to the tenure id's exist in our DB.
128+
db.insert_burn_block(
129+
&BurnchainHeaderHash([0u8; 32]),
130+
&old_miner_tenure_id,
131+
burn_block_height.saturating_sub(1),
132+
&SystemTime::now(),
133+
&BurnchainHeaderHash([1u8; 32]),
134+
)
135+
.unwrap();
136+
db.insert_burn_block(
137+
&BurnchainHeaderHash([0u8; 32]),
138+
&new_miner_tenure_id,
139+
burn_block_height,
140+
&SystemTime::now(),
141+
&BurnchainHeaderHash([1u8; 32]),
142+
)
143+
.unwrap();
144+
let (mut block_info_1, _block_proposal) = create_block_override(|b| {
145+
b.block.header.consensus_hash = old_miner_tenure_id;
146+
b.block.header.miner_signature = MessageSignature([0x02; 65]);
147+
b.block.header.chain_length = 1;
148+
b.burn_height = burn_block_height.saturating_sub(1);
149+
});
150+
db.insert_block(&block_info_1).unwrap();
151+
152+
let (mut block_info_2, _block_proposal) = create_block_override(|b| {
153+
b.block.header.consensus_hash = new_miner_tenure_id;
154+
b.block.header.miner_signature = MessageSignature([0x01; 65]);
155+
b.block.header.chain_length = 1;
156+
b.burn_height = burn_block_height;
157+
});
158+
db.insert_block(&block_info_2).unwrap();
159+
129160
let signer_state_machine = SignerStateMachine {
130161
burn_block,
131162
burn_block_height,
@@ -135,35 +166,43 @@ fn check_capitulate_miner_view() {
135166
};
136167

137168
let mut local_state_machine = LocalStateMachine::Initialized(signer_state_machine.clone());
138-
assert_eq!(
139-
local_state_machine
140-
.capitulate_miner_view(&mut global_eval, &mut db, local_address, &new_update)
141-
.unwrap(),
142-
current_miner
143-
);
144169

145-
// Let's set a blocking minority to this different view: evaluator should see no global blocks for the blocking majority and return none
146-
// I.e. only if the blocking minority is attempting to reject an reorg should it take priority over the rest.
147-
// Let's update 1 other signer to some new miner key (60 percent)
148-
for address in addresses.into_iter().skip(1).take(1) {
170+
// Let's update 40 percent of other signers to some new miner key
171+
for address in addresses.into_iter().take(4) {
149172
global_eval.insert_update(address, new_update.clone());
150173
}
174+
// Miner view should be None as we can't find consensus on a single miner
151175
assert!(
152176
local_state_machine
153177
.capitulate_miner_view(&mut global_eval, &mut db, local_address, &new_update)
154178
.is_none(),
155-
"Evaluator should have been unable to determine a majority view and return none"
179+
"Evaluator should have told me to capitulate to the old miner"
156180
);
157181

182+
// Mark the old miner's block as globally accepted
158183
db.mark_block_globally_accepted(&mut block_info_1).unwrap();
159-
160184
db.insert_block(&block_info_1).unwrap();
161185

162-
// Now that the blocking minority references a tenure which would actually get reorged, lets capitulate to their view
186+
// Miner view should stay as the old miner as it has a globally accepted block and 60% consider it valid.
187+
assert_eq!(
188+
local_state_machine
189+
.capitulate_miner_view(&mut global_eval, &mut db, local_address, &new_update)
190+
.unwrap(),
191+
old_miner,
192+
"Evaluator should have told me to capitulate to the old miner"
193+
);
194+
195+
// Now that we have a globally approved block for the new miner
196+
db.mark_block_globally_accepted(&mut block_info_2).unwrap();
197+
db.insert_block(&block_info_2).unwrap();
198+
199+
// Now that the blocking minority references a tenure which would actually get reorged, lets capitulate to the NEW view
200+
// even though both the old and new signer have > 30% approval (it has a higher burn block).
163201
assert_eq!(
164202
local_state_machine
165203
.capitulate_miner_view(&mut global_eval, &mut db, local_address, &new_update)
166204
.unwrap(),
167-
new_miner
205+
new_miner,
206+
"Evaluator should have told me to capitulate to the new miner"
168207
);
169208
}

stacks-signer/src/v0/signer.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,22 @@ impl SignerTrait<SignerMessage> for Signer {
255255
_res: &Sender<SignerResult>,
256256
current_reward_cycle: u64,
257257
) {
258+
self.check_submitted_block_proposal();
259+
self.check_pending_block_validations(stacks_client);
260+
261+
let mut prior_state = self.local_state_machine.clone();
262+
if self.reward_cycle <= current_reward_cycle {
263+
self.local_state_machine.handle_pending_update(&self.signer_db, stacks_client, &self.proposal_config)
264+
.unwrap_or_else(|e| error!("{self}: failed to update local state machine for pending update"; "err" => ?e));
265+
}
266+
267+
if prior_state != self.local_state_machine {
268+
let version = self.get_signer_protocol_version();
269+
self.local_state_machine
270+
.send_signer_update_message(&mut self.stackerdb, version);
271+
prior_state = self.local_state_machine.clone();
272+
}
273+
258274
let event_parity = match event {
259275
// Block proposal events do have reward cycles, but each proposal has its own cycle,
260276
// and the vec could be heterogeneous, so, don't differentiate.
@@ -272,8 +288,6 @@ impl SignerTrait<SignerMessage> for Signer {
272288
if event_parity == Some(other_signer_parity) {
273289
return;
274290
}
275-
self.check_submitted_block_proposal();
276-
self.check_pending_block_validations(stacks_client);
277291
debug!("{self}: Processing event: {event:?}");
278292
let Some(event) = event else {
279293
// No event. Do nothing.
@@ -292,12 +306,6 @@ impl SignerTrait<SignerMessage> for Signer {
292306
return;
293307
}
294308

295-
let prior_state = self.local_state_machine.clone();
296-
if self.reward_cycle <= current_reward_cycle {
297-
self.local_state_machine.handle_pending_update(&self.signer_db, stacks_client, &self.proposal_config)
298-
.unwrap_or_else(|e| error!("{self}: failed to update local state machine for pending update"; "err" => ?e));
299-
}
300-
301309
self.handle_event_match(stacks_client, sortition_state, event, current_reward_cycle);
302310

303311
self.check_submitted_block_proposal();
@@ -1374,7 +1382,7 @@ impl Signer {
13741382
// Not enough rejection signatures to make a decision
13751383
return;
13761384
}
1377-
debug!("{self}: {total_reject_weight}/{total_weight} signers voted to reject the block {block_hash}");
1385+
info!("{self}: {total_reject_weight}/{total_weight} signers voted to reject the block {block_hash}");
13781386
if let Err(e) = self.signer_db.mark_block_globally_rejected(&mut block_info) {
13791387
warn!("{self}: Failed to mark block as globally rejected: {e:?}",);
13801388
}

stacks-signer/src/v0/signer_state.rs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -695,24 +695,33 @@ impl LocalStateMachine {
695695
local_address: StacksAddress,
696696
local_update: &StateMachineUpdateMessage,
697697
) -> Option<StateMachineUpdateMinerState> {
698+
// First always make sure we consider our own viewpoint
699+
eval.insert_update(local_address, local_update.clone());
700+
701+
// Determine the current burn block from the local update
698702
let current_burn_block = match local_update.content {
699703
StateMachineUpdateContent::V0 { burn_block, .. }
700704
| StateMachineUpdateContent::V1 { burn_block, .. } => burn_block,
701705
};
702-
eval.insert_update(local_address, local_update.clone());
706+
707+
// Determine the global burn view
703708
let (global_burn_view, _) = eval.determine_global_burn_view()?;
704709
if current_burn_block != global_burn_view {
710+
// We don't have the majority's burn block yet...will have to wait
705711
crate::monitoring::actions::increment_signer_agreement_state_conflict(
706712
crate::monitoring::SignerAgreementStateConflict::BurnBlockDelay,
707713
);
708714
return None;
709715
}
710-
let mut current_miners = HashMap::new();
716+
717+
let mut miners = HashMap::new();
718+
let mut potential_matches = Vec::new();
719+
711720
for (address, update) in &eval.address_updates {
712721
let Some(weight) = eval.address_weights.get(address) else {
713722
continue;
714723
};
715-
let (burn_block, current_miner) = match &update.content {
724+
let (burn_block, miner_state) = match &update.content {
716725
StateMachineUpdateContent::V0 {
717726
burn_block,
718727
current_miner,
@@ -724,31 +733,48 @@ impl LocalStateMachine {
724733
..
725734
} => (burn_block, current_miner),
726735
};
727-
728736
if *burn_block != global_burn_view {
729737
continue;
730738
}
731-
732-
let StateMachineUpdateMinerState::ActiveMiner { tenure_id, .. } = current_miner else {
739+
let StateMachineUpdateMinerState::ActiveMiner { tenure_id, .. } = miner_state else {
740+
// Only consider potential active miners
733741
continue;
734742
};
735743

736-
let entry = current_miners.entry(current_miner).or_insert_with(|| 0);
744+
let entry = miners.entry(miner_state).or_insert(0);
737745
*entry += weight;
746+
if *entry <= eval.total_weight * 3 / 10 {
747+
// We don't even see a blocking minority threshold. Ignore.
748+
continue;
749+
}
738750

739-
if *entry >= eval.total_weight * 3 / 10 {
740-
let nmb_blocks = signerdb
741-
.get_globally_accepted_block_count_in_tenure(tenure_id)
742-
.unwrap_or(0);
743-
if nmb_blocks > 0 || eval.reached_agreement(*entry) {
744-
return Some(current_miner.clone());
751+
let nmb_blocks = signerdb
752+
.get_globally_accepted_block_count_in_tenure(tenure_id)
753+
.unwrap_or(0);
754+
if nmb_blocks == 0 && !eval.reached_agreement(*entry) {
755+
continue;
756+
}
757+
758+
match signerdb.get_burn_block_by_ch(tenure_id) {
759+
Ok(block) => {
760+
potential_matches.push((block.block_height, miner_state.clone()));
761+
}
762+
Err(e) => {
763+
warn!("Error retrieving burn block for consensus_hash {tenure_id} from signerdb: {e}");
745764
}
746765
}
747766
}
748-
crate::monitoring::actions::increment_signer_agreement_state_conflict(
749-
crate::monitoring::SignerAgreementStateConflict::MinerView,
750-
);
751-
None
767+
768+
potential_matches.sort_by_key(|(block_height, _)| *block_height);
769+
770+
let new_miner = potential_matches.last().map(|(_, miner)| miner.clone());
771+
if new_miner.is_none() {
772+
crate::monitoring::actions::increment_signer_agreement_state_conflict(
773+
crate::monitoring::SignerAgreementStateConflict::MinerView,
774+
);
775+
}
776+
777+
new_miner
752778
}
753779

754780
#[allow(unused_variables)]

0 commit comments

Comments
 (0)