Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: wait for outstanding proposals #5284

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use std::fmt::{Debug, Display};
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::ops::Range;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Sender;
use std::sync::Arc;
Expand Down Expand Up @@ -90,6 +91,14 @@ MinerSlotID {
BlockPushed = 1
});

impl MinerSlotID {
/// Return the u32 slot id for messages of this type from a given miner's
/// slot range in the .miners contract
pub fn get_slot_for_miner(&self, miner_range: &Range<u32>) -> u32 {
miner_range.start.saturating_add(self.to_u8().into())
}
}

impl MessageSlotIDTrait for MessageSlotID {
fn stacker_db_contract(&self, mainnet: bool, reward_cycle: u64) -> QualifiedContractIdentifier {
NakamotoSigners::make_signers_db_contract_id(reward_cycle, self.to_u32(), mainnet)
Expand Down Expand Up @@ -1238,6 +1247,21 @@ mod test {
assert_eq!(mock_block, deserialized_data);
}

#[test]
fn get_slot_for_miner() {
let miner_range = std::ops::Range { start: 7, end: 10 };
assert_eq!(
MinerSlotID::BlockProposal.get_slot_for_miner(&miner_range),
7,
"Block proposals should be in the first slot assigned to a miner"
);
assert_eq!(
MinerSlotID::BlockPushed.get_slot_for_miner(&miner_range),
8,
"Block pushes should be in the second slot assigned to a miner"
);
}

#[test]
fn test_backwards_compatibility() {
let block_rejected_hex = "010100000050426c6f636b206973206e6f7420612074656e7572652d737461727420626c6f636b2c20616e642068617320616e20756e7265636f676e697a65642074656e75726520636f6e73656e7375732068617368000691f95f84b7045f7dce7757052caa986ef042cb58f7df5031a3b5b5d0e3dda63e80000000006fb349212e1a1af1a3c712878d5159b5ec14636adb6f70be00a6da4ad4f88a9934d8a9abb229620dd8e0f225d63401e36c64817fb29e6c05591dcbe95c512df3";
Expand Down
29 changes: 23 additions & 6 deletions testnet/stacks-node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const DEFAULT_MAX_RBF_RATE: u64 = 150; // 1.5x
const DEFAULT_RBF_FEE_RATE_INCREMENT: u64 = 5;
const INV_REWARD_CYCLES_TESTNET: u64 = 6;
const DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS: u64 = 1000;
const DEFAULT_WAIT_FOR_PROPOSALS_SECS: u64 = 10;

#[derive(Clone, Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -2183,6 +2184,10 @@ pub struct MinerConfig {
/// 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
/// within the same second as its parent, it will be rejected by the signers.
pub min_time_between_blocks_ms: u64,
/// How much time (in seconds) to wait for an outstanding block
/// proposal from a parent tenure to get confirmed before
/// building a child block of that tenure.
pub wait_for_proposals_secs: u64,
}

impl Default for MinerConfig {
Expand Down Expand Up @@ -2213,6 +2218,7 @@ impl Default for MinerConfig {
max_reorg_depth: 3,
pre_nakamoto_mock_signing: false, // Should only default true if mining key is set
min_time_between_blocks_ms: DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS,
wait_for_proposals_secs: DEFAULT_WAIT_FOR_PROPOSALS_SECS,
}
}
}
Expand Down Expand Up @@ -2575,9 +2581,14 @@ pub struct MinerConfigFile {
pub max_reorg_depth: Option<u64>,
pub pre_nakamoto_mock_signing: Option<bool>,
pub min_time_between_blocks_ms: Option<u64>,
/// How much time (in seconds) to wait for an outstanding block
/// proposal from a parent tenure to get confirmed before
/// building a child block of that tenure.
pub wait_for_proposals_secs: Option<u64>,
}

impl MinerConfigFile {
#[cfg_attr(test, mutants::skip)]
fn into_config_default(self, miner_default_config: MinerConfig) -> Result<MinerConfig, String> {
let mining_key = self
.mining_key
Expand Down Expand Up @@ -2682,12 +2693,18 @@ impl MinerConfigFile {
pre_nakamoto_mock_signing: self
.pre_nakamoto_mock_signing
.unwrap_or(pre_nakamoto_mock_signing), // Should only default true if mining key is set
min_time_between_blocks_ms: self.min_time_between_blocks_ms.map(|ms| if ms < DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS {
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.");
DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS
} else {
ms
}).unwrap_or(miner_default_config.min_time_between_blocks_ms),
min_time_between_blocks_ms: self
.min_time_between_blocks_ms
.map(|ms| if ms < DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS {
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.");
DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS
} else {
ms
})
.unwrap_or(miner_default_config.min_time_between_blocks_ms),
wait_for_proposals_secs: self
.wait_for_proposals_secs
.unwrap_or(miner_default_config.wait_for_proposals_secs),
})
}
}
Expand Down
143 changes: 142 additions & 1 deletion testnet/stacks-node/src/nakamoto_node/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::time::{Duration, Instant};

