Skip to content

Commit cd41b5e

Browse files
authored
Merge branch 'develop' into feat/extend-bitcoin-reorg
2 parents 24e71d0 + 59213ab commit cd41b5e

File tree

13 files changed

+256
-145
lines changed

13 files changed

+256
-145
lines changed

.github/workflows/lock-threads.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
id: lock_threads
3030
uses: stacks-network/actions/lock-threads@main
3131
with:
32-
github-token: ${{ secrets.GH_TOKEN }}
32+
github-token: ${{ secrets.GITHUB_TOKEN }}
3333
issue-inactive-days: 7
3434
pr-inactive-days: 7
3535
discussion-inactive-days: 7

.github/workflows/workflow-cleanup.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ on:
1212
workflow-ttl:
1313
description: "How many days to keep a successful workflow (default: 30)"
1414
required: false
15-
default: "30"
15+
default: "60"
1616
failed-workflow-ttl:
1717
description: "How many days to keep failed workflows (default: 15)"
1818
required: false
19-
default: "15"
19+
default: "60"
2020
schedule:
2121
## Run every day at 00:00:00
2222
- cron: "0 0 * * *"
2323

24+
permissions:
25+
actions: write # to delete workflow runs and caches
26+
contents: read # to access repo contents
27+
2428
## env vars are transferred to composite action steps
2529
env:
2630
CACHE_TTL: 7 ## number of days to keep a cache
@@ -41,7 +45,7 @@ jobs:
4145
id: cleanup
4246
uses: stacks-network/actions/cleanup/workflows@main
4347
with:
44-
token: ${{ secrets.GH_TOKEN }}
48+
token: ${{ secrets.GITHUB_TOKEN }}
4549
cache-ttl: ${{ inputs.cache-ttl || env.CACHE_TTL}}
4650
workflow-ttl: ${{ inputs.workflow-ttl || env.WORKFLOW_TTL}}
4751
failed-workflow-ttl: ${{ inputs.failed-workflow-ttl || env.FAILED_WORKFLOW_TTL }}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8+
## [3.1.0.0.11]
9+
10+
- Hotfix for p2p stack misbehavior in mempool syncing conditions
11+
812
## [3.1.0.0.10]
913

1014
### Added

stacks-signer/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use chainstate::SortitionsView;
5353
use config::GlobalConfig;
5454
use libsigner::{SignerEvent, SignerEventReceiver, SignerEventTrait, VERSION_STRING};
5555
use runloop::SignerResult;
56+
use signerdb::BlockInfo;
5657
use stacks_common::{info, warn};
5758
use v0::signer_state::LocalStateMachine;
5859

@@ -81,6 +82,8 @@ pub trait Signer<T: SignerEventTrait>: Debug + Display {
8182
fn get_local_state_machine(&self) -> &LocalStateMachine;
8283
/// Get the number of pending block proposals
8384
fn get_pending_proposals_count(&self) -> u64;
85+
/// Get canonical block according to this signer's db
86+
fn get_canonical_tip(&self) -> Option<BlockInfo>;
8487
}
8588

8689
/// A wrapper around the running signer type for the signer

stacks-signer/src/runloop.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use stacks_common::{debug, error, info, warn};
2525
use crate::chainstate::SortitionsView;
2626
use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient};
2727
use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode};
28+
use crate::signerdb::BlockInfo;
2829
use crate::v0::signer_state::LocalStateMachine;
2930
#[cfg(any(test, feature = "testing"))]
3031
use crate::v0::tests::TEST_SKIP_SIGNER_CLEANUP;
@@ -59,6 +60,9 @@ pub struct StateInfo {
5960
pub signer_state_machines: Vec<(u64, Option<LocalStateMachine>)>,
6061
/// The number of pending block proposals for this signer
6162
pub pending_proposals_count: u64,
63+
/// The canonical tip block info according to the running signers
64+
/// as a pair of (reward-cycle, block-info)
65+
pub signer_canonical_tips: Vec<(u64, Option<BlockInfo>)>,
6266
}
6367

6468
/// The signer result that can be sent across threads
@@ -544,6 +548,16 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug>
544548
}
545549
})
546550
.unwrap_or(0),
551+
signer_canonical_tips: self
552+
.stacks_signers
553+
.iter()
554+
.map(|(reward_cycle, signer)| {
555+
let ConfiguredSigner::RegisteredSigner(ref signer) = signer else {
556+
return (*reward_cycle, None);
557+
};
558+
(*reward_cycle, signer.get_canonical_tip())
559+
})
560+
.collect(),
547561
};
548562
info!("Signer status check requested: {state_info:?}");
549563

stacks-signer/src/signerdb.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl FromRow<BurnBlockInfo> for BurnBlockInfo {
9898
}
9999
}
100100

101-
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
101+
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)]
102102
/// Store extra version-specific info in `BlockInfo`
103103
pub enum ExtraBlockInfo {
104104
#[default]
@@ -167,7 +167,7 @@ impl TryFrom<&str> for BlockState {
167167
}
168168

169169
/// Additional Info about a proposed block
170-
#[derive(Serialize, Deserialize, Debug, PartialEq)]
170+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
171171
pub struct BlockInfo {
172172
/// The block we are considering
173173
pub block: NakamotoBlock,

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: 25 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();
@@ -336,6 +344,14 @@ impl SignerTrait<SignerMessage> for Signer {
336344
.map(|results| u64::try_from(results.len()).unwrap())
337345
.unwrap_or(0)
338346
}
347+
348+
fn get_canonical_tip(&self) -> Option<BlockInfo> {
349+
self.signer_db
350+
.get_canonical_tip()
351+
.inspect_err(|e| error!("{self}: Failed to check for canonical tip: {e:?}"))
352+
.ok()
353+
.flatten()
354+
}
339355
}
340356

341357
impl Signer {
@@ -1376,7 +1392,7 @@ impl Signer {
13761392
// Not enough rejection signatures to make a decision
13771393
return;
13781394
}
1379-
debug!("{self}: {total_reject_weight}/{total_weight} signers voted to reject the block {block_hash}");
1395+
info!("{self}: {total_reject_weight}/{total_weight} signers voted to reject the block {block_hash}");
13801396
if let Err(e) = self.signer_db.mark_block_globally_rejected(&mut block_info) {
13811397
warn!("{self}: Failed to mark block as globally rejected: {e:?}",);
13821398
}

0 commit comments

Comments
 (0)