Skip to content

Commit

Permalink
Merge pull request #2920 from blockstack/fix/2904
Browse files Browse the repository at this point in the history
2.05: make it so a block whose anchored parent is in a different epoch cannot confirm a microblock stream
  • Loading branch information
jcnelson authored Nov 15, 2021
2 parents 57c711c + 62d38bc commit a750638
Show file tree
Hide file tree
Showing 9 changed files with 861 additions and 62 deletions.
7 changes: 7 additions & 0 deletions src/burnchains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,13 @@ pub mod test {
txop.txid =
Txid::from_test_data(txop.block_height, txop.vtxindex, &txop.burn_header_hash, 0);

let epoch = SortitionDB::get_stacks_epoch(ic, txop.block_height)
.unwrap()
.expect(&format!("BUG: no epoch for height {}", &txop.block_height));
if epoch.epoch_id == StacksEpochId::Epoch2_05 {
txop.memo = vec![STACKS_EPOCH_2_05_MARKER];
}

self.txs
.push(BlockstackOperationType::LeaderBlockCommit(txop.clone()));

Expand Down
5 changes: 4 additions & 1 deletion src/chainstate/burn/db/sortdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,10 @@ impl<'a> SortitionHandleConn<'a> {
Ok(winning_user_burns)
}

/// Get the block snapshot of the parent stacks block of the given stacks block
/// Get the block snapshot of the parent stacks block of the given stacks block.
/// The returned block-commit is for the given (consensus_hash, block_hash).
/// The returned BlockSnapshot is for the parent of the block identified by (consensus_hash,
/// block_hash).
pub fn get_block_snapshot_of_parent_stacks_block(
&self,
consensus_hash: &ConsensusHash,
Expand Down
11 changes: 11 additions & 0 deletions src/chainstate/stacks/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,12 @@ impl StacksBlockHeader {
// * state_index_root (validated on process_block())
Ok(())
}

/// Does this header have a microblock parent?
pub fn has_microblock_parent(&self) -> bool {
self.parent_microblock != EMPTY_MICROBLOCK_PARENT_HASH
|| self.parent_microblock_sequence != 0
}
}

impl StacksMessageCodec for StacksBlock {
Expand Down Expand Up @@ -628,6 +634,11 @@ impl StacksBlock {
}
return true;
}

/// Does this block have a microblock parent?
pub fn has_microblock_parent(&self) -> bool {
self.header.has_microblock_parent()
}
}

