Skip to content

Commit 1668fae

Browse files
authored
Merge branch 'develop' into feat/pox-4-tests-nakamoto
2 parents 1cd68b8 + 47a2b14 commit 1668fae

File tree

5 files changed

+192
-30
lines changed

5 files changed

+192
-30
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ jobs:
9898
- tests::signer::v0::miner_forking
9999
- tests::signer::v0::reloads_signer_set_in
100100
- tests::signer::v0::signers_broadcast_signed_blocks
101+
- tests::signer::v0::min_gap_between_blocks
101102
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
102103
- tests::nakamoto_integrations::check_block_heights
103104
- tests::nakamoto_integrations::clarity_burn_state

testnet/stacks-node/src/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub const OP_TX_ANY_ESTIM_SIZE: u64 = fmax!(
8686
const DEFAULT_MAX_RBF_RATE: u64 = 150; // 1.5x
8787
const DEFAULT_RBF_FEE_RATE_INCREMENT: u64 = 5;
8888
const INV_REWARD_CYCLES_TESTNET: u64 = 6;
89+
const DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS: u64 = 1000;
8990

9091
#[derive(Clone, Deserialize, Default, Debug)]
9192
pub struct ConfigFile {
@@ -2358,6 +2359,9 @@ pub struct MinerConfig {
23582359
pub wait_on_signers: Duration,
23592360
/// Whether to mock sign in Epoch 2.5 through the .miners and .signers contracts. This is used for testing purposes in Epoch 2.5 only.
23602361
pub pre_nakamoto_mock_signing: bool,
2362+
/// The minimum time to wait between mining blocks in milliseconds. The value must be greater than or equal to 1000 ms because if a block is mined
2363+
/// within the same second as its parent, it will be rejected by the signers.
2364+
pub min_time_between_blocks_ms: u64,
23612365
}
23622366

23632367
impl Default for MinerConfig {
@@ -2389,6 +2393,7 @@ impl Default for MinerConfig {
23892393
// TODO: update to a sane value based on stackerdb benchmarking
23902394
wait_on_signers: Duration::from_secs(200),
23912395
pre_nakamoto_mock_signing: false, // Should only default true if mining key is set
2396+
min_time_between_blocks_ms: DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS,
23922397
}
23932398
}
23942399
}
@@ -2739,6 +2744,7 @@ pub struct MinerConfigFile {
27392744
pub max_reorg_depth: Option<u64>,
27402745
pub wait_on_signers_ms: Option<u64>,
27412746
pub pre_nakamoto_mock_signing: Option<bool>,
2747+
pub min_time_between_blocks_ms: Option<u64>,
27422748
}
27432749

27442750
impl MinerConfigFile {
@@ -2850,6 +2856,12 @@ impl MinerConfigFile {
28502856
pre_nakamoto_mock_signing: self
28512857
.pre_nakamoto_mock_signing
28522858
.unwrap_or(pre_nakamoto_mock_signing), // Should only default true if mining key is set
2859+
min_time_between_blocks_ms: self.min_time_between_blocks_ms.map(|ms| if ms < DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS {
2860+
warn!("miner.min_time_between_blocks_ms is less than the minimum allowed value of {DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS} ms. Using the default value instead.");
2861+
DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS
2862+
} else {
2863+
ms
2864+
}).unwrap_or(miner_default_config.min_time_between_blocks_ms),
28532865
})
28542866
}
28552867
}

