mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-29 04:05:21 +08:00
Merge pull request #4576 from stacks-network/feat/2.5-pox-updates
2.5: PoX missed-slot updates
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user