Skip to content

Commit

Permalink
Merge pull request #4827 from stacks-network/feat/tenure-extend-trans…
Browse files Browse the repository at this point in the history
…actions

Feat/tenure extend transactions
  • Loading branch information
jferrant authored Jun 20, 2024
2 parents 49cca7d + 11a6262 commit 150ac0d
Show file tree
Hide file tree
Showing 7 changed files with 563 additions and 108 deletions.
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 @@ -82,6 +82,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 @@ -568,6 +569,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 @@ -845,7 +845,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 @@ -3064,44 +3064,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
166 changes: 117 additions & 49 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 @@ -641,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 @@ -682,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 @@ -858,39 +885,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 @@ -938,7 +944,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 @@ -995,6 +1001,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

0 comments on commit 150ac0d

Please sign in to comment.