Merge pull request #4576 from stacks-network/feat/2.5-pox-updates

2.5: PoX missed-slot updates
This commit is contained in:
Aaron Blankstein
2024-03-21 19:18:00 +00:00
committed by GitHub
5 changed files with 436 additions and 73 deletions

View File

@@ -92,6 +92,16 @@ impl StacksEpochId {
StacksEpochId::Epoch24 | StacksEpochId::Epoch25 | StacksEpochId::Epoch30 => true,
}
}
/// Does this epoch support unlocking PoX contributors that miss a slot?
///
/// Epoch 2.0 - 2.05 didn't support this feature, but they weren't epoch-guarded on it. Instead,
/// the behavior never activates in those epochs because the Pox1 contract does not provide
/// `contibuted_stackers` information. This check maintains that exact semantics by returning
/// true for all epochs before 2.5. For 2.5 and after, this returns false.
pub fn supports_pox_missed_slot_unlocks(&self) -> bool {
self < &StacksEpochId::Epoch25
}
}
impl std::fmt::Display for StacksEpochId {

View File

@@ -432,7 +432,7 @@ impl StacksChainState {
cycle_number: u64,
cycle_info: Option<PoxStartCycleInfo>,
) -> Result<Vec<StacksTransactionEvent>, Error> {
Self::handle_pox_cycle_start(clarity, cycle_number, cycle_info, POX_2_NAME)
Self::handle_pox_cycle_missed_unlocks(clarity, cycle_number, cycle_info, &PoxVersions::Pox2)
}
/// Do all the necessary Clarity operations at the start of a PoX reward cycle.
@@ -444,7 +444,7 @@ impl StacksChainState {
cycle_number: u64,
cycle_info: Option<PoxStartCycleInfo>,
) -> Result<Vec<StacksTransactionEvent>, Error> {
Self::handle_pox_cycle_start(clarity, cycle_number, cycle_info, POX_3_NAME)
Self::handle_pox_cycle_missed_unlocks(clarity, cycle_number, cycle_info, &PoxVersions::Pox3)
}
/// Do all the necessary Clarity operations at the start of a PoX reward cycle.
@@ -452,29 +452,36 @@ impl StacksChainState {
///
/// This should only be called for PoX v4 cycles.
pub fn handle_pox_cycle_start_pox_4(
clarity: &mut ClarityTransactionConnection,
cycle_number: u64,
cycle_info: Option<PoxStartCycleInfo>,
_clarity: &mut ClarityTransactionConnection,
_cycle_number: u64,
_cycle_info: Option<PoxStartCycleInfo>,
) -> Result<Vec<StacksTransactionEvent>, Error> {
Self::handle_pox_cycle_start(clarity, cycle_number, cycle_info, POX_4_NAME)
// PASS
Ok(vec![])
}
/// Do all the necessary Clarity operations at the start of a PoX reward cycle.
/// Currently, this just means applying any auto-unlocks to Stackers who qualified.
///
fn handle_pox_cycle_start(
fn handle_pox_cycle_missed_unlocks(
clarity: &mut ClarityTransactionConnection,
cycle_number: u64,
cycle_info: Option<PoxStartCycleInfo>,
pox_contract_name: &str,
pox_contract_ver: &PoxVersions,
) -> Result<Vec<StacksTransactionEvent>, Error> {
clarity.with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number)))??;
if !matches!(pox_contract_ver, PoxVersions::Pox2 | PoxVersions::Pox3) {
return Err(Error::InvalidStacksBlock(format!(
"Attempted to invoke missed unlocks handling on invalid PoX version ({pox_contract_ver})"
)));
}
debug!(
"Handling PoX reward cycle start";
"reward_cycle" => cycle_number,
"cycle_active" => cycle_info.is_some(),
"pox_contract" => pox_contract_name
"pox_contract" => %pox_contract_ver,
);
let cycle_info = match cycle_info {
@@ -483,7 +490,8 @@ impl StacksChainState {
};
let sender_addr = PrincipalData::from(boot::boot_code_addr(clarity.is_mainnet()));
let pox_contract = boot::boot_code_id(pox_contract_name, clarity.is_mainnet());
let pox_contract =
boot::boot_code_id(pox_contract_ver.get_name_str(), clarity.is_mainnet());
let mut total_events = vec![];
for (principal, amount_locked) in cycle_info.missed_reward_slots.iter() {
@@ -509,7 +517,8 @@ impl StacksChainState {
}).expect("FATAL: failed to accelerate PoX unlock");
// query the stacking state for this user before deleting it
let user_data = Self::get_user_stacking_state(clarity, principal, pox_contract_name);
let user_data =
Self::get_user_stacking_state(clarity, principal, pox_contract_ver.get_name_str());
// perform the unlock
let (result, _, mut events, _) = clarity
@@ -814,12 +823,19 @@ impl StacksChainState {
// pointer set by the PoX contract, then add them to auto-unlock list
if slots_taken == 0 && !contributed_stackers.is_empty() {
info!(
"Stacker missed reward slot, added to unlock list";
// "stackers" => %VecDisplay(&contributed_stackers),
"{}",
if epoch_id.supports_pox_missed_slot_unlocks() {
"Stacker missed reward slot, added to unlock list"
} else {
"Stacker missed reward slot"
};
"reward_address" => %address.clone().to_b58(),
"threshold" => threshold,
"stacked_amount" => stacked_amt
);
if !epoch_id.supports_pox_missed_slot_unlocks() {
continue;
}
contributed_stackers
.sort_by_cached_key(|(stacker, ..)| to_hex(&stacker.serialize_to_vec()));
while let Some((contributor, amt)) = contributed_stackers.pop() {
@@ -839,6 +855,9 @@ impl StacksChainState {
}
}
}
if !epoch_id.supports_pox_missed_slot_unlocks() {
missed_slots.clear();
}
info!("Reward set calculated"; "slots_occuppied" => reward_set.len());
RewardSet {
rewarded_addresses: reward_set,

View File

@@ -356,63 +356,6 @@
(define-read-only (get-reward-set-pox-address (reward-cycle uint) (index uint))
(map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: index }))
(define-private (fold-unlock-reward-cycle (set-index uint)
(data-res (response { cycle: uint,
first-unlocked-cycle: uint,
stacker: principal
} int)))
(let ((data (try! data-res))
(cycle (get cycle data))
(first-unlocked-cycle (get first-unlocked-cycle data)))
;; if current-cycle hasn't reached first-unlocked-cycle, just continue to next iter
(asserts! (>= cycle first-unlocked-cycle) (ok (merge data { cycle: (+ u1 cycle) })))
(let ((cycle-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: cycle, index: set-index })))
(cycle-entry-u (get stacker cycle-entry))
(cycle-entry-total-ustx (get total-ustx cycle-entry))
(cycle-last-entry-ix (- (get len (unwrap-panic (map-get? reward-cycle-pox-address-list-len { reward-cycle: cycle }))) u1)))
(asserts! (is-eq cycle-entry-u (some (get stacker data))) (err ERR_STACKING_CORRUPTED_STATE))
(if (not (is-eq cycle-last-entry-ix set-index))
;; do a "move" if the entry to remove isn't last
(let ((move-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: cycle, index: cycle-last-entry-ix }))))
(map-set reward-cycle-pox-address-list
{ reward-cycle: cycle, index: set-index }
move-entry)
(match (get stacker move-entry) moved-stacker
;; if the moved entry had an associated stacker, update its state
(let ((moved-state (unwrap-panic (map-get? stacking-state { stacker: moved-stacker })))
;; calculate the index into the reward-set-indexes that `cycle` is at
(moved-cycle-index (- cycle (get first-reward-cycle moved-state)))
(moved-reward-list (get reward-set-indexes moved-state))
;; reward-set-indexes[moved-cycle-index] = set-index via slice?, append, concat.
(update-list (unwrap-panic (replace-at? moved-reward-list moved-cycle-index set-index))))
(map-set stacking-state { stacker: moved-stacker }
(merge moved-state { reward-set-indexes: update-list })))
;; otherwise, we don't need to update stacking-state after move
true))
;; if not moving, just noop
true)
;; in all cases, we now need to delete the last list entry
(map-delete reward-cycle-pox-address-list { reward-cycle: cycle, index: cycle-last-entry-ix })
(map-set reward-cycle-pox-address-list-len { reward-cycle: cycle } { len: cycle-last-entry-ix })
;; finally, update `reward-cycle-total-stacked`
(map-set reward-cycle-total-stacked { reward-cycle: cycle }
{ total-ustx: (- (get total-ustx (unwrap-panic (map-get? reward-cycle-total-stacked { reward-cycle: cycle })))
cycle-entry-total-ustx) })
(ok (merge data { cycle: (+ u1 cycle)} )))))
;; This method is called by the Stacks block processor directly in order to handle the contract state mutations
;; associated with an early unlock. This can only be invoked by the block processor: it is private, and no methods
;; from this contract invoke it.
(define-private (handle-unlock (user principal) (amount-locked uint) (cycle-to-unlock uint))
(let ((user-stacking-state (unwrap-panic (map-get? stacking-state { stacker: user })))
(first-cycle-locked (get first-reward-cycle user-stacking-state))
(reward-set-indexes (get reward-set-indexes user-stacking-state)))
;; iterate over each reward set the user is a member of, and remove them from the sets. only apply to reward sets after cycle-to-unlock.
(try! (fold fold-unlock-reward-cycle reward-set-indexes (ok { cycle: first-cycle-locked, first-unlocked-cycle: cycle-to-unlock, stacker: user })))
;; Now that we've cleaned up all the reward set entries for the user, delete the user's stacking-state
(map-delete stacking-state { stacker: user })
(ok true)))
;; Add a PoX address to the `cycle-index`-th reward cycle, if `cycle-index` is between 0 and the given num-cycles (exclusive).
;; Arguments are given as a tuple, so this function can be (folded ..)'ed onto a list of its arguments.
;; Used by add-pox-addr-to-reward-cycles.