testnet/stacks-node/src/nakamoto_node/miner.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use stacks::chainstate::stacks::{
4545
use stacks::net::p2p::NetworkHandle;
4646
use stacks::net::stackerdb::StackerDBs;
4747
use stacks::net::{NakamotoBlocksData, StacksMessageType};
48+
use stacks::util::get_epoch_time_secs;
4849
use stacks::util::secp256k1::MessageSignature;
4950
use stacks_common::codec::read_next;
5051
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
@@ -317,8 +318,17 @@ impl BlockMinerThread {
317318
}
318319
}
319320
}
321+
320322
match self.mine_block(&stackerdbs) {
321-
Ok(x) => break Some(x),
323+
Ok(x) => {
324+
if !self.validate_timestamp(&x)? {
325+
info!("Block mined too quickly. Will try again.";
326+
"block_timestamp" => x.header.timestamp,
327+
);
328+
continue;
329+
}
330+
break Some(x);
331+
}
322332
Err(NakamotoNodeError::MiningFailure(ChainstateError::MinerAborted)) => {
323333
info!("Miner interrupted while mining, will try again");
324334
// sleep, and try again. if the miner was interrupted because the burnchain
@@ -1037,6 +1047,42 @@ impl BlockMinerThread {
10371047
Some(vrf_proof)
10381048
}
10391049

1050+
/// Check that the provided block is not mined too quickly after the parent block.
1051+
/// This is to ensure that the signers do not reject the block due to the block being mined within the same second as the parent block.
1052+
fn validate_timestamp(&self, x: &NakamotoBlock) -> Result<bool, NakamotoNodeError> {
1053+
let chain_state = neon_node::open_chainstate_with_faults(&self.config)
1054+
.expect("FATAL: could not open chainstate DB");
1055+
let stacks_parent_header =
1056+
NakamotoChainState::get_block_header(chain_state.db(), &x.header.parent_block_id)
1057+
.map_err(|e| {
1058+
error!(
1059+
"Could not query header info for parent block ID {}: {:?}",
1060+
&x.header.parent_block_id, &e
1061+
);
1062+
NakamotoNodeError::ParentNotFound
1063+
})?
1064+
.ok_or_else(|| {
1065+
error!(
1066+
"No header info for parent block ID {}",
1067+
&x.header.parent_block_id
1068+
);
1069+
NakamotoNodeError::ParentNotFound
1070+
})?;
1071+
let current_timestamp = get_epoch_time_secs();
1072+
let time_since_parent_ms =
1073+
current_timestamp.saturating_sub(stacks_parent_header.burn_header_timestamp) * 1000;
1074+
if time_since_parent_ms < self.config.miner.min_time_between_blocks_ms {
1075+
debug!("Parent block mined {time_since_parent_ms} ms ago. Required minimum gap between blocks is {} ms", self.config.miner.min_time_between_blocks_ms;
1076+
"current_timestamp" => current_timestamp,
1077+
"parent_block_id" => %stacks_parent_header.index_block_hash(),
1078+
"parent_block_height" => stacks_parent_header.stacks_block_height,
1079+
"parent_block_timestamp" => stacks_parent_header.burn_header_timestamp,
1080+
);
1081+
return Ok(false);
1082+
}
1083+
Ok(true)
1084+
}
1085+
10401086
// TODO: add tests from mutation testing results #4869
10411087
#[cfg_attr(test, mutants::skip)]
10421088
/// Try to mine a Stacks block by assembling one from mempool transactions and sending a

testnet/stacks-node/src/tests/bitcoin_regtest.rs

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ impl BitcoinCoreController {
4444
}
4545
}
4646

47+
fn add_rpc_cli_args(&self, command: &mut Command) {
48+
command.arg(format!("-rpcport={}", self.config.burnchain.rpc_port));
49+
50+
match (
51+
&self.config.burnchain.username,
52+
&self.config.burnchain.password,
53+
) {
54+
(Some(username), Some(password)) => {
55+
command
56+
.arg(format!("-rpcuser={username}"))
57+
.arg(format!("-rpcpassword={password}"));
58+
}
59+
_ => {}
60+
}
61+
}
62+
4763
pub fn start_bitcoind(&mut self) -> BitcoinResult<()> {
4864
std::fs::create_dir_all(&self.config.get_burnchain_path_str()).unwrap();
4965

@@ -58,30 +74,16 @@ impl BitcoinCoreController {
5874
.arg("-server=1")
5975
.arg("-listenonion=0")
6076
.arg("-rpcbind=127.0.0.1")
61-
.arg(&format!("-port={}", self.config.burnchain.peer_port))
62-
.arg(&format!(
63-
"-datadir={}",
64-
self.config.get_burnchain_path_str()
65-
))
66-
.arg(&format!("-rpcport={}", self.config.burnchain.rpc_port));
77+
.arg(format!("-port={}", self.config.burnchain.peer_port))
78+
.arg(format!("-datadir={}", self.config.get_burnchain_path_str()));
6779

68-
match (
69-
&self.config.burnchain.username,
70-
&self.config.burnchain.password,
71-
) {
72-
(Some(username), Some(password)) => {
73-
command
74-
.arg(&format!("-rpcuser={}", username))
75-
.arg(&format!("-rpcpassword={}", password));
76-
}
77-
_ => {}
78-
}
80+
self.add_rpc_cli_args(&mut command);
7981

80-
eprintln!("bitcoind spawn: {:?}", command);
82+
eprintln!("bitcoind spawn: {command:?}");
8183

8284
let mut process = match command.spawn() {
8385
Ok(child) => child,
84-
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{:?}", e))),
86+
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{e:?}"))),
8587
};
8688

8789
let mut out_reader = BufReader::new(process.stdout.take().unwrap());
@@ -108,17 +110,15 @@ impl BitcoinCoreController {
108110
pub fn stop_bitcoind(&mut self) -> Result<(), BitcoinCoreError> {
109111
if let Some(_) = self.bitcoind_process.take() {
110112
let mut command = Command::new("bitcoin-cli");
111-
command
112-
.stdout(Stdio::piped())
113-
.arg("-rpcconnect=127.0.0.1")
114-
.arg("-rpcport=8332")
115-
.arg("-rpcuser=neon-tester")
116-
.arg("-rpcpassword=neon-tester-pass")
117-
.arg("stop");
113+
command.stdout(Stdio::piped()).arg("-rpcconnect=127.0.0.1");
114+
115+
self.add_rpc_cli_args(&mut command);
116+
117+
command.arg("stop");
118118

119119
let mut process = match command.spawn() {
120120
Ok(child) => child,
121-
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{:?}", e))),
121+
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{e:?}"))),
122122
};
123123

124124
let mut out_reader = BufReader::new(process.stdout.take().unwrap());
@@ -127,7 +127,7 @@ impl BitcoinCoreController {
127127
if bytes_read == 0 {
128128
break;
129129
}
130-
eprintln!("{}", &line);
130+
eprintln!("{line}");
131131
}
132132
}
133133
Ok(())

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2788,7 +2788,8 @@ fn signer_set_rollover() {
27882788
.running_nodes
27892789
.btc_regtest_controller
27902790
.get_burnchain()
2791-
.reward_cycle_to_block_height(next_reward_cycle);
2791+
.reward_cycle_to_block_height(next_reward_cycle)
2792+
.saturating_add(1);
27922793

27932794
info!("---- Mining to next reward set calculation -----");
27942795
signer_test.run_until_burnchain_height_nakamoto(
@@ -2847,3 +2848,105 @@ fn signer_set_rollover() {
28472848
assert!(signer.stop().is_none());
28482849
}
28492850
}
2851+
2852+
#[test]
2853+
#[ignore]
2854+
/// This test checks that the signers will broadcast a block once they receive enough signatures.
2855+
fn min_gap_between_blocks() {
2856+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2857+
return;
2858+
}
2859+
2860+
tracing_subscriber::registry()
2861+
.with(fmt::layer())
2862+
.with(EnvFilter::from_default_env())
2863+
.init();
2864+
2865+
info!("------------------------- Test Setup -------------------------");
2866+
let num_signers = 5;
2867+
let sender_sk = Secp256k1PrivateKey::new();
2868+
let sender_addr = tests::to_addr(&sender_sk);
2869+
let send_amt = 100;
2870+
let send_fee = 180;
2871+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
2872+
let time_between_blocks_ms = 10_000;
2873+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2874+
num_signers,
2875+
vec![(sender_addr.clone(), send_amt + send_fee)],
2876+
Some(Duration::from_secs(15)),
2877+
|_config| {},
2878+
|config| {
2879+
config.miner.min_time_between_blocks_ms = time_between_blocks_ms;
2880+
},
2881+
&[],
2882+
);
2883+
2884+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
2885+
2886+
signer_test.boot_to_epoch_3();
2887+
2888+
let proposals_before = signer_test
2889+
.running_nodes
2890+
.nakamoto_blocks_proposed
2891+
.load(Ordering::SeqCst);
2892+
2893+
let info_before = get_chain_info(&signer_test.running_nodes.conf);
2894+
2895+
// submit a tx so that the miner will mine a block
2896+
let sender_nonce = 0;
2897+
let transfer_tx =
2898+
make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
2899+
submit_tx(&http_origin, &transfer_tx);
2900+
2901+
info!("Submitted transfer tx and waiting for block proposal. Ensure it does not arrive before the gap is exceeded");
2902+
let start_time = Instant::now();
2903+
loop {
2904+
let blocks_proposed = signer_test
2905+
.running_nodes
2906+
.nakamoto_blocks_proposed
2907+
.load(Ordering::SeqCst);
2908+
if blocks_proposed > proposals_before {
2909+
assert!(
2910+
start_time.elapsed().as_millis() >= time_between_blocks_ms.into(),
2911+
"Block proposed before gap was exceeded"
2912+
);
2913+
break;
2914+
}
2915+
std::thread::sleep(Duration::from_millis(100));
2916+
}
2917+
2918+
debug!("Ensure that the block is mined after the gap is exceeded");
2919+
2920+
let start = Instant::now();
2921+
let duration = 30;
2922+
let blocks_before = signer_test
2923+
.running_nodes
2924+
.nakamoto_blocks_mined
2925+
.load(Ordering::SeqCst);
2926+
loop {
2927+
let blocks_mined = signer_test
2928+
.running_nodes
2929+
.nakamoto_blocks_mined
2930+
.load(Ordering::SeqCst);
2931+
2932+
let info = get_chain_info(&signer_test.running_nodes.conf);
2933+
if blocks_mined > blocks_before
2934+
&& info.stacks_tip_height == info_before.stacks_tip_height + 1
2935+
{
2936+
break;
2937+
}
2938+
2939+
debug!(
2940+
"blocks_mined: {},{}, stacks_tip_height: {},{}",
2941+
blocks_mined, blocks_before, info_before.stacks_tip_height, info.stacks_tip_height
2942+
);
2943+
2944+
std::thread::sleep(Duration::from_millis(100));
2945+
assert!(
2946+
start.elapsed() < Duration::from_secs(duration),
2947+
"Block not mined within timeout"
2948+
);
2949+
}
2950+
2951+
signer_test.shutdown();
2952+
}

0 commit comments

Comments
 (0)