use clarity::boot_util::boot_code_id;
use clarity::vm::types::PrincipalData;
use libsigner::v0::messages::{MinerSlotID, SignerMessage};
use libsigner::v0::messages::{MinerSlotID, SignerMessage, SignerMessageTypePrefix};
use libsigner::StackerDBSession;
use rand::{thread_rng, Rng};
use stacks::burnchains::Burnchain;
Expand All @@ -37,6 +37,7 @@ use stacks::chainstate::stacks::{
TenureChangeCause, TenureChangePayload, TransactionAnchorMode, TransactionPayload,
TransactionVersion,
};
use stacks::codec::StacksMessageCodec;
use stacks::net::p2p::NetworkHandle;
use stacks::net::stackerdb::StackerDBs;
use stacks::net::{NakamotoBlocksData, StacksMessageType};
Expand Down Expand Up @@ -150,6 +151,8 @@ pub struct BlockMinerThread {
/// Handle to the p2p thread for block broadcast
p2p_handle: NetworkHandle,
signer_set_cache: Option<RewardSet>,
/// UNIX epoch in seconds that the miner thread started
start_time: u64,
}

impl BlockMinerThread {
Expand All @@ -176,6 +179,7 @@ impl BlockMinerThread {
reason,
p2p_handle: rt.get_p2p_handle(),
signer_set_cache: None,
start_time: get_epoch_time_secs(),
}
}

Expand Down Expand Up @@ -264,6 +268,119 @@ impl BlockMinerThread {
Ok(())
}

