Skip to content

Commit

Permalink
feat: more rigor when applying .signers transformations, fix tests, u…
Browse files Browse the repository at this point in the history
…pdate anchor block selection, fix miner issue
  • Loading branch information
kantai committed Jan 24, 2024
1 parent 8f9670f commit 8056694
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 201 deletions.
34 changes: 23 additions & 11 deletions stackslib/src/burnchains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,17 +520,6 @@ impl PoxConstants {
(effective_height % u64::from(self.reward_cycle_length)) == 1
}

pub fn is_prepare_phase_start(&self, first_block_height: u64, burn_height: u64) -> bool {
if burn_height < first_block_height {
false
} else {
let effective_height = burn_height - first_block_height;
(effective_height + u64::from(self.prepare_length))
% u64::from(self.reward_cycle_length)
== 0
}
}

pub fn reward_cycle_to_block_height(&self, first_block_height: u64, reward_cycle: u64) -> u64 {
// NOTE: the `+ 1` is because the height of the first block of a reward cycle is mod 1, not
// mod 0.
Expand All @@ -549,6 +538,29 @@ impl PoxConstants {
)
}

/// Return the reward cycle that the current prepare phase corresponds to if `block_height` is _in_ a prepare
/// phase. If it is not in a prepare phase, return None.
pub fn reward_cycle_of_prepare_phase(
&self,
first_block_height: u64,
block_height: u64,
) -> Option<u64> {
if !self.is_in_prepare_phase(first_block_height, block_height) {
return None;
}
// the None branches here should be unreachable, because if `first_block_height > block_height`,
// `is_in_prepare_phase` would have returned false, but no need to be unsafe anyways.
let effective_height = block_height.checked_sub(first_block_height)?;
let current_cycle = self.block_height_to_reward_cycle(first_block_height, block_height)?;
if effective_height % u64::from(self.reward_cycle_length) == 0 {
// if this is the "mod 0" block of a prepare phase, its corresponding reward cycle is the current one
Some(current_cycle)
} else {
// otherwise, the corresponding reward cycle is actually the _next_ reward cycle
Some(current_cycle + 1)
}
}

