Skip to content

Commit 8e0c020

Browse files
authored
Merge pull request #5573 from stacks-network/feat/miner-self-issued-time-based-tenure-extend
Feat/miner self issued time based tenure extend
2 parents 3da3394 + 62e072a commit 8e0c020

File tree

5 files changed

+190
-5
lines changed

5 files changed

+190
-5
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ jobs:
122122
- tests::signer::v0::signer_set_rollover
123123
- tests::signer::v0::signing_in_0th_tenure_of_reward_cycle
124124
- tests::signer::v0::continue_after_tenure_extend
125-
- tests::signer::v0::tenure_extend_after_idle
125+
- tests::signer::v0::tenure_extend_after_idle_signers
126+
- tests::signer::v0::tenure_extend_after_idle_miner
127+
- tests::signer::v0::tenure_extend_succeeds_after_rejected_attempt
126128
- tests::signer::v0::stx_transfers_dont_effect_idle_timeout
127129
- tests::signer::v0::idle_tenure_extend_active_mining
128130
- tests::signer::v0::multiple_miners_with_custom_chain_id

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
88
## [Unreleased]
99

1010
### Added
11+
- Add `tenure_timeout_secs` to the miner for determining when a time-based tenure extend should be attempted.
1112

1213
### Changed
1314

stackslib/src/config/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ const DEFAULT_FIRST_REJECTION_PAUSE_MS: u64 = 5_000;
9494
const DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS: u64 = 10_000;
9595
const DEFAULT_BLOCK_COMMIT_DELAY_MS: u64 = 20_000;
9696
const DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE: u8 = 25;
97+
// This should be greater than the signers' timeout. This is used for issuing fallback tenure extends
98+
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 420;
9799