/// See if there's an outstanding block proposal in the .miners stackerdb
/// from our parent tenure
#[cfg_attr(test, mutants::skip)]
fn check_outstanding_block_proposal(
parent_tenure_election_ch: &ConsensusHash,
stacks_parent_header: &StacksHeaderInfo,
sortdb: &SortitionDB,
stackerdbs: &StackerDBs,
is_mainnet: bool,
wait_for_parent_proposals_secs: u64,
my_start_time: u64,
) -> Result<(), NakamotoNodeError> {
let cur_burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
.expect("FATAL: failed to query sortition DB for canonical burn chain tip");

let parent_proposal_slot = match NakamotoChainState::get_miner_slot(
sortdb,
&cur_burn_chain_tip,
parent_tenure_election_ch,
) {
Ok(Some(parent_slots)) => MinerSlotID::BlockProposal.get_slot_for_miner(&parent_slots),
Ok(None) => {
debug!("Parent tenure no longer has a miner slot, will not check for outstanding proposals");
return Ok(());
}
Err(e) => {
info!(
"Failed to lookup miner slots for parent tenure, will not check for outstanding proposals";
"err" => ?e
);
return Ok(());
}
};
let miners_contract = boot_code_id(MINERS_NAME, is_mainnet);
let latest_chunk = match stackerdbs.get_latest_chunk(&miners_contract, parent_proposal_slot)
{
Ok(Some(chunk)) => chunk,
Ok(None) => {
debug!("Parent tenure slots have no proposal data");
return Ok(());
}
Err(e) => {
info!(
"Failed to read miner slot for parent tenure, will not check for outstanding proposals";
"err" => ?e
);
return Ok(());
}
};

let latest_proposal = match SignerMessage::consensus_deserialize(
&mut latest_chunk.as_slice(),
) {
Ok(SignerMessage::BlockProposal(proposal)) => proposal,
Ok(d) => {
info!(
"Parent tenure's miner slot contained data other than a proposal, will not check for outstanding proposals";
"message.type_prefix" => ?SignerMessageTypePrefix::from(&d),
);
return Ok(());
}
Err(e) => {
if latest_chunk.len() > 0 {
info!(
"Failed to parse message in parent tenure's miner slot, will not check for outstanding proposals";
"err" => ?e
);
}
return Ok(());
}
};

if latest_proposal.block.header.chain_length <= stacks_parent_header.stacks_block_height {
debug!("Parent block proposal found, and its the block we are building on top of");
return Ok(());
}

let proposal_timestamp = latest_proposal.block.header.timestamp;
let current_time = get_epoch_time_secs();

if proposal_timestamp > current_time {
// don't wait for insanity
debug!(
"Found outstanding parent block proposal, but its timestamp is in the future, ignoring.";
);
return Ok(());
}

// if enough time has passed, this proposal should have been accepted already, so just ignore it
if current_time.saturating_sub(proposal_timestamp) > wait_for_parent_proposals_secs {
debug!(
"Found outstanding parent block proposal, but enough time has passed that this proposal should be ignored.";
);
return Ok(());
}

// if enough time has passed since the miner thread itself started, just ignore the proposal
if current_time.saturating_sub(my_start_time) > wait_for_parent_proposals_secs {
debug!(
"Found outstanding parent block proposal, but enough time has passed since miner thread started that this proposal should be ignored.";
);
return Ok(());
}

info!("Found outstanding parent block proposal, which the signer set could still be considering");

// otherwise, there *is* an outstanding proposal, which the signer set could still be considering
// signal the miner thread to abort and retry
return Err(NakamotoNodeError::MiningFailure(
ChainstateError::MinerAborted,
));
}

pub fn run_miner(
mut self,
prior_miner: Option<JoinHandle<Result<(), NakamotoNodeError>>>,
Expand Down Expand Up @@ -1023,6 +1140,30 @@ impl BlockMinerThread {
return Err(NakamotoNodeError::ParentNotFound);
};

if self.last_block_mined.is_none() {
let Some(ParentTenureInfo {
ref parent_tenure_consensus_hash,
..
}) = parent_block_info.parent_tenure
else {
warn!(
"Miner should be starting a new tenure, but failed to load parent tenure info"
);
return Err(NakamotoNodeError::ParentNotFound);
};
let stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), false)
.map_err(|e| NakamotoNodeError::MiningFailure(ChainstateError::NetError(e)))?;
Self::check_outstanding_block_proposal(
parent_tenure_consensus_hash,
&parent_block_info.stacks_parent_header,
&burn_db,
&stackerdbs,
self.config.is_mainnet(),
self.config.miner.wait_for_proposals_secs,
self.start_time,
)?;
}

// create our coinbase if this is the first block we've mined this tenure
let tenure_start_info = self.make_tenure_start_info(
&chain_state,
Expand Down
19 changes: 18 additions & 1 deletion testnet/stacks-node/src/tests/signer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
mod v0;

use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -512,6 +512,23 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
.collect::<Vec<_>>()
}

/// Get the slot id for each signer
fn get_slot_per_signer(&self, reward_cycle: u64) -> HashMap<Secp256k1PublicKey, SignerSlotID> {
let slots = self
.get_signer_slots(reward_cycle)
.expect("FATAL: failed to get signer slots from stackerdb");
let mut signer_to_slot_id = HashMap::new();
for signer_config in self.signer_configs.iter() {
let pk = Secp256k1PublicKey::from_private(&signer_config.stacks_private_key);
for (slot_id, (address, _)) in slots.iter().enumerate() {
if address == &signer_config.stacks_address {
signer_to_slot_id.insert(pk, SignerSlotID(u32::try_from(slot_id).unwrap()));
}
}
}
signer_to_slot_id
}

/// Get the signer public keys for the given reward cycle
fn get_signer_public_keys(&self, reward_cycle: u64) -> Vec<StacksPublicKey> {
let entries = self.get_reward_set_signers(reward_cycle);
Expand Down
Loading
Loading