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/tenure extend transactions #4827

Merged
merged 15 commits into from
Jun 20, 2024
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ jobs:
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
- tests::nakamoto_integrations::check_block_heights
- tests::nakamoto_integrations::clarity_burn_state
- tests::nakamoto_integrations::continue_tenure_extend
# Do not run this one until we figure out why it fails in CI
# - tests::neon_integrations::bitcoin_reorg_flap
# - tests::neon_integrations::bitcoin_reorg_flap_with_follower
Expand Down
2 changes: 2 additions & 0 deletions stackslib/src/chainstate/nakamoto/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ use crate::util_lib::boot::boot_code_id;
use crate::util_lib::db::Error as DBError;

/// Nakamaoto tenure information
#[derive(Debug)]
pub struct NakamotoTenureInfo {
/// Coinbase tx, if this is a new tenure
pub coinbase_tx: Option<StacksTransaction>,
Expand Down Expand Up @@ -505,6 +506,7 @@ impl NakamotoBlockBuilder {
"execution_consumed" => %consumed,
"%-full" => block_limit.proportion_largest_dimension(&consumed),
"assembly_time_ms" => ts_end.saturating_sub(ts_start),
"consensus_hash" => %block.header.consensus_hash
);

Ok((block, consumed, size, tx_events))
Expand Down
62 changes: 24 additions & 38 deletions stackslib/src/chainstate/nakamoto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ impl NakamotoBlock {
// discontinuous
warn!(
"Invalid block -- discontiguous";
"previosu_tenure_end" => %tc_payload.previous_tenure_end,
"previous_tenure_end" => %tc_payload.previous_tenure_end,
"parent_block_id" => %self.header.parent_block_id
);
return Err(());
Expand Down Expand Up @@ -2948,44 +2948,30 @@ impl NakamotoChainState {
let burn_header_height = tenure_block_snapshot.block_height;
let block_hash = block.header.block_hash();

let new_tenure = match block.is_wellformed_tenure_start_block() {
Ok(true) => true,
Ok(false) => {
// this block is mined in the ongoing tenure.
if !Self::check_tenure_continuity(
chainstate_tx,
burn_dbconn,
&parent_ch,
&block.header,
)? {
// this block is not part of the ongoing tenure; it's invalid
return Err(ChainstateError::ExpectedTenureChange);
}
false
}
Err(_) => {
return Err(ChainstateError::InvalidStacksBlock(
"Invalid tenure changes in nakamoto block".into(),
));
}
};
let new_tenure = block.is_wellformed_tenure_start_block().map_err(|_| {
ChainstateError::InvalidStacksBlock("Invalid tenure changes in nakamoto block".into())
})?;
// this block is mined in the ongoing tenure.
if !new_tenure
&& !Self::check_tenure_continuity(
chainstate_tx,
burn_dbconn,
&parent_ch,
&block.header,
)?
{
// this block is not part of the ongoing tenure; it's invalid
return Err(ChainstateError::ExpectedTenureChange);
}
let tenure_extend = block.is_wellformed_tenure_extend_block().map_err(|_| {
ChainstateError::InvalidStacksBlock("Invalid tenure changes in nakamoto block".into())
})?;

let tenure_extend = match block.is_wellformed_tenure_extend_block() {
Ok(true) => {
if new_tenure {
return Err(ChainstateError::InvalidStacksBlock(
"Both started and extended tenure".into(),
));
}
true
}
Ok(false) => false,
Err(_) => {
return Err(ChainstateError::InvalidStacksBlock(
"Invalid tenure extend in nakamoto block".into(),
));
}
};
if tenure_extend && new_tenure {
return Err(ChainstateError::InvalidStacksBlock(
"Both started and extended tenure".into(),
));
}

let parent_coinbase_height = if block.is_first_mined() {
0
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/nakamoto/tenure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,7 @@ impl NakamotoChainState {
}

/// Check that this block is in the same tenure as its parent, and that this tenure is the
/// highest-seen tenure. Use this to check blocks that do _not_ have tenure-changes.
/// highest-seen tenure. Use this to check blocks that do _not_ have BlockFound tenure-changes.
///
/// Returns Ok(bool) to indicate whether or not this block is in the same tenure as its parent.
/// Returns Err(..) on DB error
Expand Down
179 changes: 117 additions & 62 deletions testnet/stacks-node/src/nakamoto_node/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ use stacks::util::secp256k1::MessageSignature;
use stacks_common::codec::read_next;
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
use stacks_common::types::{PrivateKey, StacksEpochId};
use stacks_common::util::hash::Hash160;
use stacks_common::util::vrf::VRFProof;
use wsts::curve::point::Point;
use wsts::curve::scalar::Scalar;
Expand Down Expand Up @@ -77,8 +76,12 @@ pub enum MinerDirective {
StopTenure,
}

#[derive(PartialEq, Debug, Clone)]
/// Tenure info needed to construct a tenure change or tenure extend transaction
struct ParentTenureInfo {
/// The number of blocks in the parent tenure
parent_tenure_blocks: u64,
/// The consensus hash of the parent tenure
parent_tenure_consensus_hash: ConsensusHash,
}

Expand All @@ -91,6 +94,33 @@ struct ParentStacksBlockInfo {
parent_tenure: Option<ParentTenureInfo>,
}

/// The reason the miner thread was spawned
#[derive(PartialEq, Clone, Debug)]
pub enum MinerReason {
/// The miner thread was spawned to begin a new tenure
BlockFound,
/// The miner thread was spawned to extend an existing tenure
Extended {
/// Current consensus hash on the underlying burnchain. Corresponds to the last-seen
/// sortition.
burn_view_consensus_hash: ConsensusHash,
},
}

impl std::fmt::Display for MinerReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MinerReason::BlockFound => write!(f, "BlockFound"),
MinerReason::Extended {
burn_view_consensus_hash,
} => write!(
f,
"Extended: burn_view_consensus_hash = {burn_view_consensus_hash:?}",
),
}
}
}

pub struct BlockMinerThread {
/// node config struct
config: Config,
Expand All @@ -105,20 +135,26 @@ pub struct BlockMinerThread {
/// Copy of the node's registered VRF key
registered_key: RegisteredKey,
/// Burnchain block snapshot which elected this miner
burn_election_block: BlockSnapshot,
/// Current burnchain tip
burn_block: BlockSnapshot,
/// The start of the parent tenure for this tenure
parent_tenure_id: StacksBlockId,
/// Handle to the node's event dispatcher
event_dispatcher: EventDispatcher,
/// The reason the miner thread was spawned
reason: MinerReason,
}

impl BlockMinerThread {
/// Instantiate the miner thread
pub fn new(
rt: &RelayerThread,
registered_key: RegisteredKey,
burn_election_block: BlockSnapshot,
burn_block: BlockSnapshot,
parent_tenure_id: StacksBlockId,
reason: MinerReason,
) -> BlockMinerThread {
BlockMinerThread {
config: rt.config.clone(),
Expand All @@ -127,9 +163,11 @@ impl BlockMinerThread {
burnchain: rt.burnchain.clone(),
mined_blocks: vec![],
registered_key,
burn_election_block,
burn_block,
event_dispatcher: rt.event_dispatcher.clone(),
parent_tenure_id,
reason,
}
}

Expand Down Expand Up @@ -165,6 +203,7 @@ impl BlockMinerThread {
"parent_tenure_id" => %self.parent_tenure_id,
"thread_id" => ?thread::current().id(),
"burn_block_consensus_hash" => %self.burn_block.consensus_hash,
"reason" => %self.reason,
);
if let Some(prior_miner) = prior_miner {
Self::stop_miner(&self.globals, prior_miner)?;
Expand Down Expand Up @@ -308,19 +347,6 @@ impl BlockMinerThread {
))
})?;

let reward_cycle = self
.burnchain
.pox_constants
.block_height_to_reward_cycle(
self.burnchain.first_block_height,
self.burn_block.block_height,
)
.ok_or_else(|| {
NakamotoNodeError::SigningCoordinatorFailure(
"Building on a burn block that is before the first burn block".into(),
)
})?;

let reward_info = match load_nakamoto_reward_set(
self.burnchain
.pox_reward_cycle(tip.block_height.saturating_add(1))
Expand Down Expand Up @@ -654,25 +680,13 @@ impl BlockMinerThread {
}

fn generate_tenure_change_tx(
&mut self,
&self,
nonce: u64,
parent_block_id: StacksBlockId,
parent_tenure_consensus_hash: ConsensusHash,
parent_tenure_blocks: u64,
miner_pkh: Hash160,
payload: TenureChangePayload,
) -> Result<StacksTransaction, NakamotoNodeError> {
let is_mainnet = self.config.is_mainnet();
let chain_id = self.config.burnchain.chain_id;
let tenure_change_tx_payload = TransactionPayload::TenureChange(TenureChangePayload {
tenure_consensus_hash: self.burn_block.consensus_hash.clone(),
prev_tenure_consensus_hash: parent_tenure_consensus_hash,
burn_view_consensus_hash: self.burn_block.consensus_hash.clone(),
previous_tenure_end: parent_block_id,
previous_tenure_blocks: u32::try_from(parent_tenure_blocks)
.expect("FATAL: more than u32 blocks in a tenure"),
cause: TenureChangeCause::BlockFound,
pubkey_hash: miner_pkh,
});
let tenure_change_tx_payload = TransactionPayload::TenureChange(payload);

let mut tx_auth = self.keychain.get_transaction_auth().unwrap();
tx_auth.set_origin_nonce(nonce);
Expand All @@ -695,7 +709,7 @@ impl BlockMinerThread {

/// Create a coinbase transaction.
fn generate_coinbase_tx(
&mut self,
&self,
nonce: u64,
epoch_id: StacksEpochId,
vrf_proof: VRFProof,
Expand Down Expand Up @@ -867,39 +881,18 @@ impl BlockMinerThread {
.make_vrf_proof()
.ok_or_else(|| NakamotoNodeError::BadVrfConstruction)?;

if self.mined_blocks.is_empty() {
if parent_block_info.parent_tenure.is_none() {
warn!(
"Miner should be starting a new tenure, but failed to load parent tenure info"
);
return Err(NakamotoNodeError::ParentNotFound);
}
}

let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash();
if self.mined_blocks.is_empty() && parent_block_info.parent_tenure.is_none() {
warn!("Miner should be starting a new tenure, but failed to load parent tenure info");
return Err(NakamotoNodeError::ParentNotFound);
};

// create our coinbase if this is the first block we've mined this tenure
let tenure_start_info = if let Some(ref par_tenure_info) = parent_block_info.parent_tenure {
let current_miner_nonce = parent_block_info.coinbase_nonce;
let tenure_change_tx = self.generate_tenure_change_tx(
current_miner_nonce,
parent_block_id,
par_tenure_info.parent_tenure_consensus_hash,
par_tenure_info.parent_tenure_blocks,
self.keychain.get_nakamoto_pkh(),
)?;
let coinbase_tx =
self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof);
NakamotoTenureInfo {
coinbase_tx: Some(coinbase_tx),
tenure_change_tx: Some(tenure_change_tx),
}
} else {
NakamotoTenureInfo {
coinbase_tx: None,
tenure_change_tx: None,
}
};
let tenure_start_info = self.make_tenure_start_info(
&chain_state,
&parent_block_info,
vrf_proof,
target_epoch_id,
)?;

parent_block_info.stacks_parent_header.microblock_tail = None;

Expand Down Expand Up @@ -953,7 +946,7 @@ impl BlockMinerThread {
.map_err(|_| NakamotoNodeError::UnexpectedChainState)?,
&mut mem_pool,
&parent_block_info.stacks_parent_header,
&self.burn_block.consensus_hash,
&self.burn_election_block.consensus_hash,
self.burn_block.total_burn,
tenure_start_info,
self.config
Expand Down Expand Up @@ -1010,6 +1003,68 @@ impl BlockMinerThread {
Ok(block)
}

/// Create the tenure start info for the block we're going to build
fn make_tenure_start_info(
&self,
chainstate: &StacksChainState,
parent_block_info: &ParentStacksBlockInfo,
vrf_proof: VRFProof,
target_epoch_id: StacksEpochId,
) -> Result<NakamotoTenureInfo, NakamotoNodeError> {
let current_miner_nonce = parent_block_info.coinbase_nonce;
let Some(parent_tenure_info) = &parent_block_info.parent_tenure else {
return Ok(NakamotoTenureInfo {
coinbase_tx: None,
tenure_change_tx: None,
});
};

let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash();
let mut payload = TenureChangePayload {
tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(),
prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash,
burn_view_consensus_hash: self.burn_election_block.consensus_hash.clone(),
previous_tenure_end: parent_block_id,
previous_tenure_blocks: u32::try_from(parent_tenure_info.parent_tenure_blocks)
.expect("FATAL: more than u32 blocks in a tenure"),
cause: TenureChangeCause::BlockFound,
pubkey_hash: self.keychain.get_nakamoto_pkh(),
};

let (tenure_change_tx, coinbase_tx) = match &self.reason {
MinerReason::BlockFound => {
let tenure_change_tx =
self.generate_tenure_change_tx(current_miner_nonce, payload)?;
let coinbase_tx =
self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof);
(Some(tenure_change_tx), Some(coinbase_tx))
}
MinerReason::Extended {
burn_view_consensus_hash,
} => {
let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length(
chainstate.db(),
&self.burn_election_block.consensus_hash,
)
.map_err(NakamotoNodeError::MiningFailure)?;
debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far);
payload = payload.extend(
*burn_view_consensus_hash,
parent_block_id,
num_blocks_so_far,
);
let tenure_change_tx =
self.generate_tenure_change_tx(current_miner_nonce, payload)?;
(Some(tenure_change_tx), None)
}
};

Ok(NakamotoTenureInfo {
coinbase_tx,
tenure_change_tx,
})
}

/// Check if the tenure needs to change -- if so, return a BurnchainTipChanged error
fn check_burn_tip_changed(&self, sortdb: &SortitionDB) -> Result<(), NakamotoNodeError> {
let cur_burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
Expand Down
Loading