pub fn is_in_prepare_phase(&self, first_block_height: u64, block_height: u64) -> bool {
Self::static_is_in_prepare_phase(
first_block_height,
Expand Down
199 changes: 86 additions & 113 deletions stackslib/src/chainstate/nakamoto/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ impl<'a, T: BlockEventDispatcher> OnChainRewardSetProvider<'a, T> {
sortdb: &SortitionDB,
block_id: &StacksBlockId,
) -> Result<RewardSet, Error> {
// TODO: this method should read the .signers contract to get the reward set entries.
// they will have been set via `NakamotoChainState::check_and_handle_prepare_phase_start()`.
let cycle = burnchain
.block_height_to_reward_cycle(cycle_start_burn_height)
.expect("FATAL: no reward cycle for burn height");

let mut registered_addrs =
let registered_addrs =
chainstate.get_reward_addresses_in_cycle(burnchain, sortdb, cycle, block_id)?;

let liquid_ustx = chainstate.get_liquid_ustx(block_id);
Expand Down Expand Up @@ -194,9 +196,9 @@ pub fn get_nakamoto_reward_cycle_info<U: RewardSetProvider>(
"reward_cycle_length" => burnchain.pox_constants.reward_cycle_length,
"prepare_phase_length" => burnchain.pox_constants.prepare_length);

// find the last tenure-start Stacks block processed in the preceeding prepare phase
// (i.e. the first block in the tenure of the parent of the first Stacks block processed in the prepare phase).
// Note that we may not have processed it yet. But, if we do find it, then it's
// Find the first Stacks block in this reward cycle's preceding prepare phase.
// This block will have invoked `.signers.stackerdb-set-signer-slots()` with the reward set.
// Note that we may not have processed it yet. But, if we do find it, then it's
// unique (and since Nakamoto Stacks blocks are processed in order, the anchor block
// cannot change later).
let prepare_phase_sortitions =
Expand All @@ -215,125 +217,99 @@ pub fn get_nakamoto_reward_cycle_info<U: RewardSetProvider>(
return Ok(None);
};

for sn in prepare_phase_sortitions.into_iter() {
if !sn.sortition {
continue;
}
// iterate over the prepare_phase_sortitions, finding the first such sortition
// with a processed stacks block
let Some(anchor_block_header) = prepare_phase_sortitions
.into_iter()
.find_map(|sn| {
if !sn.sortition {
return None
}

// find the first Stacks block processed in the prepare phase
let parent_block_id = if let Some(nakamoto_start_block) =
NakamotoChainState::get_nakamoto_tenure_start_block_header(
match NakamotoChainState::get_nakamoto_tenure_start_block_header(
chain_state.db(),
&sn.consensus_hash,
)? {
nakamoto_start_block
.anchored_header
.as_stacks_nakamoto()
// TODO: maybe `get_nakamoto_tenure_start_block_header` should
// return a type that doesn't require this unwrapping?
.expect("FATAL: queried non-Nakamoto tenure start header")
.parent_block_id
} else {
let Some(block_header) =
StacksChainState::get_stacks_block_header_info_by_consensus_hash(
chain_state.db(),
&sn.consensus_hash,
)?
else {
// no header for this snapshot (possibly invalid)
debug!("Failed to find block by consensus hash"; "consensus_hash" => %sn.consensus_hash);
continue;
};
let Some(parent_block_id) = StacksChainState::get_parent_block_id(
chain_state.db(),
&block_header.index_block_hash(),
)?
else {
debug!("Failed to get parent block"; "block_id" => %block_header.index_block_hash());
continue;
};
parent_block_id
};

// find the tenure-start block of the tenure of the parent of this Stacks block.
// in epoch 2, this is the preceding anchor block
// in nakamoto, this is the tenure-start block of the preceding tenure
let parent_block_header =
NakamotoChainState::get_block_header(chain_state.db(), &parent_block_id)?
.expect("FATAL: no parent for processed Stacks block in prepare phase");

let anchor_block_header = match &parent_block_header.anchored_header {
StacksBlockHeaderTypes::Epoch2(..) => parent_block_header.clone(),
StacksBlockHeaderTypes::Nakamoto(..) => {
NakamotoChainState::get_nakamoto_tenure_start_block_header(
chain_state.db(),
&parent_block_header.consensus_hash,
)?
.expect("FATAL: no parent for processed Stacks block in prepare phase")
) {
Ok(Some(x)) => return Some(Ok(x)),
Err(e) => return Some(Err(e)),
Ok(None) => {}, // pass: if cannot find nakamoto block, maybe it was a 2.x block?
}
};

let anchor_block_sn = SortitionDB::get_block_snapshot_consensus(
sort_db.conn(),
&anchor_block_header.consensus_hash,
)?
.expect("FATAL: no snapshot for winning PoX anchor block");

// make sure the `anchor_block` field is the same as whatever goes into the block-commit,
// or PoX ancestry queries won't work
let (block_id, stacks_block_hash) = match anchor_block_header.anchored_header {
StacksBlockHeaderTypes::Epoch2(header) => (
StacksBlockId::new(&anchor_block_header.consensus_hash, &header.block_hash()),
header.block_hash(),
),
StacksBlockHeaderTypes::Nakamoto(header) => {
(header.block_id(), BlockHeaderHash(header.block_id().0))
match StacksChainState::get_stacks_block_header_info_by_consensus_hash(
chain_state.db(),
&sn.consensus_hash,
){
Ok(Some(x)) => return Some(Ok(x)),
Err(e) => return Some(Err(e)),
Ok(None) => {
// no header for this snapshot (possibly invalid)
debug!("Failed to find block by consensus hash"; "consensus_hash" => %sn.consensus_hash);
return None
}
}
})
// if there was a chainstate error during the lookup, yield the error
.transpose()? else {
// no stacks block known yet
info!("No PoX anchor block known yet for cycle {reward_cycle}");
return Ok(None)
};

let txid = anchor_block_sn.winning_block_txid;

info!(
"Anchor block selected";
"cycle" => reward_cycle,
"block_id" => %block_id,
"consensus_hash" => %anchor_block_header.consensus_hash,
"burn_height" => anchor_block_header.burn_header_height,
"anchor_chain_tip" => %parent_block_header.index_block_hash(),
"anchor_chain_tip_height" => %parent_block_header.burn_header_height,
"first_prepare_sortition_id" => %first_sortition_id
);
let anchor_block_sn = SortitionDB::get_block_snapshot_consensus(
sort_db.conn(),
&anchor_block_header.consensus_hash,
)?
.expect("FATAL: no snapshot for winning PoX anchor block");

// make sure the `anchor_block` field is the same as whatever goes into the block-commit,
// or PoX ancestry queries won't work
let (block_id, stacks_block_hash) = match anchor_block_header.anchored_header {
StacksBlockHeaderTypes::Epoch2(ref header) => (
StacksBlockId::new(&anchor_block_header.consensus_hash, &header.block_hash()),
header.block_hash(),
),
StacksBlockHeaderTypes::Nakamoto(ref header) => {
(header.block_id(), BlockHeaderHash(header.block_id().0))
}
};

let reward_set = provider.get_reward_set(
reward_start_height,
chain_state,
burnchain,
sort_db,
&block_id,
)?;
debug!(
"Stacks anchor block (ch {}) {} cycle {} is processed",
&anchor_block_header.consensus_hash, &block_id, reward_cycle
);
let anchor_status =
PoxAnchorBlockStatus::SelectedAndKnown(stacks_block_hash, txid, reward_set);
let txid = anchor_block_sn.winning_block_txid;

info!(
"Anchor block selected";
"cycle" => reward_cycle,
"block_id" => %block_id,
"consensus_hash" => %anchor_block_header.consensus_hash,
"burn_height" => anchor_block_header.burn_header_height,
"anchor_chain_tip" => %anchor_block_header.index_block_hash(),
"anchor_chain_tip_height" => %anchor_block_header.burn_header_height,
"first_prepare_sortition_id" => %first_sortition_id
);

let rc_info = RewardCycleInfo {
reward_cycle,
anchor_status,
};
let reward_set = provider.get_reward_set(
reward_start_height,
chain_state,
burnchain,
sort_db,
&block_id,
)?;
debug!(
"Stacks anchor block (ch {}) {} cycle {} is processed",
&anchor_block_header.consensus_hash, &block_id, reward_cycle
);
let anchor_status = PoxAnchorBlockStatus::SelectedAndKnown(stacks_block_hash, txid, reward_set);

// persist this
let mut tx = sort_db.tx_begin()?;
SortitionDB::store_preprocessed_reward_set(&mut tx, &first_sortition_id, &rc_info)?;
tx.commit()?;
let rc_info = RewardCycleInfo {
reward_cycle,
anchor_status,
};

return Ok(Some(rc_info));
}
// persist this
let mut tx = sort_db.tx_begin()?;
SortitionDB::store_preprocessed_reward_set(&mut tx, &first_sortition_id, &rc_info)?;
tx.commit()?;

// no stacks block known yet
info!("No PoX anchor block known yet for cycle {}", reward_cycle);
return Ok(None);
return Ok(Some(rc_info));
}

/// Get the next PoX recipients in the Nakamoto epoch.
Expand Down Expand Up @@ -398,9 +374,6 @@ impl<
/// to ensure that the PoX stackers have been selected for this cycle. This means that we
/// don't proceed to process Nakamoto blocks until the reward cycle has begun. Also, the last
/// reward cycle of epoch2 _must_ be PoX so we have stackers who can sign.
///
/// TODO: how do signers register their initial keys? Can we just deploy a pre-registration
/// contract?
pub fn can_process_nakamoto(&mut self) -> Result<bool, Error> {
let canonical_sortition_tip = self
.canonical_sortition_tip
Expand Down
20 changes: 6 additions & 14 deletions stackslib/src/chainstate/nakamoto/coordinator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use crate::util_lib::boot::boot_code_id;
fn advance_to_nakamoto(
peer: &mut TestPeer,
test_signers: &TestSigners,
test_stackers: Vec<&TestStacker>,
test_stackers: Vec<TestStacker>,
) {
let mut peer_nonce = 0;
let private_key = peer.config.private_key.clone();
Expand Down Expand Up @@ -162,9 +162,10 @@ pub fn boot_nakamoto<'a>(
peer_config.burnchain.pox_constants.pox_3_activation_height = 26;
peer_config.burnchain.pox_constants.v3_unlock_height = 27;
peer_config.burnchain.pox_constants.pox_4_activation_height = 31;
peer_config.test_stackers = Some(test_stackers.clone());
let mut peer = TestPeer::new(peer_config);

advance_to_nakamoto(&mut peer, &test_signers, test_stackers.iter().collect());
advance_to_nakamoto(&mut peer, &test_signers, test_stackers);

peer
}
Expand All @@ -175,21 +176,12 @@ fn make_replay_peer<'a>(peer: &'a mut TestPeer<'a>) -> TestPeer<'a> {
replay_config.test_name = format!("{}.replay", &peer.config.test_name);
replay_config.server_port = 0;
replay_config.http_port = 0;
replay_config.test_stackers = peer.config.test_stackers.clone();

let private_key = peer.config.private_key.clone();
let signer_private_key = StacksPrivateKey::from_seed(&[3]);

let test_stackers = replay_config.test_stackers.clone().unwrap_or(vec![]);
let mut replay_peer = TestPeer::new(replay_config);
let observer = TestEventObserver::new();
advance_to_nakamoto(
&mut replay_peer,
&TestSigners::default(),
vec![&TestStacker {
stacker_private_key: private_key,
signer_private_key,
amount: 1_000_000_000_000_000_000,
}],
);
advance_to_nakamoto(&mut replay_peer, &TestSigners::default(), test_stackers);

// sanity check
let replay_tip = {
Expand Down
16 changes: 11 additions & 5 deletions stackslib/src/chainstate/nakamoto/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,17 @@ impl NakamotoBlockBuilder {
) -> Result<MinerTenureInfo<'a>, Error> {
debug!("Nakamoto miner tenure begin");

let burn_tip = SortitionDB::get_canonical_chain_tip_bhh(burn_dbconn.conn())?;
let burn_tip_height = u32::try_from(
SortitionDB::get_canonical_burn_chain_tip(burn_dbconn.conn())?.block_height,
)
.expect("block height overflow");
// must build off of the header's consensus hash as the burnchain view, not the canonical_tip_bhh:
let burn_sn = SortitionDB::get_block_snapshot_consensus(burn_dbconn.conn(), &self.header.consensus_hash)?
.ok_or_else(|| {
warn!(
"Could not mine. The expected burnchain consensus hash has not been processed by our SortitionDB";
"consensus_hash" => %self.header.consensus_hash
);
Error::NoSuchBlockError
})?;
let burn_tip = burn_sn.burn_header_hash;
let burn_tip_height = u32::try_from(burn_sn.block_height).expect("block height overflow");

let mainnet = chainstate.config().mainnet;

Expand Down
Loading

0 comments on commit 8056694

Please sign in to comment.