impl StacksMessageCodec for StacksMicroblockHeader {
Expand Down
1 change: 1 addition & 0 deletions src/chainstate/stacks/db/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ mod test {
&user_burns,
&ExecutionCost::zero(),
123,
false,
)
.unwrap();
tx.commit().unwrap();
Expand Down
90 changes: 85 additions & 5 deletions src/chainstate/stacks/db/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3104,12 +3104,36 @@ impl StacksChainState {
return Some((end, None));
}

/// Validate an anchored block against the burn chain state.
/// Determine whether or not a block executed an epoch transition. That is, did this block
/// call `initialize_epoch_2_05()` or similar when it was processed.
pub fn block_crosses_epoch_boundary(
block_conn: &DBConn,
parent_consensus_hash: &ConsensusHash,
parent_block_hash: &BlockHeaderHash,
) -> Result<bool, db_error> {
let sql = "SELECT 1 FROM epoch_transitions WHERE block_id = ?1";
let args: &[&dyn ToSql] = &[&StacksBlockHeader::make_index_block_hash(
parent_consensus_hash,
parent_block_hash,
)];
let res = block_conn
.query_row(sql, args, |_r| Ok(()))
.optional()
.map(|x| x.is_some())?;

Ok(res)
}

/// Validate an anchored block against the burn chain state. Determines if this given Stacks
/// block can attach to the chainstate. Called before inserting the block into the staging
/// DB.
///
/// Returns Some(commit burn, total burn) if valid
/// Returns None if not valid
/// * consensus_hash is the PoX history hash of the burnchain block whose sortition
/// (ostensibly) selected this block for inclusion.
fn validate_anchored_block_burnchain(
blocks_conn: &DBConn,
db_handle: &SortitionHandleConn,
consensus_hash: &ConsensusHash,
block: &StacksBlock,
Expand Down Expand Up @@ -3190,6 +3214,27 @@ impl StacksChainState {
return Ok(None);
}

// NEW in 2.05
// if the parent block marks an epoch transition, then its children necessarily run in a
// different Clarity epoch. Its children therefore are not permitted to confirm any of
// their parents' microblocks.
if StacksChainState::block_crosses_epoch_boundary(
blocks_conn,
&stacks_chain_tip.consensus_hash,
&stacks_chain_tip.winning_stacks_block_hash,
)? {
if block.has_microblock_parent() {
warn!(
"Invalid block {}/{}: its parent {}/{} crossed the epoch boundary but this block confirmed its microblocks",
&consensus_hash,
&block.block_hash(),
&stacks_chain_tip.consensus_hash,
&stacks_chain_tip.winning_stacks_block_hash
);
return Ok(None);
}
}

let sortition_burns =
SortitionDB::get_block_burn_amount(db_handle, &penultimate_sortition_snapshot)
.expect("FATAL: have block commit but no total burns in its sortition");
Expand All @@ -3204,7 +3249,10 @@ impl StacksChainState {
/// to the blockchain. The consensus_hash is the hash of the burnchain block whose sortition
/// elected the given Stacks block.
///
/// If we find the same Stacks block in two or more burnchain forks, insert it there too
/// If we find the same Stacks block in two or more burnchain forks, insert it there too.
///
/// (New in 2.05+) If the anchored block descends from a parent anchored block in a different
/// system epoch, then it *must not* have a parent microblock stream.
///
/// sort_ic: an indexed connection to a sortition DB
/// consensus_hash: this is the consensus hash of the sortition that chose this block
Expand Down Expand Up @@ -3274,6 +3322,7 @@ impl StacksChainState {

// does this block match the burnchain state? skip if not
let validation_res = StacksChainState::validate_anchored_block_burnchain(
&block_tx,
&sort_handle,
consensus_hash,
block,
Expand Down Expand Up @@ -3896,10 +3945,11 @@ impl StacksChainState {

/// If an epoch transition occurs at this Stacks block,
/// apply the transition and return any receipts from the transition.
/// Return (applied?, receipts)
pub fn process_epoch_transition(
clarity_tx: &mut ClarityTx,
chain_tip_burn_header_height: u32,
) -> Result<Vec<StacksTransactionReceipt>, Error> {
) -> Result<(bool, Vec<StacksTransactionReceipt>), Error> {
// is this stacks block the first of a new epoch?
let (stacks_parent_epoch, sortition_epoch) = clarity_tx.with_clarity_db_readonly(|db| {
(
Expand All @@ -3909,6 +3959,7 @@ impl StacksChainState {
});

let mut receipts = vec![];
let mut applied = false;

if let Some(sortition_epoch) = sortition_epoch {
// the parent stacks block has a different epoch than what the Sortition DB
Expand All @@ -3929,14 +3980,15 @@ impl StacksChainState {
"Should only transition from Epoch20 to Epoch2_05"
);
receipts.push(clarity_tx.block.initialize_epoch_2_05()?);
applied = true;
}
StacksEpochId::Epoch2_05 => {
panic!("No defined transition from Epoch2_05 forward")
}
}
}
}
Ok(receipts)
Ok((applied, receipts))
}

/// Process any Stacking-related bitcoin operations
Expand Down Expand Up @@ -4263,6 +4315,31 @@ impl StacksChainState {

let mainnet = chainstate_tx.get_config().mainnet;
let next_block_height = block.header.total_work.work;
let applied_epoch_transition;

// NEW in 2.05
// if the parent marked an epoch transition -- i.e. its children necessarily run in
// different Clarity epochs -- then this block cannot confirm any of its microblocks.
if StacksChainState::block_crosses_epoch_boundary(
chainstate_tx.deref(),
&parent_chain_tip.consensus_hash,
&parent_chain_tip.anchored_header.block_hash(),
)? {
debug!(
"Block {}/{} (mblock parent {}) crosses epoch boundary from parent {}/{}",
chain_tip_consensus_hash,
&block.block_hash(),
&block.header.parent_microblock,
&parent_chain_tip.consensus_hash,
&parent_chain_tip.anchored_header.block_hash()
);
if block.has_microblock_parent() {
let msg =
"Invalid block, mined in different epoch than parent but confirms microblocks";
warn!("{}", &msg);
return Err(Error::InvalidStacksBlock(msg.to_string()));
}
}

// find matured miner rewards, so we can grant them within the Clarity DB tx.
let latest_matured_miners = StacksChainState::get_scheduled_block_rewards(
Expand Down Expand Up @@ -4494,11 +4571,13 @@ impl StacksChainState {
"evaluated_epoch" => %evaluated_epoch);

// is this stacks block the first of a new epoch?
let mut receipts = StacksChainState::process_epoch_transition(
let (epoch_transition, mut receipts) = StacksChainState::process_epoch_transition(
&mut clarity_tx,
chain_tip_burn_header_height,
)?;

applied_epoch_transition = epoch_transition;

// process stacking operations from bitcoin ops
receipts.extend(StacksChainState::process_stacking_ops(
&mut clarity_tx,
Expand Down Expand Up @@ -4698,6 +4777,7 @@ impl StacksChainState {
user_burns,
&block_execution_cost,
block_size,
applied_epoch_transition,
)
.expect("FATAL: failed to advance chain tip");

Expand Down
Loading

0 comments on commit a750638

Please sign in to comment.