98100
static HELIUM_DEFAULT_CONNECTION_OPTIONS: LazyLock<ConnectionOptions> =
99101
LazyLock::new(|| ConnectionOptions {
@@ -2151,6 +2153,8 @@ pub struct MinerConfig {
21512153
pub block_commit_delay: Duration,
21522154
/// The percentage of the remaining tenure cost limit to consume each block.
21532155
pub tenure_cost_limit_per_block_percentage: Option<u8>,
2156+
/// Duration to wait before attempting to issue a tenure extend
2157+
pub tenure_timeout: Duration,
21542158
}
21552159

21562160
impl Default for MinerConfig {
@@ -2187,6 +2191,7 @@ impl Default for MinerConfig {
21872191
tenure_cost_limit_per_block_percentage: Some(
21882192
DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE,
21892193
),
2194+
tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
21902195
}
21912196
}
21922197
}
@@ -2572,6 +2577,7 @@ pub struct MinerConfigFile {
25722577
pub subsequent_rejection_pause_ms: Option<u64>,
25732578
pub block_commit_delay_ms: Option<u64>,
25742579
pub tenure_cost_limit_per_block_percentage: Option<u8>,
2580+
pub tenure_timeout_secs: Option<u64>,
25752581
}
25762582

25772583
impl MinerConfigFile {
@@ -2712,6 +2718,7 @@ impl MinerConfigFile {
27122718
subsequent_rejection_pause_ms: self.subsequent_rejection_pause_ms.unwrap_or(miner_default_config.subsequent_rejection_pause_ms),
27132719
block_commit_delay: self.block_commit_delay_ms.map(Duration::from_millis).unwrap_or(miner_default_config.block_commit_delay),
27142720
tenure_cost_limit_per_block_percentage,
2721+
tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
27152722
})
27162723
}
27172724
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub struct BlockMinerThread {
139139
burnchain: Burnchain,
140140
/// Last block mined
141141
last_block_mined: Option<NakamotoBlock>,
142-
/// Number of blocks mined since a tenure change/extend
142+
/// Number of blocks mined since a tenure change/extend was attempted
143143
mined_blocks: u64,
144144
/// Copy of the node's registered VRF key
145145
registered_key: RegisteredKey,
@@ -160,6 +160,8 @@ pub struct BlockMinerThread {
160160
/// Handle to the p2p thread for block broadcast
161161
p2p_handle: NetworkHandle,
162162
signer_set_cache: Option<RewardSet>,
163+
/// The time at which tenure change/extend was attempted
164+
tenure_change_time: Instant,
163165
}
164166

165167
impl BlockMinerThread {
@@ -187,6 +189,7 @@ impl BlockMinerThread {
187189
reason,
188190
p2p_handle: rt.get_p2p_handle(),
189191
signer_set_cache: None,
192+
tenure_change_time: Instant::now(),
190193
}
191194
}
192195

@@ -1186,7 +1189,9 @@ impl BlockMinerThread {
11861189
if self.last_block_mined.is_some() {
11871190
// Check if we can extend the current tenure
11881191
let tenure_extend_timestamp = coordinator.get_tenure_extend_timestamp();
1189-
if get_epoch_time_secs() <= tenure_extend_timestamp {
1192+
if get_epoch_time_secs() <= tenure_extend_timestamp
1193+
&& self.tenure_change_time.elapsed() <= self.config.miner.tenure_timeout
1194+
{
11901195
return Ok(NakamotoTenureInfo {
11911196
coinbase_tx: None,
11921197
tenure_change_tx: None,
@@ -1195,6 +1200,8 @@ impl BlockMinerThread {
11951200
info!("Miner: Time-based tenure extend";
11961201
"current_timestamp" => get_epoch_time_secs(),
11971202
"tenure_extend_timestamp" => tenure_extend_timestamp,
1203+
"tenure_change_time_elapsed" => self.tenure_change_time.elapsed().as_secs(),
1204+
"tenure_timeout_secs" => self.config.miner.tenure_timeout.as_secs(),
11981205
);
11991206
self.tenure_extend_reset();
12001207
}
@@ -1265,6 +1272,7 @@ impl BlockMinerThread {
12651272
}
12661273

12671274
fn tenure_extend_reset(&mut self) {
1275+
self.tenure_change_time = Instant::now();
12681276
self.reason = MinerReason::Extended {
12691277
burn_view_consensus_hash: self.burn_block.consensus_hash,
12701278
};

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

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,8 +2580,8 @@ fn signers_broadcast_signed_blocks() {
25802580

25812581
#[test]
25822582
#[ignore]
2583-
/// This test verifies that a miner will produce a TenureExtend transaction after the idle timeout is reached.
2584-
fn tenure_extend_after_idle() {
2583+
/// This test verifies that a miner will produce a TenureExtend transaction after the signers' idle timeout is reached.
2584+
fn tenure_extend_after_idle_signers() {
25852585
if env::var("BITCOIND_TEST") != Ok("1".into()) {
25862586
return;
25872587
}
@@ -2629,6 +2629,173 @@ fn tenure_extend_after_idle() {
26292629
signer_test.shutdown();
26302630
}
26312631

2632+
#[test]
2633+
#[ignore]
2634+
/// This test verifies that a miner will produce a TenureExtend transaction after the miner's idle timeout
2635+
/// even if they do not see the signers' tenure extend timestamp responses.
2636+
fn tenure_extend_after_idle_miner() {
2637+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2638+
return;
2639+
}
2640+
2641+
tracing_subscriber::registry()
2642+
.with(fmt::layer())
2643+
.with(EnvFilter::from_default_env())
2644+
.init();
2645+
2646+
info!("------------------------- Test Setup -------------------------");
2647+
let num_signers = 5;
2648+
let sender_sk = Secp256k1PrivateKey::new();
2649+
let sender_addr = tests::to_addr(&sender_sk);
2650+
let send_amt = 100;
2651+
let send_fee = 180;
2652+
let _recipient = PrincipalData::from(StacksAddress::burn_address(false));
2653+
let idle_timeout = Duration::from_secs(30);
2654+
let miner_idle_timeout = idle_timeout + Duration::from_secs(10);
2655+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2656+
num_signers,
2657+
vec![(sender_addr, send_amt + send_fee)],
2658+
|config| {
2659+
config.tenure_idle_timeout = idle_timeout;
2660+
},
2661+
|config| {
2662+
config.miner.tenure_timeout = miner_idle_timeout;
2663+
},
2664+
None,
2665+
None,
2666+
);
2667+
let _http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
2668+
2669+
signer_test.boot_to_epoch_3();
2670+
2671+
info!("---- Nakamoto booted, starting test ----");
2672+
signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
2673+
2674+
info!("---- Start a new tenure but ignore block signatures so no timestamps are recorded ----");
2675+
let tip_height_before = get_chain_info(&signer_test.running_nodes.conf).stacks_tip_height;
2676+
TEST_IGNORE_SIGNERS.set(true);
2677+
next_block_and(
2678+
&mut signer_test.running_nodes.btc_regtest_controller,
2679+
30,
2680+
|| {
2681+
let tip_height = get_chain_info(&signer_test.running_nodes.conf).stacks_tip_height;
2682+
Ok(tip_height > tip_height_before)
2683+
},
2684+
)
2685+
.expect("Failed to mine the tenure change block");
2686+
2687+
// Now, wait for a block with a tenure change due to the new block
2688+
wait_for(30, || {
2689+
Ok(last_block_contains_tenure_change_tx(
2690+
TenureChangeCause::BlockFound,
2691+
))
2692+
})
2693+
.expect("Timed out waiting for a block with a tenure change");
2694+
2695+
info!("---- Waiting for a tenure extend ----");
2696+
2697+
TEST_IGNORE_SIGNERS.set(false);
2698+
// Now, wait for a block with a tenure extend
2699+
wait_for(miner_idle_timeout.as_secs() + 20, || {
2700+
Ok(last_block_contains_tenure_change_tx(
2701+
TenureChangeCause::Extended,
2702+
))
2703+
})
2704+
.expect("Timed out waiting for a block with a tenure extend");
2705+
signer_test.shutdown();
2706+
}
2707+
2708+
#[test]
2709+
#[ignore]
2710+
/// This test verifies that a miner that attempts to produce a tenure extend too early will be rejected by the signers,
2711+
/// but will eventually succeed after the signers' idle timeout has passed.
2712+
fn tenure_extend_succeeds_after_rejected_attempt() {
2713+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2714+
return;
2715+
}
2716+
2717+
tracing_subscriber::registry()
2718+
.with(fmt::layer())
2719+
.with(EnvFilter::from_default_env())
2720+
.init();
2721+
2722+
info!("------------------------- Test Setup -------------------------");
2723+
let num_signers = 5;
2724+
let sender_sk = Secp256k1PrivateKey::new();
2725+
let sender_addr = tests::to_addr(&sender_sk);
2726+
let send_amt = 100;
2727+
let send_fee = 180;
2728+
let _recipient = PrincipalData::from(StacksAddress::burn_address(false));
2729+
let idle_timeout = Duration::from_secs(30);
2730+
let miner_idle_timeout = Duration::from_secs(20);
2731+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2732+
num_signers,
2733+
vec![(sender_addr, send_amt + send_fee)],
2734+
|config| {
2735+
config.tenure_idle_timeout = idle_timeout;
2736+
},
2737+
|config| {
2738+
config.miner.tenure_timeout = miner_idle_timeout;
2739+
},
2740+
None,
2741+
None,
2742+
);
2743+
let _http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
2744+
2745+
signer_test.boot_to_epoch_3();
2746+
2747+
info!("---- Nakamoto booted, starting test ----");
2748+
signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
2749+
2750+
info!("---- Waiting for a rejected tenure extend ----");
2751+
// Now, wait for a block with a tenure extend proposal from the miner, but ensure it is rejected.
2752+
wait_for(30, || {
2753+
let block = test_observer::get_stackerdb_chunks()
2754+
.into_iter()
2755+
.flat_map(|chunk| chunk.modified_slots)
2756+
.find_map(|chunk| {
2757+
let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
2758+
.expect("Failed to deserialize SignerMessage");
2759+
if let SignerMessage::BlockProposal(proposal) = message {
2760+
if proposal.block.get_tenure_tx_payload().unwrap().cause
2761+
== TenureChangeCause::Extended
2762+
{
2763+
return Some(proposal.block);
2764+
}
2765+
}
2766+
None
2767+
});
2768+
let Some(block) = &block else {
2769+
return Ok(false);
2770+
};
2771+
let signatures = test_observer::get_stackerdb_chunks()
2772+
.into_iter()
2773+
.flat_map(|chunk| chunk.modified_slots)
2774+
.filter_map(|chunk| {
2775+
let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
2776+
.expect("Failed to deserialize SignerMessage");
2777+
if let SignerMessage::BlockResponse(BlockResponse::Rejected(rejected)) = message {
2778+
if block.header.signer_signature_hash() == rejected.signer_signature_hash {
2779+
return Some(rejected.signature);
2780+
}
2781+
}
2782+
None
2783+
})
2784+
.collect::<Vec<_>>();
2785+
Ok(signatures.len() >= num_signers * 7 / 10)
2786+
})
2787+
.expect("Test timed out while waiting for a rejected tenure extend");
2788+
2789+
info!("---- Waiting for an accepted tenure extend ----");
2790+
wait_for(idle_timeout.as_secs() + 10, || {
2791+
Ok(last_block_contains_tenure_change_tx(
2792+
TenureChangeCause::Extended,
2793+
))
2794+
})
2795+
.expect("Test timed out while waiting for an accepted tenure extend");
2796+
signer_test.shutdown();
2797+
}
2798+
26322799
#[test]
26332800
#[ignore]
26342801
/// Verify that Nakamoto blocks that don't modify the tenure's execution cost

0 commit comments

Comments
 (0)