Skip to content

Commit 242cd7e

Browse files
committed
Fix capitulate_miner_view so don't flip flop between multiple potential miners
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 7936329 commit 242cd7e

File tree

2 files changed

+117
-52
lines changed

2 files changed

+117
-52
lines changed

stacks-signer/src/tests/signer_state.rs

Lines changed: 74 additions & 35 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) {
149-
global_eval.insert_update(address, new_update.clone());
170+
// Let's update 40 percent of other signers to some new miner key
171+
for address in addresses.iter().take(4) {
172+
global_eval.insert_update(address.clone(), 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_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)