Skip to content

Commit a21df6f

Browse files
authored
Merge branch 'develop' into chore/document-node-config
2 parents e27a4d3 + 92bd39e commit a21df6f

File tree

37 files changed

+2268
-1101
lines changed

37 files changed

+2268
-1101
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ 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+
## [Unreleased]
9+
10+
### Added
11+
- Persisted tracking of StackerDB slot versions for mining. This improves miner p2p performance.
12+
813
## [3.1.0.0.9]
914

1015
### Added
@@ -13,6 +18,8 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1318
- Added new `ValidateRejectCode` values to the `/v3/block_proposal` endpoint
1419
- Added `StateMachineUpdateContent::V1` to support a vector of `StacksTransaction` expected to be replayed in subsequent Stacks blocks
1520
- Include a reason string in the transaction receipt when a transaction is rolled back due to a post-condition. This should help users in understanding what went wrong.
21+
- Updated `StackerDBListener` to monitor signer state machine updates and store signer global state information, enabling miners to perform transaction replays.
22+
- Added a testnet `replay_transactions` flag to the miner configuration to feature-gate transaction replay. When enabled, the miner will construct a replay block if a threshold of signers signals that a transaction set requires replay.
1623

1724
### Changed
1825

clarity/src/vm/analysis/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ use self::trait_checker::TraitChecker;
3434
use self::type_checker::v2_05::TypeChecker as TypeChecker2_05;
3535
use self::type_checker::v2_1::TypeChecker as TypeChecker2_1;
3636
pub use self::types::{AnalysisPass, ContractAnalysis};
37+
#[cfg(feature = "rusqlite")]
3738
use crate::vm::ast::{build_ast_with_rules, ASTRules};
3839
use crate::vm::costs::LimitedCostTracker;
3940
#[cfg(feature = "rusqlite")]
4041
use crate::vm::database::MemoryBackingStore;
4142
use crate::vm::database::STORE_CONTRACT_SRC_INTERFACE;
4243
use crate::vm::representations::SymbolicExpression;
43-
use crate::vm::types::{QualifiedContractIdentifier, TypeSignature};
44+
use crate::vm::types::QualifiedContractIdentifier;
45+
#[cfg(feature = "rusqlite")]
46+
use crate::vm::types::TypeSignature;
4447
use crate::vm::ClarityVersion;
4548

4649
/// Used by CLI tools like the docs generator. Not used in production

clarity/src/vm/docs/contracts.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ use hashbrown::{HashMap, HashSet};
44
use stacks_common::consts::CHAIN_ID_TESTNET;
55
use stacks_common::types::StacksEpochId;
66

7-
#[cfg(feature = "rusqlite")]
8-
use crate::vm::analysis::mem_type_check;
9-
use crate::vm::analysis::ContractAnalysis;
7+
use crate::vm::analysis::{mem_type_check, ContractAnalysis};
108
use crate::vm::ast::{build_ast_with_rules, ASTRules};
119
use crate::vm::contexts::GlobalContext;
1210
use crate::vm::costs::LimitedCostTracker;
13-
#[cfg(feature = "rusqlite")]
1411
use crate::vm::database::MemoryBackingStore;
1512
use crate::vm::docs::{get_input_type_string, get_output_type_string, get_signature};
1613
use crate::vm::types::{FunctionType, QualifiedContractIdentifier, Value};
@@ -63,7 +60,6 @@ fn make_func_ref(func_name: &str, func_type: &FunctionType, description: &str) -
6360
}
6461
}
6562