View File

@@ -54,13 +54,13 @@ use crate::chainstate::burn::{BlockSnapshot, ConsensusHash};
use crate::chainstate::coordinator::tests::pox_addr_from;
use crate::chainstate::stacks::address::{PoxAddress, PoxAddressType20, PoxAddressType32};
use crate::chainstate::stacks::boot::pox_2_tests::{
check_pox_print_event, generate_pox_clarity_value, get_partial_stacked,
check_pox_print_event, generate_pox_clarity_value, get_partial_stacked, get_reward_cycle_total,
get_reward_set_entries_at, get_stacking_state_pox, get_stx_account_at, with_clarity_db_ro,
PoxPrintFields, StackingStateCheckData,
};
use crate::chainstate::stacks::boot::{
BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET, POX_2_NAME,
POX_3_NAME,
PoxVersions, BOOT_CODE_COST_VOTING_TESTNET as BOOT_CODE_COST_VOTING, BOOT_CODE_POX_TESTNET,
POX_2_NAME, POX_3_NAME,
};
use crate::chainstate::stacks::db::{
MinerPaymentSchedule, StacksChainState, StacksHeaderInfo, MINER_REWARD_MATURITY,
@@ -90,6 +90,48 @@ pub fn get_tip(sortdb: Option<&SortitionDB>) -> BlockSnapshot {
SortitionDB::get_canonical_burn_chain_tip(&sortdb.unwrap().conn()).unwrap()
}
fn make_simple_pox_4_lock(
key: &StacksPrivateKey,
peer: &mut TestPeer,
amount: u128,
lock_period: u128,
) -> StacksTransaction {
let addr = key_to_stacks_addr(key);
let pox_addr = PoxAddress::from_legacy(AddressHashMode::SerializeP2PKH, addr.bytes.clone());
let signer_pk = StacksPublicKey::from_private(&key);
let tip = get_tip(peer.sortdb.as_ref());
let next_reward_cycle = peer
.config
.burnchain
.block_height_to_reward_cycle(tip.block_height)
.unwrap();
let nonce = get_account(peer, &addr.into()).nonce;
let auth_id = u128::from(nonce);
let signature = make_signer_key_signature(
&pox_addr,
&key,
next_reward_cycle.into(),
&Pox4SignatureTopic::StackStx,
lock_period,
amount,
auth_id,
);
make_pox_4_lockup(
key,
nonce,
amount,
&pox_addr,
lock_period,
&signer_pk,
tip.block_height,
Some(signature),
amount,
auth_id,
)
}
pub fn make_test_epochs_pox() -> (Vec<StacksEpoch>, PoxConstants) {
let EMPTY_SORTITIONS = 25;
let EPOCH_2_1_HEIGHT = EMPTY_SORTITIONS + 11; // 36
@@ -5297,3 +5339,350 @@ pub fn get_last_block_sender_transactions(
})
.collect::<Vec<_>>()
}
/// In this test case, two Stackers, Alice and Bob stack in PoX 4. Alice stacks enough
/// to qualify for slots, but Bob does not. In PoX-2 and PoX-3, this would result
/// in an auto unlock, but PoX-4 it should not.
#[test]
fn missed_slots_no_unlock() {
let EXPECTED_FIRST_V2_CYCLE = 8;
// the sim environment produces 25 empty sortitions before
// tenures start being tracked.
let EMPTY_SORTITIONS = 25;
let (epochs, mut pox_constants) = make_test_epochs_pox();
pox_constants.pox_4_activation_height = u32::try_from(epochs[7].start_height).unwrap() + 1;
let mut burnchain = Burnchain::default_unittest(
0,
&BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(),
);
burnchain.pox_constants = pox_constants.clone();
let observer = TestEventObserver::new();
let (mut peer, mut keys) = instantiate_pox_peer_with_epoch(
&burnchain,
&function_name!(),
Some(epochs.clone()),
Some(&observer),
);
peer.config.check_pox_invariants = None;
let alice = keys.pop().unwrap();
let bob = keys.pop().unwrap();
let alice_address = key_to_stacks_addr(&alice);
let bob_address = key_to_stacks_addr(&bob);
let mut coinbase_nonce = 0;
let first_v4_cycle = burnchain
.block_height_to_reward_cycle(burnchain.pox_constants.pox_4_activation_height as u64)
.unwrap()
+ 1;
// produce blocks until epoch 2.5
while get_tip(peer.sortdb.as_ref()).block_height <= epochs[7].start_height {
peer.tenure_with_txs(&[], &mut coinbase_nonce);
}
// perform lockups so we can test that pox-4 does not exhibit unlock-on-miss behavior
let tip = get_tip(peer.sortdb.as_ref());
let alice_lockup =
make_simple_pox_4_lock(&alice, &mut peer, 1024 * POX_THRESHOLD_STEPS_USTX, 6);
let bob_lockup = make_simple_pox_4_lock(&bob, &mut peer, 1 * POX_THRESHOLD_STEPS_USTX, 6);
let txs = [alice_lockup, bob_lockup];
let mut latest_block = peer.tenure_with_txs(&txs, &mut coinbase_nonce);
// check that the "raw" reward set will contain entries for alice and bob
// for the pox-4 cycles
for cycle_number in first_v4_cycle..first_v4_cycle + 6 {
let cycle_start = burnchain.reward_cycle_to_block_height(cycle_number);
let reward_set_entries = get_reward_set_entries_at(&mut peer, &latest_block, cycle_start);
assert_eq!(
reward_set_entries.len(),
2,
"Reward set should contain two entries in cycle {cycle_number}"
);
assert_eq!(
reward_set_entries[0].reward_address.bytes(),
bob_address.bytes.0.to_vec()
);
assert_eq!(
reward_set_entries[1].reward_address.bytes(),
alice_address.bytes.0.to_vec()
);
}
// we'll produce blocks until the next reward cycle gets through the "handled start" code
// this is one block after the reward cycle starts
let height_target = burnchain.reward_cycle_to_block_height(first_v4_cycle) + 1;
let auto_unlock_coinbase = height_target - 1 - EMPTY_SORTITIONS;
// but first, check that bob has locked tokens at (height_target + 1)
let bob_bal = get_stx_account_at(
&mut peer,
&latest_block,
&bob_address.to_account_principal(),
);
assert_eq!(bob_bal.amount_locked(), POX_THRESHOLD_STEPS_USTX);
while get_tip(peer.sortdb.as_ref()).block_height < height_target {
latest_block = peer.tenure_with_txs(&[], &mut coinbase_nonce);
}
// check that the "raw" reward sets for all cycles contain entries for alice and bob still!
for cycle_number in first_v4_cycle..(first_v4_cycle + 6) {
let cycle_start = burnchain.reward_cycle_to_block_height(cycle_number);
let reward_set_entries = get_reward_set_entries_at(&mut peer, &latest_block, cycle_start);
assert_eq!(reward_set_entries.len(), 2);
assert_eq!(
reward_set_entries[0].reward_address.bytes(),
bob_address.bytes.0.to_vec()
);
assert_eq!(
reward_set_entries[1].reward_address.bytes(),
alice_address.bytes.0.to_vec()
);
}
let expected_unlock_height = burnchain.reward_cycle_to_block_height(first_v4_cycle + 6) - 1;
// now check that bob has an unlock height of `height_target`
let bob_bal = get_stx_account_at(
&mut peer,
&latest_block,
&bob_address.to_account_principal(),
);
assert_eq!(bob_bal.unlock_height(), expected_unlock_height);
assert_eq!(bob_bal.amount_locked(), POX_THRESHOLD_STEPS_USTX);
let alice_bal = get_stx_account_at(
&mut peer,
&latest_block,
&alice_address.to_account_principal(),
);
assert_eq!(alice_bal.unlock_height(), expected_unlock_height);
assert_eq!(alice_bal.amount_locked(), POX_THRESHOLD_STEPS_USTX * 1024);
// check that the total reward cycle amounts have not decremented
for cycle_number in first_v4_cycle..(first_v4_cycle + 6) {
assert_eq!(
get_reward_cycle_total(&mut peer, &latest_block, cycle_number),
1025 * POX_THRESHOLD_STEPS_USTX
);
}
// check that bob's stacking-state is gone and alice's stacking-state is correct
let bob_state = get_stacking_state_pox(
&mut peer,
&latest_block,
&bob_address.to_account_principal(),
PoxVersions::Pox4.get_name_str(),
)
.expect("Bob should have stacking-state entry")
.expect_tuple()
.unwrap();
let reward_indexes_str = bob_state.get("reward-set-indexes").unwrap().to_string();
assert_eq!(reward_indexes_str, "(u1 u1 u1 u1 u1 u1)");
let alice_state = get_stacking_state_pox(
&mut peer,
&latest_block,
&alice_address.to_account_principal(),
PoxVersions::Pox4.get_name_str(),
)
.expect("Alice should have stacking-state entry")
.expect_tuple()
.unwrap();
let reward_indexes_str = alice_state.get("reward-set-indexes").unwrap().to_string();
assert_eq!(reward_indexes_str, "(u0 u0 u0 u0 u0 u0)");
// check that bob is still locked at next block
latest_block = peer.tenure_with_txs(&[], &mut coinbase_nonce);
let bob_bal = get_stx_account_at(
&mut peer,
&latest_block,
&bob_address.to_account_principal(),
);
assert_eq!(bob_bal.unlock_height(), expected_unlock_height);
assert_eq!(bob_bal.amount_locked(), POX_THRESHOLD_STEPS_USTX);
// now let's check some tx receipts
let blocks = observer.get_blocks();
let mut alice_txs = HashMap::new();
let mut bob_txs = HashMap::new();
let mut coinbase_txs = vec![];
let mut reward_cycles_in_2_5 = 0u64;
for b in blocks.into_iter() {
if let Some(ref reward_set_data) = b.reward_set_data {
let signers_set = reward_set_data.reward_set.signers.as_ref().unwrap();
assert_eq!(signers_set.len(), 1);
assert_eq!(
StacksPublicKey::from_private(&alice).to_bytes_compressed(),
signers_set[0].signing_key.to_vec()
);
let rewarded_addrs = HashSet::<_>::from_iter(
reward_set_data
.reward_set
.rewarded_addresses
.iter()
.map(|a| a.to_burnchain_repr()),
);
assert_eq!(rewarded_addrs.len(), 1);
assert_eq!(
reward_set_data.reward_set.rewarded_addresses[0].bytes(),
alice_address.bytes.0.to_vec(),
);
reward_cycles_in_2_5 += 1;
eprintln!("{:?}", b.reward_set_data)
}
for (i, r) in b.receipts.into_iter().enumerate() {
if i == 0 {
coinbase_txs.push(r);
continue;
}
match r.transaction {
TransactionOrigin::Stacks(ref t) => {
let addr = t.auth.origin().address_testnet();
if addr == alice_address {
alice_txs.insert(t.auth.get_origin_nonce(), r);
} else if addr == bob_address {
bob_txs.insert(t.auth.get_origin_nonce(), r);
}
}
_ => {}
}
}
}
assert_eq!(alice_txs.len(), 1);
assert_eq!(bob_txs.len(), 1);
// only mined one 2.5 reward cycle, but make sure it was picked up in the events loop above
assert_eq!(reward_cycles_in_2_5, 1);
// all should have committedd okay
assert!(
match bob_txs.get(&0).unwrap().result {
Value::Response(ref r) => r.committed,
_ => false,
},
"Bob tx0 should have committed okay"
);
// Check that the event produced by "handle-unlock" has a well-formed print event
// and that this event is included as part of the coinbase tx
for unlock_coinbase_index in [auto_unlock_coinbase] {
// expect the unlock to occur 1 block after the handle-unlock method was invoked.
let expected_unlock_height = unlock_coinbase_index + EMPTY_SORTITIONS + 1;
let expected_cycle = pox_constants
.block_height_to_reward_cycle(0, expected_unlock_height)
.unwrap();
assert!(
coinbase_txs[unlock_coinbase_index as usize].events.is_empty(),
"handle-unlock events are coinbase events and there should be no handle-unlock invocation in this test"
);
}
}
/// In this test case, we lockup enough to get participation to be non-zero, but not enough to qualify for a reward slot.
#[test]
fn no_lockups_2_5() {
let EXPECTED_FIRST_V2_CYCLE = 8;
// the sim environment produces 25 empty sortitions before
// tenures start being tracked.
let EMPTY_SORTITIONS = 25;
let (epochs, mut pox_constants) = make_test_epochs_pox();
pox_constants.pox_4_activation_height = u32::try_from(epochs[7].start_height).unwrap() + 1;
let mut burnchain = Burnchain::default_unittest(
0,
&BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(),
);
burnchain.pox_constants = pox_constants.clone();
let observer = TestEventObserver::new();
let (mut peer, mut keys) = instantiate_pox_peer_with_epoch(
&burnchain,
&function_name!(),
Some(epochs.clone()),
Some(&observer),
);
peer.config.check_pox_invariants = None;
let alice = keys.pop().unwrap();
let bob = keys.pop().unwrap();
let alice_address = key_to_stacks_addr(&alice);
let bob_address = key_to_stacks_addr(&bob);
let mut coinbase_nonce = 0;
let first_v4_cycle = burnchain
.block_height_to_reward_cycle(burnchain.pox_constants.pox_4_activation_height as u64)
.unwrap()
+ 1;
// produce blocks until epoch 2.5
while get_tip(peer.sortdb.as_ref()).block_height <= epochs[7].start_height {
peer.tenure_with_txs(&[], &mut coinbase_nonce);
}
let tip = get_tip(peer.sortdb.as_ref());
let bob_lockup = make_simple_pox_4_lock(&bob, &mut peer, 1 * POX_THRESHOLD_STEPS_USTX, 6);
let txs = [bob_lockup];
let mut latest_block = peer.tenure_with_txs(&txs, &mut coinbase_nonce);
// check that the "raw" reward set will contain an entry for bob
for cycle_number in first_v4_cycle..first_v4_cycle + 6 {
let cycle_start = burnchain.reward_cycle_to_block_height(cycle_number);
let reward_set_entries = get_reward_set_entries_at(&mut peer, &latest_block, cycle_start);
assert_eq!(
reward_set_entries.len(),
1,
"Reward set should contain one entry in cycle {cycle_number}"
);
assert_eq!(
reward_set_entries[0].reward_address.bytes(),
bob_address.bytes.0.to_vec()
);
}
// we'll produce blocks until the next reward cycle gets through the "handled start" code
// this is one block after the reward cycle starts
let height_target = burnchain.reward_cycle_to_block_height(first_v4_cycle + 1) + 1;
let auto_unlock_coinbase = height_target - 1 - EMPTY_SORTITIONS;
// but first, check that bob has locked tokens at (height_target + 1)
let bob_bal = get_stx_account_at(
&mut peer,
&latest_block,
&bob_address.to_account_principal(),
);
assert_eq!(bob_bal.amount_locked(), POX_THRESHOLD_STEPS_USTX);
while get_tip(peer.sortdb.as_ref()).block_height < height_target {
latest_block = peer.tenure_with_txs(&[], &mut coinbase_nonce);
}
let blocks = observer.get_blocks();
for b in blocks.into_iter() {
if let Some(ref reward_set_data) = b.reward_set_data {
assert_eq!(reward_set_data.reward_set.signers, Some(vec![]));
assert!(reward_set_data.reward_set.rewarded_addresses.is_empty());
eprintln!("{:?}", b.reward_set_data)
}
}
}

View File

@@ -1867,6 +1867,7 @@ pub mod test {
pub winner_txid: Txid,
pub matured_rewards: Vec<MinerReward>,
pub matured_rewards_info: Option<MinerRewardInfo>,
pub reward_set_data: Option<RewardSetData>,
}
pub struct TestEventObserver {
@@ -1912,6 +1913,7 @@ pub mod test {
winner_txid,
matured_rewards: matured_rewards.to_owned(),
matured_rewards_info: matured_rewards_info.map(|info| info.clone()),
reward_set_data: reward_set_data.clone(),
})
}