66-
#[cfg(feature = "rusqlite")]
6763
#[allow(clippy::expect_used)]
6864
fn get_constant_value(var_name: &str, contract_content: &str) -> Value {
6965
let to_eval = format!("{}\n{}", contract_content, var_name);
@@ -72,7 +68,6 @@ fn get_constant_value(var_name: &str, contract_content: &str) -> Value {
7268
.expect("BUG: failed to return constant value")
7369
}
7470

75-
#[cfg(feature = "rusqlite")]
7671
fn doc_execute(program: &str) -> Result<Option<Value>, vm::Error> {
7772
let contract_id = QualifiedContractIdentifier::transient();
7873
let mut contract_context = ContractContext::new(contract_id.clone(), ClarityVersion::Clarity2);
@@ -99,7 +94,6 @@ fn doc_execute(program: &str) -> Result<Option<Value>, vm::Error> {
9994
})
10095
}
10196

102-
#[cfg(feature = "rusqlite")]
10397
#[allow(clippy::expect_used)]
10498
pub fn make_docs(
10599
content: &str,
@@ -185,7 +179,6 @@ pub fn make_docs(
185179

186180
/// Produce a set of documents for multiple contracts, supplied as a list of `(contract_name, contract_content)` pairs,
187181
/// and a map from `contract_name` to corresponding `ContractSupportDocs`
188-
#[cfg(feature = "rusqlite")]
189182
pub fn produce_docs_refs<A: AsRef<str>, B: AsRef<str>>(
190183
contracts: &[(A, B)],
191184
support_docs: &HashMap<&str, ContractSupportDocs>,

clarity/src/vm/docs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::vm::types::{FixedFunction, FunctionType};
2323
use crate::vm::variables::NativeVariables;
2424
use crate::vm::ClarityVersion;
2525

26+
#[cfg(feature = "rusqlite")]
2627
pub mod contracts;
2728

2829
#[derive(Serialize)]

libsigner/src/tests/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2-
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
2+
// Copyright (C) 2020-2025 Stacks Open Internet Foundation
33
//
44
// This program is free software: you can redistribute it and/or modify
55
// it under the terms of the GNU General Public License as published by
@@ -15,6 +15,7 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
mod http;
18+
mod signer_state;
1819

1920
use std::fmt::Debug;
2021
use std::io::{Read, Write};

libsigner/src/tests/signer_state.rs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
// Copyright (C) 2020-2025 Stacks Open Internet Foundation
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
use std::collections::HashMap;
16+
17+
use clarity::types::chainstate::{
18+
ConsensusHash, StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey,
19+
};
20+
use clarity::util::hash::Hash160;
21+
use clarity::util::secp256k1::MessageSignature;
22+
23+
use crate::v0::messages::{
24+
StateMachineUpdate as StateMachineUpdateMessage, StateMachineUpdateContent,
25+
StateMachineUpdateMinerState,
26+
};
27+
use crate::v0::signer_state::{GlobalStateEvaluator, SignerStateMachine};
28+
29+
fn generate_global_state_evaluator(num_addresses: u32) -> GlobalStateEvaluator {
30+
let address_weights = generate_random_address_with_equal_weights(num_addresses);
31+
let active_protocol_version = 0;
32+
let local_supported_signer_protocol_version = 1;
33+
34+
let update = StateMachineUpdateMessage::new(
35+
active_protocol_version,
36+
local_supported_signer_protocol_version,
37+
StateMachineUpdateContent::V0 {
38+
burn_block: ConsensusHash([0x55; 20]),
39+
burn_block_height: 100,
40+
current_miner: StateMachineUpdateMinerState::ActiveMiner {
41+
current_miner_pkh: Hash160([0xab; 20]),
42+
tenure_id: ConsensusHash([0x44; 20]),
43+
parent_tenure_id: ConsensusHash([0x22; 20]),
44+
parent_tenure_last_block: StacksBlockId([0x33; 32]),
45+
parent_tenure_last_block_height: 1,
46+
},
47+
},
48+
)
49+
.unwrap();
50+
51+
let mut address_updates = HashMap::new();
52+
for address in address_weights.keys() {
53+
address_updates.insert(*address, update.clone());
54+
}
55+
GlobalStateEvaluator::new(address_updates, address_weights)
56+
}
57+
58+
fn generate_random_address_with_equal_weights(num_addresses: u32) -> HashMap<StacksAddress, u32> {
59+
let mut address_weights = HashMap::new();
60+
for _ in 0..num_addresses {
61+
let stacks_address = StacksAddress::p2pkh(
62+
false,
63+
&StacksPublicKey::from_private(&StacksPrivateKey::random()),
64+
);
65+
address_weights.insert(stacks_address, 10);
66+
}
67+
address_weights
68+
}
69+
70+
#[test]
71+
fn determine_latest_supported_signer_protocol_versions() {
72+
let mut global_eval = generate_global_state_evaluator(5);
73+
74+
let addresses: Vec<_> = global_eval.address_weights.keys().cloned().collect();
75+
let local_address = addresses[0];
76+
77+
let local_update = global_eval
78+
.address_updates
79+
.get(&local_address)
80+
.unwrap()
81+
.clone();
82+
assert_eq!(
83+
global_eval
84+
.determine_latest_supported_signer_protocol_version()
85+
.unwrap(),
86+
local_update.local_supported_signer_protocol_version
87+
);
88+
89+
let StateMachineUpdateMessage {
90+
active_signer_protocol_version,
91+
local_supported_signer_protocol_version,
92+
content:
93+
StateMachineUpdateContent::V0 {
94+
burn_block,
95+
burn_block_height,
96+
current_miner,
97+
},
98+
..
99+
} = local_update.clone()
100+
else {
101+
panic!("Unexpected state machine update message version");
102+
};
103+
104+
// Let's update 3 signers (60 percent) to support seperate but greater protocol versions
105+
for (i, address) in addresses.into_iter().skip(1).take(3).enumerate() {
106+
let new_version = local_update.local_supported_signer_protocol_version + i as u64 + 1;
107+
let new_update = StateMachineUpdateMessage::new(
108+
active_signer_protocol_version,
109+
new_version,
110+
StateMachineUpdateContent::V0 {
111+
burn_block,
112+
burn_block_height,
113+
current_miner: current_miner.clone(),
114+
},
115+
)
116+
.unwrap();
117+
global_eval.insert_update(address, new_update);
118+
}
119+
120+
assert_eq!(
121+
global_eval
122+
.determine_latest_supported_signer_protocol_version()
123+
.unwrap(),
124+
local_supported_signer_protocol_version
125+
);
126+
127+
// Let's tip the scales over to version number 2 by updating the local signer's version...
128+
// i.e. > 70% will have version 2 or higher in their map
129+
let local_update = StateMachineUpdateMessage::new(
130+
active_signer_protocol_version,
131+
3,
132+
StateMachineUpdateContent::V0 {
133+
burn_block,
134+
burn_block_height,
135+
current_miner,
136+
},
137+
)
138+
.unwrap();
139+
140+
global_eval.insert_update(local_address, local_update);
141+
142+
assert_eq!(
143+
global_eval
144+
.determine_latest_supported_signer_protocol_version()
145+
.unwrap(),
146+
local_supported_signer_protocol_version + 1
147+
);
148+
}
149+
150+
#[test]
151+
fn determine_global_burn_views() {
152+
let mut global_eval = generate_global_state_evaluator(5);
153+
154+
let addresses: Vec<_> = global_eval.address_weights.keys().cloned().collect();
155+
let local_address = addresses[0];
156+
let local_update = global_eval
157+
.address_updates
158+
.get(&local_address)
159+
.unwrap()
160+
.clone();
161+
let StateMachineUpdateMessage {
162+
active_signer_protocol_version,
163+
local_supported_signer_protocol_version,
164+
content:
165+
StateMachineUpdateContent::V0 {
166+
burn_block,
167+
burn_block_height,
168+
current_miner,
169+
},
170+
..
171+
} = local_update.clone()
172+
else {
173+
panic!("Unexpected state machine update message version");
174+
};
175+
176+
assert_eq!(
177+
global_eval.determine_global_burn_view().unwrap(),
178+
(burn_block, burn_block_height)
179+
);
180+
181+
// Let's update 3 signers (60 percent) to support a new burn block view
182+
let new_update = StateMachineUpdateMessage::new(
183+
active_signer_protocol_version,
184+
local_supported_signer_protocol_version,
185+
StateMachineUpdateContent::V0 {
186+
burn_block,
187+
burn_block_height: burn_block_height.wrapping_add(1),
188+
current_miner: current_miner.clone(),
189+
},
190+
)
191+
.unwrap();
192+
for address in addresses.into_iter().skip(1).take(3) {
193+
global_eval.insert_update(address, new_update.clone());
194+
}
195+
196+
assert!(
197+
global_eval.determine_global_burn_view().is_none(),
198+
"We should not have reached agreement on the burn block height"
199+
);
200+
201+
// Let's tip the scales over to burn block height + 1
202+
global_eval.insert_update(local_address, new_update);
203+
assert_eq!(
204+
global_eval.determine_global_burn_view().unwrap(),
205+
(burn_block, burn_block_height.wrapping_add(1))
206+
);
207+
}
208+
209+
#[test]
210+
fn determine_global_states() {
211+
let mut global_eval = generate_global_state_evaluator(5);
212+
213+
let addresses: Vec<_> = global_eval.address_weights.keys().cloned().collect();
214+
let local_address = addresses[0];
215+
let local_update = global_eval
216+
.address_updates
217+
.get(&local_address)
218+
.unwrap()
219+
.clone();
220+
let StateMachineUpdateMessage {
221+
active_signer_protocol_version,
222+
local_supported_signer_protocol_version,
223+
content:
224+
StateMachineUpdateContent::V0 {
225+
burn_block,
226+
burn_block_height,
227+
current_miner,
228+
},
229+
..
230+
} = local_update.clone()
231+
else {
232+
panic!("Unexpected state machine update message version");
233+
};
234+
235+
let state_machine = SignerStateMachine {
236+
burn_block,
237+
burn_block_height,
238+
current_miner: (&current_miner).into(),
239+
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...
240+
tx_replay_set: None,
241+
};
242+
243+
global_eval.insert_update(local_address, local_update);
244+
assert_eq!(global_eval.determine_global_state().unwrap(), state_machine);
245+
let new_miner = StateMachineUpdateMinerState::ActiveMiner {
246+
current_miner_pkh: Hash160([0x00; 20]),
247+
tenure_id: ConsensusHash([0x44; 20]),
248+
parent_tenure_id: ConsensusHash([0x22; 20]),
249+
parent_tenure_last_block: StacksBlockId([0x33; 32]),
250+
parent_tenure_last_block_height: 1,
251+
};
252+
253+
let new_update = StateMachineUpdateMessage::new(
254+
active_signer_protocol_version,
255+
local_supported_signer_protocol_version,
256+
StateMachineUpdateContent::V0 {
257+
burn_block,
258+
burn_block_height,
259+
current_miner: new_miner.clone(),
260+
},
261+
)
262+
.unwrap();
263+
264+
// Let's update 3 signers to some new miner key (60 percent)
265+
for address in addresses.into_iter().skip(1).take(3) {
266+
global_eval.insert_update(address, new_update.clone());
267+
}
268+
269+
assert!(
270+
global_eval.determine_global_state().is_none(),
271+
"We should have a disagreement about the current miner"
272+
);
273+
274+
let state_machine = SignerStateMachine {
275+
burn_block,
276+
burn_block_height,
277+
current_miner: (&new_miner).into(),
278+
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...
279+
tx_replay_set: None,
280+
};
281+
282+
global_eval.insert_update(local_address, new_update);
283+
// Let's tip the scales over to a different miner
284+
assert_eq!(global_eval.determine_global_state().unwrap(), state_machine)
285+
}

0 commit comments

Comments
 (0)