From 811257e69b09de3d2e097f90550c47145ed671f5 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 11 Aug 2022 10:22:55 -0500 Subject: [PATCH 01/25] correlate non-delegated stack-stx lockups with stacker --- src/chainstate/stacks/boot/mod.rs | 279 ++++++++++++++++++++------ src/chainstate/stacks/boot/pox-2.clar | 138 +++++++------ 2 files changed, 299 insertions(+), 118 deletions(-) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 4e4239fd3..8876a7e8b 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -148,6 +148,12 @@ fn tuple_to_pox_addr(tuple_data: TupleData) -> (AddressHashMode, Hash160) { (version, hashbytes) } +pub struct RawRewardSetEntry { + pub reward_address: StacksAddress, + pub amount_stacked: u128, + pub stacker: Option, +} + impl StacksChainState { fn eval_boot_code_read_only( &mut self, @@ -275,17 +281,23 @@ impl StacksChainState { /// are summed. pub fn make_reward_set( threshold: u128, - mut addresses: Vec<(StacksAddress, u128)>, + mut addresses: Vec, ) -> Vec { let mut reward_set = vec![]; // the way that we sum addresses relies on sorting. - addresses.sort_by_key(|k| k.0.bytes.0); - while let Some((address, mut stacked_amt)) = addresses.pop() { + addresses.sort_by_key(|k| k.reward_address.bytes.0); + while let Some(RawRewardSetEntry { + reward_address: address, + amount_stacked: mut stacked_amt, + .. + }) = addresses.pop() + { // peak at the next address in the set, and see if we need to sum - while addresses.last().map(|x| &x.0) == Some(&address) { - let (_, additional_amt) = addresses + while addresses.last().map(|x| &x.reward_address) == Some(&address) { + let additional_amt = addresses .pop() - .expect("BUG: first() returned some, but pop() is none."); + .expect("BUG: first() returned some, but pop() is none.") + .amount_stacked; stacked_amt = stacked_amt .checked_add(additional_amt) .expect("CORRUPTION: Stacker stacked > u128 max amount"); @@ -328,12 +340,12 @@ impl StacksChainState { pub fn get_reward_threshold_and_participation( pox_settings: &PoxConstants, - addresses: &[(StacksAddress, u128)], + addresses: &[RawRewardSetEntry], liquid_ustx: u128, ) -> (u128, u128) { let participation = addresses .iter() - .fold(0, |agg, (_, stacked_amt)| agg + stacked_amt); + .fold(0, |agg, entry| agg + entry.amount_stacked); assert!( participation <= liquid_ustx, @@ -359,25 +371,13 @@ impl StacksChainState { (threshold, participation) } - /// Each address will have at least (get-stacking-minimum) tokens. - pub fn get_reward_addresses( + fn get_reward_addresses_pox_1( &mut self, - burnchain: &Burnchain, sortdb: &SortitionDB, - current_burn_height: u64, block_id: &StacksBlockId, - ) -> Result, Error> { - let reward_cycle = burnchain - .block_height_to_reward_cycle(current_burn_height) - .ok_or(Error::PoxNoRewardCycle)?; - - let reward_cycle_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); - - let pox_contract_name = burnchain - .pox_constants - .active_pox_contract(reward_cycle_start_height); - - if !self.is_pox_active(sortdb, block_id, reward_cycle as u128, pox_contract_name)? { + reward_cycle: u64, + ) -> Result, Error> { + if !self.is_pox_active(sortdb, block_id, reward_cycle as u128, POX_1_NAME)? { debug!( "PoX was voted disabled in block {} (reward cycle {})", block_id, reward_cycle @@ -385,14 +385,12 @@ impl StacksChainState { return Ok(vec![]); } - debug!("Using pox_contract = {}", pox_contract_name); - // how many in this cycle? let num_addrs = self .eval_boot_code_read_only( sortdb, block_id, - pox_contract_name, + POX_1_NAME, &format!("(get-reward-set-size u{})", reward_cycle), )? .expect_u128(); @@ -410,7 +408,7 @@ impl StacksChainState { .eval_boot_code_read_only( sortdb, block_id, - pox_contract_name, + POX_1_NAME, &format!("(get-reward-set-pox-address u{} u{})", reward_cycle, i), )? .expect_optional() @@ -439,16 +437,138 @@ impl StacksChainState { false => hash_mode.to_version_testnet(), }; + let reward_address = StacksAddress::new(version, hash); debug!( "PoX reward address (for {} ustx): {}", - total_ustx, - &StacksAddress::new(version, hash) + total_ustx, &reward_address, ); - ret.push((StacksAddress::new(version, hash), total_ustx)); + ret.push(RawRewardSetEntry { + reward_address, + amount_stacked: total_ustx, + stacker: None, + }) } Ok(ret) } + + fn get_reward_addresses_pox_2( + &mut self, + sortdb: &SortitionDB, + block_id: &StacksBlockId, + reward_cycle: u64, + ) -> Result, Error> { + if !self.is_pox_active(sortdb, block_id, reward_cycle as u128, POX_2_NAME)? { + debug!( + "PoX was voted disabled in block {} (reward cycle {})", + block_id, reward_cycle + ); + return Ok(vec![]); + } + + // how many in this cycle? + let num_addrs = self + .eval_boot_code_read_only( + sortdb, + block_id, + POX_2_NAME, + &format!("(get-reward-set-size u{})", reward_cycle), + )? + .expect_u128(); + + debug!( + "At block {:?} (reward cycle {}): {} PoX reward addresses", + block_id, reward_cycle, num_addrs + ); + + let mut ret = vec![]; + for i in 0..num_addrs { + // value should be (optional (tuple (pox-addr (tuple (...))) (total-ustx uint))). + // Get the tuple. + let tuple_data = self + .eval_boot_code_read_only( + sortdb, + block_id, + POX_2_NAME, + &format!("(get-reward-set-pox-address u{} u{})", reward_cycle, i), + )? + .expect_optional() + .expect(&format!( + "FATAL: missing PoX address in slot {} out of {} in reward cycle {}", + i, num_addrs, reward_cycle + )) + .expect_tuple(); + + let pox_addr_tuple = tuple_data + .get("pox-addr") + .expect(&format!("FATAL: no 'pox-addr' in return value from (get-reward-set-pox-address u{} u{})", reward_cycle, i)) + .to_owned() + .expect_tuple(); + + let (hash_mode, hash) = tuple_to_pox_addr(pox_addr_tuple); + + let total_ustx = tuple_data + .get("total-ustx") + .expect(&format!("FATAL: no 'total-ustx' in return value from (get-reward-set-pox-address u{} u{})", reward_cycle, i)) + .to_owned() + .expect_u128(); + + let stacker = tuple_data + .get("stacker") + .expect(&format!("FATAL: no 'total-ustx' in return value from (get-reward-set-pox-address u{} u{})", reward_cycle, i)) + .to_owned() + .expect_optional() + .map(|value| value.expect_principal()); + + let version = match self.mainnet { + true => hash_mode.to_version_mainnet(), + false => hash_mode.to_version_testnet(), + }; + + let reward_address = StacksAddress::new(version, hash); + debug!( + "PoX reward address (for {} ustx): {}", + total_ustx, &reward_address, + ); + ret.push(RawRewardSetEntry { + reward_address, + amount_stacked: total_ustx, + stacker, + }) + } + + Ok(ret) + } + + /// Each address will have at least (get-stacking-minimum) tokens. + pub fn get_reward_addresses( + &mut self, + burnchain: &Burnchain, + sortdb: &SortitionDB, + current_burn_height: u64, + block_id: &StacksBlockId, + ) -> Result, Error> { + let reward_cycle = burnchain + .block_height_to_reward_cycle(current_burn_height) + .ok_or(Error::PoxNoRewardCycle)?; + + let reward_cycle_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); + + let pox_contract_name = burnchain + .pox_constants + .active_pox_contract(reward_cycle_start_height); + + debug!("Using pox_contract = {}", pox_contract_name); + + match pox_contract_name { + x if x == POX_1_NAME => self.get_reward_addresses_pox_1(sortdb, block_id, reward_cycle), + x if x == POX_2_NAME => self.get_reward_addresses_pox_2(sortdb, block_id, reward_cycle), + unknown_contract => { + panic!("Blockchain implementation failure: PoX contract name '{}' is unknown. Chainstate is corrupted.", + unknown_contract); + } + } + } } #[cfg(test)] @@ -492,22 +612,38 @@ pub mod test { fn make_reward_set_units() { let threshold = 1_000; let addresses = vec![ - ( - StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), - 1500, - ), - ( - StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), - 500, - ), - ( - StacksAddress::from_string("STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0").unwrap(), - 1500, - ), - ( - StacksAddress::from_string("ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940").unwrap(), - 400, - ), + RawRewardSetEntry { + reward_address: StacksAddress::from_string( + "STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0", + ) + .unwrap(), + amount_stacked: 1500, + stacker: None, + }, + RawRewardSetEntry { + reward_address: StacksAddress::from_string( + "ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940", + ) + .unwrap(), + amount_stacked: 500, + stacker: None, + }, + RawRewardSetEntry { + reward_address: StacksAddress::from_string( + "STVK1K405H6SK9NKJAP32GHYHDJ98MMNP8Y6Z9N0", + ) + .unwrap(), + amount_stacked: 1500, + stacker: None, + }, + RawRewardSetEntry { + reward_address: StacksAddress::from_string( + "ST76D2FMXZ7D2719PNE4N71KPSX84XCCNCMYC940", + ) + .unwrap(), + amount_stacked: 400, + stacker: None, + }, ]; assert_eq!( StacksChainState::make_reward_set(threshold, addresses).len(), @@ -533,7 +669,11 @@ pub mod test { assert_eq!( StacksChainState::get_reward_threshold_and_participation( &test_pox_constants, - &[(rand_addr(), liquid)], + &[RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: liquid, + stacker: None + }], liquid ) .0, @@ -555,7 +695,11 @@ pub mod test { assert_eq!( StacksChainState::get_reward_threshold_and_participation( &test_pox_constants, - &[(rand_addr(), liquid / 4)], + &[RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: liquid / 4, + stacker: None + }], liquid ) .0, @@ -566,8 +710,16 @@ pub mod test { StacksChainState::get_reward_threshold_and_participation( &test_pox_constants, &[ - (rand_addr(), liquid / 4), - (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128)) + RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: liquid / 4, + stacker: None + }, + RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: 10_000_000 * (MICROSTACKS_PER_STACKS as u128), + stacker: None + }, ], liquid ) @@ -580,8 +732,16 @@ pub mod test { StacksChainState::get_reward_threshold_and_participation( &test_pox_constants, &[ - (rand_addr(), liquid / 4), - (rand_addr(), (MICROSTACKS_PER_STACKS as u128)) + RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: liquid / 4, + stacker: None + }, + RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: MICROSTACKS_PER_STACKS as u128, + stacker: None + }, ], liquid ) @@ -593,7 +753,11 @@ pub mod test { assert_eq!( StacksChainState::get_reward_threshold_and_participation( &test_pox_constants, - &[(rand_addr(), liquid)], + &[RawRewardSetEntry { + reward_address: rand_addr(), + amount_stacked: liquid, + stacker: None + }], liquid ) .0, @@ -1139,8 +1303,11 @@ pub mod test { state .get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) .and_then(|mut addrs| { - addrs.sort_by_key(|k| k.0.bytes.0); - Ok(addrs) + addrs.sort_by_key(|k| k.reward_address.bytes.0); + Ok(addrs + .into_iter() + .map(|x| (x.reward_address, x.amount_stacked)) + .collect()) }) } diff --git a/src/chainstate/stacks/boot/pox-2.clar b/src/chainstate/stacks/boot/pox-2.clar index c9a51706c..41121767b 100644 --- a/src/chainstate/stacks/boot/pox-2.clar +++ b/src/chainstate/stacks/boot/pox-2.clar @@ -73,7 +73,9 @@ ;; how long the uSTX are locked, in reward cycles. lock-period: uint, ;; reward cycle when rewards begin - first-reward-cycle: uint + first-reward-cycle: uint, + ;; indexes in each reward-set associated with this user + reward-set-indexes: (list 12 uint) } ) @@ -109,7 +111,8 @@ { reward-cycle: uint, index: uint } { pox-addr: { version: (buff 1), hashbytes: (buff 20) }, - total-ustx: uint + total-ustx: uint, + stacker: (optional principal) } ) @@ -234,20 +237,19 @@ ;; Add a single PoX address to a single reward cycle. ;; Used to build up a set of per-reward-cycle PoX addresses. ;; No checking will be done -- don't call if this PoX address is already registered in this reward cycle! +;; Returns the index into the reward cycle that the PoX address is stored to (define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) (reward-cycle uint) - (amount-ustx uint)) - (let ( - (sz (get-reward-set-size reward-cycle)) - ) - (map-set reward-cycle-pox-address-list - { reward-cycle: reward-cycle, index: sz } - { pox-addr: pox-addr, total-ustx: amount-ustx }) - (map-set reward-cycle-pox-address-list-len - { reward-cycle: reward-cycle } - { len: (+ u1 sz) }) - (+ u1 sz)) -) + (amount-ustx uint) + (stacker (optional principal))) + (let ((sz (get-reward-set-size reward-cycle))) + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: sz } + { pox-addr: pox-addr, total-ustx: amount-ustx, stacker: stacker }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + sz)) ;; How many uSTX are stacked? (define-read-only (get-total-ustx-stacked (reward-cycle uint)) @@ -269,34 +271,42 @@ ;; the pox-addr was added to the given cycle. (define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) + (reward-set-indexes (list 12 uint)) (first-reward-cycle uint) (num-cycles uint) + (stacker (optional principal)) (amount-ustx uint) (i uint)))) (let ((reward-cycle (+ (get first-reward-cycle params) (get i params))) (num-cycles (get num-cycles params)) - (i (get i params))) + (i (get i params)) + (reward-set-index (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle)) + (reward-index + ;; record how many uSTX this pox-addr will stack for in the given reward cycle + (append-reward-cycle-pox-addr + (get pox-addr params) + reward-cycle + (get amount-ustx params) + (get stacker params) + ))) + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + (some reward-index)) + none)) + (next-i (if (< i num-cycles) (+ i u1) (+ i u0)))) { pox-addr: (get pox-addr params), first-reward-cycle: (get first-reward-cycle params), num-cycles: num-cycles, amount-ustx: (get amount-ustx params), - i: (if (< i num-cycles) - (let ((total-ustx (get-total-ustx-stacked reward-cycle))) - ;; record how many uSTX this pox-addr will stack for in the given reward cycle - (append-reward-cycle-pox-addr - (get pox-addr params) - reward-cycle - (get amount-ustx params)) - - ;; update running total - (map-set reward-cycle-total-stacked - { reward-cycle: reward-cycle } - { total-ustx: (+ (get amount-ustx params) total-ustx) }) - - ;; updated _this_ reward cycle - (+ i u1)) - (+ i u0)) + stacker: (get stacker params), + reward-set-indexes: (match + reward-set-index new (unwrap-panic (as-max-len? (append (get reward-set-indexes params) new) u12)) + (get reward-set-indexes params)), + i: next-i })) ;; Add a PoX address to a given sequence of reward cycle lists. @@ -305,16 +315,17 @@ (define-private (add-pox-addr-to-reward-cycles (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))) (first-reward-cycle uint) (num-cycles uint) - (amount-ustx uint)) - (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + (amount-ustx uint) + (stacker principal)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11)) + (results (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, + reward-set-indexes: (list), amount-ustx: amount-ustx, i: u0, stacker: (some stacker) }))) ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. ;; It _should_ be equal to num-cycles. - (asserts! - (is-eq num-cycles - (get i (fold add-pox-addr-to-ith-reward-cycle cycle-indexes - { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx, i: u0 }))) - (err ERR_STACKING_UNREACHABLE)) - (ok true))) + (asserts! (is-eq num-cycles (get i results)) (err ERR_STACKING_UNREACHABLE)) + (asserts! (is-eq num-cycles (len (get reward-set-indexes results))) (err ERR_STACKING_UNREACHABLE)) + (ok (get reward-set-indexes results)))) (define-private (add-pox-partial-stacked-to-ith-cycle (cycle-index uint) @@ -479,19 +490,18 @@ (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) ;; register the PoX address with the amount stacked - (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx)) + (let ((reward-set-indexes (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx tx-sender)))) + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + reward-set-indexes: reward-set-indexes, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period }) - ;; add stacker record - (map-set stacking-state - { stacker: tx-sender } - { amount-ustx: amount-ustx, - pox-addr: pox-addr, - first-reward-cycle: first-reward-cycle, - lock-period: lock-period }) - - ;; return the lock-up information, so the node can actually carry out the lock. - (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })) -) + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })))) (define-public (revoke-delegate-stx) (begin @@ -557,6 +567,8 @@ { pox-addr: pox-addr, first-reward-cycle: reward-cycle, num-cycles: u1, + reward-set-indexes: (list), + stacker: none, amount-ustx: amount-ustx, i: u0 }) ;; don't update the stacking-state map, @@ -628,6 +640,7 @@ { amount-ustx: amount-ustx, pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, + reward-set-indexes: (list), lock-period: lock-period }) ;; return the lock-up information, so the node can actually carry out the lock. @@ -728,18 +741,18 @@ ;; register the PoX address with the amount stacked ;; for the new cycles - (try! (add-pox-addr-to-reward-cycles pox-addr first-extend-cycle extend-count amount-ustx)) + (let ((reward-set-indexes (try! (add-pox-addr-to-reward-cycles pox-addr first-extend-cycle extend-count amount-ustx tx-sender)))) + ;; update stacker record + (map-set stacking-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + pox-addr: pox-addr, + reward-set-indexes: reward-set-indexes, + first-reward-cycle: cur-cycle, + lock-period: lock-period }) - ;; update stacker record - (map-set stacking-state - { stacker: tx-sender } - { amount-ustx: amount-ustx, - pox-addr: pox-addr, - first-reward-cycle: first-extend-cycle, - lock-period: lock-period }) - - ;; return lock-up information - (ok { stacker: tx-sender, unlock-burn-height: new-unlock-ht })))) + ;; return lock-up information + (ok { stacker: tx-sender, unlock-burn-height: new-unlock-ht }))))) ;; As a delegator, extend an active stacking lock, issuing a "partial commitment" for the ;; extended-to cycles. @@ -817,6 +830,7 @@ { stacker: stacker } { amount-ustx: amount-ustx, pox-addr: pox-addr, + reward-set-indexes: (list), first-reward-cycle: first-extend-cycle, lock-period: lock-period }) From 59b5c4bd6b5484d6cb2c15756fef4d9d3a605495 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 16 Aug 2022 19:31:08 -0500 Subject: [PATCH 02/25] first pass at actually processing unlocks. fair amount of complexity here. --- clarity/src/vm/database/clarity_db.rs | 28 +++ clarity/src/vm/database/structures.rs | 33 +++ clarity/src/vm/types/mod.rs | 7 + src/chainstate/burn/db/sortdb.rs | 61 +++++- src/chainstate/stacks/boot/mod.rs | 284 ++++++++++++++++++++++++++ src/chainstate/stacks/db/blocks.rs | 30 ++- src/chainstate/stacks/miner.rs | 3 + src/clarity_vm/clarity.rs | 4 + src/clarity_vm/database/mod.rs | 69 +++++++ 9 files changed, 516 insertions(+), 3 deletions(-) diff --git a/clarity/src/vm/database/clarity_db.rs b/clarity/src/vm/database/clarity_db.rs index 04de43104..44a3e198d 100644 --- a/clarity/src/vm/database/clarity_db.rs +++ b/clarity/src/vm/database/clarity_db.rs @@ -1166,6 +1166,23 @@ impl<'a> ClarityDatabase<'a> { ) } + /// Like fetch_entry_unknown_descriptor, except that it expects + /// to receive a value (i.e., it expects the state to exist) + /// This should only ever be invoked outside of Clarity, e.g. by the VM + /// when loading state from a known-contract + pub fn expect_fetch_entry( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + map_name: &str, + key_value: &Value, + ) -> Result { + self.fetch_entry_unknown_descriptor(contract_identifier, map_name, key_value) + .map(|v| { + v.expect_optional() + .expect("Expected fetch_entry to return a value") + }) + } + pub fn fetch_entry_unknown_descriptor( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -1176,6 +1193,7 @@ impl<'a> ClarityDatabase<'a> { self.fetch_entry(contract_identifier, map_name, key_value, &descriptor) } + /// Returns a Clarity optional type wrapping a found or not found result pub fn fetch_entry( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -1360,6 +1378,16 @@ impl<'a> ClarityDatabase<'a> { }) } + pub fn delete_entry_unknown_descriptor( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + map_name: &str, + key_value: &Value, + ) -> Result { + let descriptor = self.load_map(contract_identifier, map_name)?; + self.delete_entry(contract_identifier, map_name, key_value, &descriptor) + } + pub fn delete_entry( &mut self, contract_identifier: &QualifiedContractIdentifier, diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index d8fafba1a..f1c8bc151 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -474,6 +474,39 @@ impl<'db, 'conn> STXBalanceSnapshot<'db, 'conn> { }; } + /// Lock `amount_to_lock` tokens on this account until `unlock_burn_height`. + /// After calling, this method will set the balance to a "LockedPoxTwo" balance, + /// because this method is only invoked as a result of PoX2 interactions + pub fn accelerate_unlock(&mut self) { + let unlocked = self.unlock_available_tokens_if_any(); + if unlocked > 0 { + debug!("Consolidated after account-token-lock"); + } + + let new_unlock_height = self.burn_block_height + 1; + self.balance = match self.balance { + STXBalance::Unlocked { amount } => STXBalance::Unlocked { amount }, + STXBalance::LockedPoxOne { + amount_unlocked, + amount_locked, + .. + } => STXBalance::LockedPoxOne { + amount_unlocked, + amount_locked, + unlock_height: new_unlock_height, + }, + STXBalance::LockedPoxTwo { + amount_unlocked, + amount_locked, + .. + } => STXBalance::LockedPoxTwo { + amount_unlocked, + amount_locked, + unlock_height: new_unlock_height, + }, + }; + } + /// Unlock any tokens that are unlockable at the current /// burn block height, and return the amount newly unlocked fn unlock_available_tokens_if_any(&mut self) -> u128 { diff --git a/clarity/src/vm/types/mod.rs b/clarity/src/vm/types/mod.rs index c7660ba5e..7bf4f870a 100644 --- a/clarity/src/vm/types/mod.rs +++ b/clarity/src/vm/types/mod.rs @@ -1339,6 +1339,13 @@ impl TupleData { self.data_map.len() as u64 } + /// This is like `from_data` for constructing a tuple, but is to be used in contexts + /// where the constructed tuple is *known* to be a valid Clarity value (e.g., static contexts) + /// This panics if the tuple is not a valid Clarity value. + pub fn from_data_static(data: Vec<(ClarityName, Value)>) -> TupleData { + Self::from_data(data).expect("FATAL: static Clarity tuple initialization failed") + } + pub fn from_data(mut data: Vec<(ClarityName, Value)>) -> Result { let mut type_map = BTreeMap::new(); let mut data_map = BTreeMap::new(); diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index f28593874..3f172458e 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -1506,7 +1506,6 @@ impl<'a> SortitionHandleConn<'a> { SortitionHandleConn::open_reader(connection, &sn.sortition_id) } - #[cfg(test)] pub fn get_last_anchor_block_hash(&self) -> Result, db_error> { let anchor_block_hash = SortitionDB::parse_last_anchor_block_hash( self.get_indexed(&self.context.chain_tip, &db_keys::pox_last_anchor())?, @@ -1546,6 +1545,66 @@ impl<'a> SortitionHandleConn<'a> { }) } + /// is the given block a descendant of `potential_ancestor`? + /// * block_at_burn_height: the burn height of the sortition that chose the stacks block to check + /// * potential_ancestor: the stacks block hash of the potential ancestor + pub fn descended_from( + &mut self, + block_at_burn_height: u64, + potential_ancestor: &BlockHeaderHash, + ) -> Result { + let earliest_block_height = self.conn().query_row( + "SELECT block_height FROM snapshots WHERE winning_stacks_block_hash = ? ORDER BY block_height ASC LIMIT 1", + &[potential_ancestor], + |row| Ok(u64::from_row(row).expect("Expected u64 in database")))?; + + let mut sn = self + .get_block_snapshot_by_height(block_at_burn_height)? + .ok_or_else(|| { + test_debug!("No snapshot at height {}", block_at_burn_height); + db_error::NotFoundError + })?; + + while sn.block_height >= earliest_block_height { + if !sn.sortition { + return Ok(false); + } + if &sn.winning_stacks_block_hash == potential_ancestor { + return Ok(true); + } + + // step back to the parent + match SortitionDB::get_block_commit_parent_sortition_id( + self.conn(), + &sn.winning_block_txid, + &sn.sortition_id, + )? { + Some(parent_sortition_id) => { + // we have the block_commit parent memoization data + test_debug!( + "Parent sortition of {} memoized as {}", + &sn.winning_block_txid, + &parent_sortition_id + ); + sn = SortitionDB::get_block_snapshot(self.conn(), &parent_sortition_id)? + .ok_or_else(|| db_error::NotFoundError)?; + } + None => { + // we do not have the block_commit parent memoization data + // step back to the parent + test_debug!("No parent sortition memo for {}", &sn.winning_block_txid); + let block_commit = + get_block_commit_by_txid(&self.conn(), &sn.winning_block_txid)? + .expect("CORRUPTION: winning block commit for snapshot not found"); + sn = self + .get_block_snapshot_by_height(block_commit.parent_block_ptr as u64)? + .ok_or_else(|| db_error::NotFoundError)?; + } + } + } + return Ok(false); + } + fn get_tip_indexed(&self, key: &str) -> Result, db_error> { self.get_indexed(&self.context.chain_tip, key) } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 8876a7e8b..58187ce8a 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -27,13 +27,17 @@ use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::index::marf::MarfConnection; use crate::chainstate::stacks::Error; use crate::clarity_vm::clarity::ClarityConnection; +use crate::clarity_vm::clarity::ClarityTransactionConnection; +use crate::clarity_vm::database::PoxStartCycleInfo; use crate::core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX}; +use clarity::types::chainstate::BlockHeaderHash; use clarity::vm::contexts::ContractContext; use clarity::vm::costs::{ cost_functions::ClarityCostFunction, ClarityCostFunctionReference, CostStateSummary, }; use clarity::vm::database::ClarityDatabase; use clarity::vm::database::{NULL_BURN_STATE_DB, NULL_HEADER_DB}; +use clarity::vm::errors::InterpreterError; use clarity::vm::representations::ClarityName; use clarity::vm::representations::ContractName; use clarity::vm::types::{ @@ -50,6 +54,7 @@ use crate::types::chainstate::StacksAddress; use crate::types::chainstate::StacksBlockId; use crate::util_lib::boot; use crate::vm::{costs::LimitedCostTracker, SymbolicExpression}; +use clarity::vm::clarity::Error as ClarityError; use clarity::vm::ClarityVersion; const BOOT_CODE_POX_BODY: &'static str = std::include_str!("pox.clar"); @@ -154,7 +159,286 @@ pub struct RawRewardSetEntry { pub stacker: Option, } +const POX_CYCLE_START_HANDLED_VALUE: &'static str = "1"; + impl StacksChainState { + /// Return the MARF key used to store whether or not a given PoX + /// cycle's "start" has been handled by the Stacks fork yet. This + /// is used in Stacks 2.1 to help process unlocks. + fn handled_pox_cycle_start_key(cycle_number: u64) -> String { + format!("chainstate_pox::handled_cycle_start::{}", cycle_number) + } + + /// Returns whether or not the `cycle_number` PoX cycle has been handled by the + /// Stacks fork in the opened `clarity_db`. + pub fn handled_pox_cycle_start(clarity_db: &mut ClarityDatabase, cycle_number: u64) -> bool { + let db_key = Self::handled_pox_cycle_start_key(cycle_number); + match clarity_db.get::(&db_key) { + Some(x) => x == POX_CYCLE_START_HANDLED_VALUE, + None => false, + } + } + + fn mark_pox_cycle_handled(db: &mut ClarityDatabase, cycle_number: u64) { + let db_key = Self::handled_pox_cycle_start_key(cycle_number); + db.put(&db_key, &POX_CYCLE_START_HANDLED_VALUE.to_string()); + } + + pub fn handle_pox_cycle_start( + clarity: &mut ClarityTransactionConnection, + cycle_number: u64, + cycle_info: Option, + ) -> Result<(), Error> { + let cycle_info = match cycle_info { + Some(x) => x, + None => { + return clarity + .with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number))) + .map_err(Error::from) + } + }; + + let pox_contract = boot::boot_code_id(POX_2_NAME, clarity.is_mainnet()); + + for (principal, amount_locked) in cycle_info.missed_reward_slots.iter() { + // we have to do several things for each principal + // 1. lookup their Stacks account and accelerate their unlock + // 2. remove the user's entries from every `reward-cycle-pox-address-list` they were in + // (a) this can be done by moving the last entry to the now vacated spot, + // and, if necessary, updating the associated `stacking-state` entry's pointer + // (b) or, if they were the only entry in the list, then just deleting them from the list + // 3. correct the `reward-cycle-total-stacked` entry for every reward cycle they were in + // 4. delete the user's stacking-state entry. + clarity.with_clarity_db(|db| { + // lookup the Stacks account and alter their unlock height to next block + let mut balance = db.get_stx_balance_snapshot(&principal); + if balance.canonical_balance_repr().amount_locked() < *amount_locked { + panic!("Principal missed reward slots, but did not have as many locked tokens as expected"); + } + + balance.accelerate_unlock(); + balance.save(); + + // get the user's stacking-state entry so that we can update the reward-cycle entries + let lookup_tuple = Value::Tuple(TupleData::from_data_static(vec![("stacker".into(), principal.clone().into())])); + let stacking_state_entry = db + .expect_fetch_entry( + &pox_contract, + "stacking-state", + &lookup_tuple + )? + .expect_tuple(); + + let first_reward_cycle_locked = stacking_state_entry + .get("first-reward-cycle") + .expect("Malformed return tuple from stacking-state") + .clone() + .expect_u128(); + if (cycle_number as u128) < first_reward_cycle_locked { + panic!("Unlocking for a cycle before this stacker has stacked"); + } + + let skip_reward_cycle_resets = (cycle_number as u128) - first_reward_cycle_locked; + + let reward_set_indexes = stacking_state_entry.get("reward-set-indexes") + .expect("Malformed return tuple from stacking-state") + .clone() + .expect_list(); + for (reward_set_offset, reward_set_index) in reward_set_indexes.into_iter().enumerate() { + if (reward_set_offset as u128) < skip_reward_cycle_resets { + continue + } + // zero out `reward-cycle-pox-address-list` entries and update `reward-cycle-total-stacked` + let reward_cycle_to_update = (reward_set_offset as u128) + (cycle_number as u128); + // this is the index of the entry we want to remove from the list + let target_entry_index = reward_set_index.expect_u128(); + + let target_key: Value = TupleData::from_data_static(vec![ + ("reward-cycle".into(), Value::UInt(reward_cycle_to_update)), + ("index".into(), Value::UInt(target_entry_index)) + ]).into(); + + let reward_cycle_entry = db.expect_fetch_entry( + &pox_contract, + "reward-cycle-pox-address-list", + &target_key + )?.expect_tuple(); + + let reward_cycle_entry_principal = reward_cycle_entry + .get("stacker") + .expect("Malformed tuple returned by PoX contract") + .clone() + .expect_optional() + .expect("Reward set entry for auto-unlock should have associated stacker") + .expect_principal(); + + assert_eq!(&reward_cycle_entry_principal, principal); + + let reward_cycle_entry_total_ustx = reward_cycle_entry + .get("total-ustx") + .expect("Malformed tuple returned by PoX contract") + .clone() + .expect_u128(); + + // compress the list: + // (a) move the last entry in `reward-cycle-pox-address-list` to this index + let reward_cycle_len_key = TupleData::from_data_static(vec![("reward-cycle".into(), Value::UInt(reward_cycle_to_update))]).into(); + let last_cycle_entry_index = db + .expect_fetch_entry( + &pox_contract, + "reward-cycle-pox-address-list-len", + &reward_cycle_len_key, + )? + .expect_tuple() + .get("len") + .expect("Malformed tuple returned by PoX contract") + .clone() + .expect_u128() + .checked_sub(1) + .expect("Reward set size was 0 even though we unlocked an existing entry"); + + let move_reward_cycle_map_key: Value = TupleData::from_data_static(vec![ + ("reward-cycle".into(), Value::UInt(reward_cycle_to_update)), + ("index".into(), Value::UInt(last_cycle_entry_index)) + ]).into(); + // only need to move if the entry we want to remove is not the last entry + if last_cycle_entry_index != target_entry_index { + let move_reward_cycle_entry = db.expect_fetch_entry( + &pox_contract, + "reward-cycle-pox-address-list", + &move_reward_cycle_map_key + )?.expect_tuple(); + + let stacker = move_reward_cycle_entry + .get("stacker") + .expect("Malformed tuple return by PoX contract") + .clone() + .expect_optional(); + + // overwrite the targeted entry with the last entry in the list + db.set_entry_unknown_descriptor( + &pox_contract, + "reward-cycle-pox-address-list", + target_key, + move_reward_cycle_entry.into() + )?; + + // if the last entry in `reward-cycle-pox-address-list` had an associated stacker, + // we must also update that stacker's `stacking-state` + if let Some(stacker_val) = stacker { + let moved_stacker = stacker_val.expect_principal(); + // load the `stacking-state` + let moved_state_key = TupleData::from_data_static(vec![("stacker".into(), moved_stacker.clone().into())]).into(); + let mut moved_state_entry = db + .expect_fetch_entry( + &pox_contract, + "stacking-state", + &moved_state_key, + )? + .expect_tuple(); + // calculate the index into the reward-set-indexes list that + // this reward cycle is at + let moved_cycle_index: usize = reward_cycle_to_update + .checked_sub( + moved_state_entry + .get("first-reward-cycle") + .expect("Malformed tuple return by PoX contract") + .clone() + .expect_u128() + ) + .expect("FATAL: Moved a reward set entry for a stacker whose first-reward-cycle was after the current cycle") + .try_into() + .expect("FATAL: list size is greater than usize"); + + // update the list of reward set indexes + let mut moved_reward_indexes = moved_state_entry + .get("reward-set-indexes") + .expect("Malformed tuple return by PoX contract") + .clone() + .expect_list(); + assert!(moved_reward_indexes.len() > moved_cycle_index, "FATAL: Calculated bad move index"); + + moved_reward_indexes[moved_cycle_index] = Value::UInt(last_cycle_entry_index); + + moved_state_entry.data_map.insert("reward-set-indexes".into(), + Value::list_from(moved_reward_indexes) + .expect("Failed to reconstruct Clarity list")); + // store the new state back into the stacking-state map + db.set_entry_unknown_descriptor( + &pox_contract, + "stacking-state", + moved_state_key, + moved_state_entry.into() + )?; + } + } + + // always delete the last entry and decrement the list length + + db.delete_entry_unknown_descriptor( + &pox_contract, + "reward-cycle-pox-address-list", + &move_reward_cycle_map_key, + )?; + + db.set_entry_unknown_descriptor( + &pox_contract, "reward-cycle-pox-address-list-len", reward_cycle_len_key, + TupleData::from_data_static(vec![("len".into(), Value::UInt(last_cycle_entry_index))]).into() + )?; + + // Finally, update `reward-cycle-total-stacked` + let total_stacked_key = TupleData::from_data_static(vec![("reward-cycle".into(), Value::UInt(reward_cycle_to_update))]) + .into(); + let next_total_stacked_amount = db + .expect_fetch_entry( + &pox_contract, + "reward-cycle-pox-address-list", + &total_stacked_key, + )? + .expect_tuple() + .get_owned("total-ustx") + .expect("Malformed tuple returned by PoX contract") + .expect_u128() + .checked_sub(reward_cycle_entry_total_ustx) + .expect("FATAL: Unlocked more STX in a cycle than were stacked in that cycle"); + db.set_entry_unknown_descriptor( + &pox_contract, + "reward-cycle-pox-address-list", + total_stacked_key, + TupleData::from_data_static(vec![("total-ustx".into(), Value::UInt(next_total_stacked_amount))]).into(), + )?; + } + + // Now that we've cleaned up all the reward set entries for the user, lets delete the user's stacking-state + db.delete_entry_unknown_descriptor( + &pox_contract, + "stacking-state", + &lookup_tuple + )?; + + Ok(()) + })?; + } + + Ok(()) + } + + /// After Stacks 2.1, invoke to process any Stacks chainstate operations required + /// at the start of a reward cycle (i.e., qualifying unlocks) + pub fn process_pox_cycle_start_2_1(last_anchor_block: Option) { + // Step 1: determine if a PoX anchor block was chosen for this cycle + // *and* that this Stacks block descends from that block + match last_anchor_block { + Some(_) => {} + None => { + debug!( + "No anchor block chosen in this PoX cycle, so no need to process cycle start" + ); + return; + } + } + } + fn eval_boot_code_read_only( &mut self, sortdb: &SortitionDB, diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 0327dc94a..3f72536b6 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -45,6 +45,7 @@ use crate::chainstate::stacks::{ C32_ADDRESS_VERSION_TESTNET_MULTISIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG, }; use crate::clarity_vm::clarity::{ClarityBlockConnection, ClarityConnection, ClarityInstance}; +use crate::clarity_vm::database::SortitionDBRef; use crate::codec::MAX_MESSAGE_LEN; use crate::codec::{read_next, write_next}; use crate::core::mempool::MemPoolDB; @@ -5064,6 +5065,7 @@ impl StacksChainState { chainstate_tx: &'b mut ChainstateTx, clarity_instance: &'a mut ClarityInstance, burn_dbconn: &'b dyn BurnStateDB, + sortition_dbconn: &'b dyn SortitionDBRef, conn: &Connection, // connection to the sortition DB chain_tip: &StacksHeaderInfo, burn_tip: BurnchainHeaderHash, @@ -5074,8 +5076,10 @@ impl StacksChainState { mainnet: bool, miner_id_opt: Option, ) -> Result, Error> { - let parent_index_hash = - StacksBlockHeader::make_index_block_hash(&parent_consensus_hash, &parent_header_hash); + let parent_index_hash = StacksBlockId::new(&parent_consensus_hash, &parent_header_hash); + let parent_sortition_id = burn_dbconn + .get_sortition_id_from_consensus_hash(&parent_consensus_hash) + .expect("Failed to get parent SortitionID from ConsensusHash"); // find matured miner rewards, so we can grant them within the Clarity DB tx. let (latest_matured_miners, matured_miner_parent) = { @@ -5137,6 +5141,27 @@ impl StacksChainState { let evaluated_epoch = clarity_tx.get_epoch(); clarity_tx.reset_cost(parent_block_cost.clone()); + if evaluated_epoch >= StacksEpochId::Epoch21 { + let pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( + burn_tip_height.into(), + burn_dbconn.get_burn_start_height().into(), + burn_dbconn.get_pox_reward_cycle_length().into(), + ).expect("FATAL: Unrecoverable chainstate corruption: Epoch 2.1 code evaluated before first burn block height"); + let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { + Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) + }); + if !handled { + let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( + &parent_sortition_id, + chain_tip.burn_header_height.into(), + pox_reward_cycle, + )?; + clarity_tx.block.as_transaction(|clarity_tx| { + Self::handle_pox_cycle_start(clarity_tx, pox_reward_cycle, pox_start_cycle_info) + })?; + } + } + let matured_miner_rewards_opt = match StacksChainState::find_mature_miner_rewards( &mut clarity_tx, conn, @@ -5428,6 +5453,7 @@ impl StacksChainState { chainstate_tx, clarity_instance, burn_dbconn, + burn_dbconn, &burn_dbconn.tx(), &parent_chain_tip, parent_burn_hash, diff --git a/src/chainstate/stacks/miner.rs b/src/chainstate/stacks/miner.rs index a10dc0ec7..ac6f7dff3 100644 --- a/src/chainstate/stacks/miner.rs +++ b/src/chainstate/stacks/miner.rs @@ -118,6 +118,8 @@ pub struct MinerEpochInfo<'a> { pub chainstate_tx: ChainstateTx<'a>, pub clarity_instance: &'a mut ClarityInstance, pub burn_tip: BurnchainHeaderHash, + /// This is the expected burn tip height (i.e., the current burnchain tip + 1) + /// of the mined block pub burn_tip_height: u32, pub parent_microblocks: Vec, pub mainnet: bool, @@ -1881,6 +1883,7 @@ impl StacksBlockBuilder { &mut info.chainstate_tx, info.clarity_instance, burn_dbconn, + burn_dbconn, burn_dbconn.conn(), &self.chain_tip, info.burn_tip, diff --git a/src/clarity_vm/clarity.rs b/src/clarity_vm/clarity.rs index 09fe43aa6..1236c478c 100644 --- a/src/clarity_vm/clarity.rs +++ b/src/clarity_vm/clarity.rs @@ -1148,6 +1148,10 @@ impl<'a, 'b> ClarityTransactionConnection<'a, 'b> { .and_then(|(value, ..)| Ok(value)) } + pub fn is_mainnet(&self) -> bool { + return self.mainnet; + } + /// Commit the changes from the edit log. /// panics if there is more than one open savepoint pub fn commit(mut self) { diff --git a/src/clarity_vm/database/mod.rs b/src/clarity_vm/database/mod.rs index 268d2e5d0..3803d63d0 100644 --- a/src/clarity_vm/database/mod.rs +++ b/src/clarity_vm/database/mod.rs @@ -1,3 +1,4 @@ +use clarity::vm::types::PrincipalData; use rusqlite::{Connection, OptionalExtension}; use crate::chainstate::burn::db::sortdb::{ @@ -18,6 +19,7 @@ use clarity::vm::errors::{InterpreterResult, RuntimeErrorType}; use crate::chainstate::stacks::db::ChainstateTx; use crate::chainstate::stacks::index::marf::{MarfConnection, MARF}; use crate::chainstate::stacks::index::{ClarityMarfTrieId, TrieMerkleProof}; +use crate::chainstate::stacks::Error as ChainstateError; use crate::types::chainstate::StacksBlockId; use crate::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, SortitionId}; use crate::types::chainstate::{StacksAddress, VRFSeed}; @@ -222,6 +224,73 @@ fn get_matured_reward(conn: &DBConn, child_id_bhh: &StacksBlockId) -> Option Result, ChainstateError>; +} + +pub struct PoxStartCycleInfo { + pub missed_reward_slots: Vec<(PrincipalData, u128)>, +} + +impl SortitionDBRef for SortitionHandleTx<'_> { + fn get_pox_start_cycle_info( + &self, + sortition_id: &SortitionId, + parent_stacks_block_burn_ht: u64, + cycle_index: u64, + ) -> Result, ChainstateError> { + let readonly_marf = self + .index() + .reopen_readonly() + .expect("BUG: failure trying to get a read-only interface into the sortition db."); + let mut context = self.context.clone(); + context.chain_tip = sortition_id.clone(); + let mut handle = SortitionHandleConn::new(&readonly_marf, context); + + let descended_from_last_pox_anchor = match handle.get_last_anchor_block_hash()? { + Some(pox_anchor) => handle.descended_from(parent_stacks_block_burn_ht, &pox_anchor)?, + None => return Ok(None), + }; + + if !descended_from_last_pox_anchor { + return Ok(None); + } + + Ok(Some(PoxStartCycleInfo { + missed_reward_slots: vec![], + })) + } +} + +impl SortitionDBRef for SortitionDBConn<'_> { + fn get_pox_start_cycle_info( + &self, + sortition_id: &SortitionId, + parent_stacks_block_burn_ht: u64, + cycle_index: u64, + ) -> Result, ChainstateError> { + let mut handle = self.as_handle(sortition_id); + + let descended_from_last_pox_anchor = match handle.get_last_anchor_block_hash()? { + Some(pox_anchor) => handle.descended_from(parent_stacks_block_burn_ht, &pox_anchor)?, + None => return Ok(None), + }; + + Ok(Some(PoxStartCycleInfo { + missed_reward_slots: vec![], + })) + } +} + impl BurnStateDB for SortitionHandleTx<'_> { fn get_burn_block_height(&self, sortition_id: &SortitionId) -> Option { match SortitionDB::get_block_snapshot(self.tx(), sortition_id) { From 7bc7bccdd5ea7b9f681f6d0da2b2d6fe8fef9a63 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 17 Aug 2022 16:58:55 -0500 Subject: [PATCH 03/25] implemented auto-unlock, now for testing... --- src/chainstate/burn/db/sortdb.rs | 47 ++++++++++++++--- src/chainstate/coordinator/mod.rs | 14 ++--- src/chainstate/stacks/boot/mod.rs | 85 +++++++++++++++++++++++++++---- src/clarity_vm/database/mod.rs | 42 +++++++-------- 4 files changed, 142 insertions(+), 46 deletions(-) diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 3f172458e..5a9231db3 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -50,6 +50,7 @@ use crate::chainstate::burn::{BlockSnapshot, ConsensusHash, OpsHash, SortitionHa use crate::chainstate::coordinator::{ Error as CoordinatorError, PoxAnchorBlockStatus, RewardCycleInfo, }; +use crate::chainstate::stacks::boot::PoxStartCycleInfo; use crate::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo}; use crate::chainstate::stacks::index::marf::MARFOpenOpts; use crate::chainstate::stacks::index::marf::MarfConnection; @@ -748,6 +749,10 @@ impl db_keys { "sortition_db::last_anchor_block" } + pub fn pox_reward_cycle_unlocks(cycle: u64) -> String { + format!("sortition_db::reward_set_unlocks::{}", cycle) + } + pub fn pox_reward_set_size() -> &'static str { "sortition_db::reward_set::size" } @@ -1182,9 +1187,9 @@ impl<'a> SortitionHandleTx<'a> { test_debug!( "Pick recipients for anchor block {} -- {} reward recipient(s)", anchor_block, - reward_set.len() + reward_set.rewarded_addresses.len() ); - if reward_set.len() == 0 { + if reward_set.rewarded_addresses.len() == 0 { return Ok(None); } @@ -1194,6 +1199,7 @@ impl<'a> SortitionHandleTx<'a> { let chosen_recipients = reward_set_vrf_seed.choose_two( reward_set + .rewarded_addresses .len() .try_into() .expect("BUG: u32 overflow in PoX outputs per commit"), @@ -1204,7 +1210,7 @@ impl<'a> SortitionHandleTx<'a> { recipients: chosen_recipients .into_iter() .map(|ix| { - let recipient = reward_set[ix as usize].clone(); + let recipient = reward_set.rewarded_addresses[ix as usize].clone(); info!("PoX recipient chosen"; "recipient" => recipient.clone().to_b58(), "block_height" => block_height); @@ -1513,6 +1519,19 @@ impl<'a> SortitionHandleConn<'a> { Ok(anchor_block_hash) } + pub fn get_reward_cycle_unlocks( + &mut self, + cycle: u64, + ) -> Result, db_error> { + let start_info = self + .get_tip_indexed(&db_keys::pox_reward_cycle_unlocks(cycle))? + .map(|x| { + PoxStartCycleInfo::deserialize(&x) + .expect("CORRUPTION: Failed to deserialize PoxStartCycleInfo from database") + }); + Ok(start_info) + } + fn get_reward_set_size(&self) -> Result { self.get_tip_indexed(&db_keys::pox_reward_set_size()) .map(|x| { @@ -4366,7 +4385,7 @@ impl<'a> SortitionHandleTx<'a> { // if we've selected an anchor _and_ know of the anchor, // write the reward set information if let Some(mut reward_set) = reward_info.known_selected_anchor_block_owned() { - if reward_set.len() > 0 { + if reward_set.rewarded_addresses.len() > 0 { // if we have a reward set, then we must also have produced a recipient // info for this block let mut recipients_to_remove: Vec<_> = recipient_info @@ -4378,17 +4397,31 @@ impl<'a> SortitionHandleTx<'a> { recipients_to_remove.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)); // remove from the reward set any consumed addresses in this first reward block for (addr, ix) in recipients_to_remove.iter() { - assert_eq!(&reward_set.remove(*ix as usize), addr, + assert_eq!(&reward_set.rewarded_addresses.remove(*ix as usize), addr, "BUG: Attempted to remove used address from reward set, but failed to do so safely"); } } keys.push(db_keys::pox_reward_set_size().to_string()); - values.push(db_keys::reward_set_size_to_string(reward_set.len())); - for (ix, address) in reward_set.iter().enumerate() { + values.push(db_keys::reward_set_size_to_string( + reward_set.rewarded_addresses.len(), + )); + for (ix, address) in reward_set.rewarded_addresses.iter().enumerate() { keys.push(db_keys::pox_reward_set_entry(ix as u16)); values.push(address.to_string()); } + // if there are qualifying auto-unlocks, record them + if !reward_set.start_cycle_state.is_empty() { + let cycle_number = Burnchain::static_block_height_to_reward_cycle( + snapshot.block_height, + self.context.first_block_height, + self.context.pox_constants.reward_cycle_length.into(), + ) + .expect("FATAL: PoX reward cycle started before first block height"); + + keys.push(db_keys::pox_reward_cycle_unlocks(cycle_number)); + values.push(reward_set.start_cycle_state.serialize()); + } } else { keys.push(db_keys::pox_reward_set_size().to_string()); values.push(db_keys::reward_set_size_to_string(0)); diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index cb952ec3b..0abd1aeda 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -63,6 +63,8 @@ use crate::chainstate::stacks::index::marf::MARFOpenOpts; pub use self::comm::CoordinatorCommunication; +use super::stacks::boot::RewardSet; + pub mod comm; #[cfg(test)] pub mod tests; @@ -71,7 +73,7 @@ pub mod tests; /// reward cycle's relationship to its PoX anchor #[derive(Debug, PartialEq)] pub enum PoxAnchorBlockStatus { - SelectedAndKnown(BlockHeaderHash, Vec), + SelectedAndKnown(BlockHeaderHash, RewardSet), SelectedAndUnknown(BlockHeaderHash), NotSelected, } @@ -96,7 +98,7 @@ impl RewardCycleInfo { SelectedAndKnown(_, _) | NotSelected => true, } } - pub fn known_selected_anchor_block(&self) -> Option<&Vec> { + pub fn known_selected_anchor_block(&self) -> Option<&RewardSet> { use self::PoxAnchorBlockStatus::*; match self.anchor_status { SelectedAndUnknown(_) => None, @@ -104,7 +106,7 @@ impl RewardCycleInfo { NotSelected => None, } } - pub fn known_selected_anchor_block_owned(self) -> Option> { + pub fn known_selected_anchor_block_owned(self) -> Option { use self::PoxAnchorBlockStatus::*; match self.anchor_status { SelectedAndUnknown(_) => None, @@ -209,7 +211,7 @@ pub trait RewardSetProvider { burnchain: &Burnchain, sortdb: &SortitionDB, block_id: &StacksBlockId, - ) -> Result, Error>; + ) -> Result; } pub struct OnChainRewardSetProvider(); @@ -222,7 +224,7 @@ impl RewardSetProvider for OnChainRewardSetProvider { burnchain: &Burnchain, sortdb: &SortitionDB, block_id: &StacksBlockId, - ) -> Result, Error> { + ) -> Result { let registered_addrs = chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?; @@ -243,7 +245,7 @@ impl RewardSetProvider for OnChainRewardSetProvider { "participation" => participation, "liquid_ustx" => liquid_ustx, "registered_addrs" => registered_addrs.len()); - return Ok(vec![]); + return Ok(RewardSet::empty()); } else { info!("PoX reward cycle threshold computed"; "burn_height" => current_burn_height, diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 58187ce8a..c65cd0eaf 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -28,8 +28,8 @@ use crate::chainstate::stacks::index::marf::MarfConnection; use crate::chainstate::stacks::Error; use crate::clarity_vm::clarity::ClarityConnection; use crate::clarity_vm::clarity::ClarityTransactionConnection; -use crate::clarity_vm::database::PoxStartCycleInfo; use crate::core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX}; +use crate::util_lib::strings::VecDisplay; use clarity::types::chainstate::BlockHeaderHash; use clarity::vm::contexts::ContractContext; use clarity::vm::costs::{ @@ -159,8 +159,45 @@ pub struct RawRewardSetEntry { pub stacker: Option, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct PoxStartCycleInfo { + pub missed_reward_slots: Vec<(PrincipalData, u128)>, +} + +#[derive(Debug, PartialEq)] +pub struct RewardSet { + pub rewarded_addresses: Vec, + pub start_cycle_state: PoxStartCycleInfo, +} + const POX_CYCLE_START_HANDLED_VALUE: &'static str = "1"; +impl PoxStartCycleInfo { + pub fn serialize(&self) -> String { + serde_json::to_string(self).expect("FATAL: failure to serialize internal struct") + } + + pub fn deserialize(from: &str) -> Option { + serde_json::from_str(from).ok() + } + + pub fn is_empty(&self) -> bool { + self.missed_reward_slots.is_empty() + } +} + +impl RewardSet { + /// Create an empty reward set where no one gets an early unlock + pub fn empty() -> RewardSet { + RewardSet { + rewarded_addresses: vec![], + start_cycle_state: PoxStartCycleInfo { + missed_reward_slots: vec![], + }, + } + } +} + impl StacksChainState { /// Return the MARF key used to store whether or not a given PoX /// cycle's "start" has been handled by the Stacks fork yet. This @@ -184,6 +221,8 @@ impl StacksChainState { db.put(&db_key, &POX_CYCLE_START_HANDLED_VALUE.to_string()); } + /// 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. pub fn handle_pox_cycle_start( clarity: &mut ClarityTransactionConnection, cycle_number: u64, @@ -563,25 +602,32 @@ impl StacksChainState { /// are repeated floor(stacked_amt / threshold) times. /// If an address appears in `addresses` multiple times, then the address's associated amounts /// are summed. - pub fn make_reward_set( - threshold: u128, - mut addresses: Vec, - ) -> Vec { + pub fn make_reward_set(threshold: u128, mut addresses: Vec) -> RewardSet { let mut reward_set = vec![]; + let mut missed_slots = vec![]; // the way that we sum addresses relies on sorting. addresses.sort_by_key(|k| k.reward_address.bytes.0); while let Some(RawRewardSetEntry { reward_address: address, amount_stacked: mut stacked_amt, - .. + stacker, }) = addresses.pop() { + let mut contributed_stackers = vec![]; + if let Some(stacker) = stacker.as_ref() { + contributed_stackers.push((stacker.clone(), stacked_amt)); + } // peak at the next address in the set, and see if we need to sum while addresses.last().map(|x| &x.reward_address) == Some(&address) { - let additional_amt = addresses + let next_contrib = addresses .pop() - .expect("BUG: first() returned some, but pop() is none.") - .amount_stacked; + .expect("BUG: first() returned some, but pop() is none."); + let additional_amt = next_contrib.amount_stacked; + + if let Some(stacker) = next_contrib.stacker { + contributed_stackers.push((stacker.clone(), additional_amt)); + } + stacked_amt = stacked_amt .checked_add(additional_amt) .expect("CORRUPTION: Stacker stacked > u128 max amount"); @@ -599,9 +645,28 @@ impl StacksChainState { test_debug!("Add to PoX reward set: {:?}", &address); reward_set.push(address.clone()); } + // if stacker did not qualify for a slot *and* they have a stacker + // pointer set by the PoX contract, then add them to auto-unlock list + if slots_taken == 0 && !contributed_stackers.is_empty() { + debug!( + "Stacker missed reward slot, added to unlock list"; + // "stackers" => %VecDisplay(&contributed_stackers), + "reward_address" => %address.clone().to_b58(), + "threshold" => threshold, + "stacked_amount" => stacked_amt + ); + for (contributor, amt) in contributed_stackers { + missed_slots.push((contributor, amt)); + } + } } info!("Reward set calculated"; "slots_occuppied" => reward_set.len()); - reward_set + RewardSet { + rewarded_addresses: reward_set, + start_cycle_state: PoxStartCycleInfo { + missed_reward_slots: missed_slots, + }, + } } pub fn get_threshold_from_participation( diff --git a/src/clarity_vm/database/mod.rs b/src/clarity_vm/database/mod.rs index 3803d63d0..46701228e 100644 --- a/src/clarity_vm/database/mod.rs +++ b/src/clarity_vm/database/mod.rs @@ -4,6 +4,7 @@ use rusqlite::{Connection, OptionalExtension}; use crate::chainstate::burn::db::sortdb::{ SortitionDB, SortitionDBConn, SortitionHandleConn, SortitionHandleTx, }; +use crate::chainstate::stacks::boot::PoxStartCycleInfo; use crate::chainstate::stacks::db::accounts::MinerReward; use crate::chainstate::stacks::db::{MinerPaymentSchedule, StacksChainState, StacksHeaderInfo}; use crate::chainstate::stacks::index::MarfTrieId; @@ -237,8 +238,22 @@ pub trait SortitionDBRef: BurnStateDB { ) -> Result, ChainstateError>; } -pub struct PoxStartCycleInfo { - pub missed_reward_slots: Vec<(PrincipalData, u128)>, +fn get_pox_start_cycle_info( + handle: &mut SortitionHandleConn, + parent_stacks_block_burn_ht: u64, + cycle_index: u64, +) -> Result, ChainstateError> { + let descended_from_last_pox_anchor = match handle.get_last_anchor_block_hash()? { + Some(pox_anchor) => handle.descended_from(parent_stacks_block_burn_ht, &pox_anchor)?, + None => return Ok(None), + }; + + if !descended_from_last_pox_anchor { + return Ok(None); + } + + let start_info = handle.get_reward_cycle_unlocks(cycle_index)?; + Ok(start_info) } impl SortitionDBRef for SortitionHandleTx<'_> { @@ -256,18 +271,7 @@ impl SortitionDBRef for SortitionHandleTx<'_> { context.chain_tip = sortition_id.clone(); let mut handle = SortitionHandleConn::new(&readonly_marf, context); - let descended_from_last_pox_anchor = match handle.get_last_anchor_block_hash()? { - Some(pox_anchor) => handle.descended_from(parent_stacks_block_burn_ht, &pox_anchor)?, - None => return Ok(None), - }; - - if !descended_from_last_pox_anchor { - return Ok(None); - } - - Ok(Some(PoxStartCycleInfo { - missed_reward_slots: vec![], - })) + get_pox_start_cycle_info(&mut handle, parent_stacks_block_burn_ht, cycle_index) } } @@ -279,15 +283,7 @@ impl SortitionDBRef for SortitionDBConn<'_> { cycle_index: u64, ) -> Result, ChainstateError> { let mut handle = self.as_handle(sortition_id); - - let descended_from_last_pox_anchor = match handle.get_last_anchor_block_hash()? { - Some(pox_anchor) => handle.descended_from(parent_stacks_block_burn_ht, &pox_anchor)?, - None => return Ok(None), - }; - - Ok(Some(PoxStartCycleInfo { - missed_reward_slots: vec![], - })) + get_pox_start_cycle_info(&mut handle, parent_stacks_block_burn_ht, cycle_index) } } From 6fff6197cd6883fe3b1d5ed303a9edb1db617c9c Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 18 Aug 2022 11:05:20 -0500 Subject: [PATCH 04/25] update existing tests --- src/chainstate/coordinator/tests.rs | 10 ++++++++-- src/chainstate/stacks/boot/contract_tests.rs | 8 +++++--- src/chainstate/stacks/boot/mod.rs | 4 +++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs index a1d3a2607..633c50fa3 100644 --- a/src/chainstate/coordinator/tests.rs +++ b/src/chainstate/coordinator/tests.rs @@ -33,6 +33,7 @@ use crate::chainstate::burn::operations::leader_block_commit::*; use crate::chainstate::burn::operations::*; use crate::chainstate::burn::*; use crate::chainstate::coordinator::{Error as CoordError, *}; +use crate::chainstate::stacks::boot::PoxStartCycleInfo; use crate::chainstate::stacks::db::{ accounts::MinerReward, ClarityTx, StacksChainState, StacksHeaderInfo, }; @@ -359,8 +360,13 @@ impl RewardSetProvider for StubbedRewardSetProvider { burnchain: &Burnchain, sortdb: &SortitionDB, block_id: &StacksBlockId, - ) -> Result, chainstate::coordinator::Error> { - Ok(self.0.clone()) + ) -> Result { + Ok(RewardSet { + rewarded_addresses: self.0.clone(), + start_cycle_state: PoxStartCycleInfo { + missed_reward_slots: vec![], + }, + }) } } diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index ef59378b7..15bfedf1c 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -942,6 +942,7 @@ fn pox_2_lock_extend_units() { } else { &POX_ADDRS[1] }; + let expected_stacker = Value::from(&USER_KEYS[1]); assert_eq!( env.eval_read_only( &POX_2_CONTRACT_TESTNET, @@ -950,9 +951,10 @@ fn pox_2_lock_extend_units() { .unwrap() .0, execute(&format!( - "{{ pox-addr: {}, total-ustx: u{} }}", + "{{ pox-addr: {}, total-ustx: u{}, stacker: (some '{}) }}", expected_pox_addr, - 1_000_000 + 1_000_000, + &expected_stacker, )) ); } @@ -1508,7 +1510,7 @@ fn pox_2_delegate_extend_units() { .unwrap() .0, execute(&format!( - "{{ pox-addr: {}, total-ustx: u{} }}", + "{{ pox-addr: {}, total-ustx: u{}, stacker: none }}", expected_pox_addr, MIN_THRESHOLD.deref(), )) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index c65cd0eaf..9c9611354 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -995,7 +995,9 @@ pub mod test { }, ]; assert_eq!( - StacksChainState::make_reward_set(threshold, addresses).len(), + StacksChainState::make_reward_set(threshold, addresses) + .rewarded_addresses + .len(), 3 ); } From a4645758765cc822d53c8f10a31f1ddeb52f915a Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Thu, 18 Aug 2022 16:39:42 -0500 Subject: [PATCH 05/25] first simple test for pox auto-unlock --- src/burnchains/burnchain.rs | 12 + src/chainstate/stacks/boot/contract_tests.rs | 2 +- src/chainstate/stacks/boot/mod.rs | 99 +++--- src/chainstate/stacks/boot/pox-2.clar | 2 +- src/chainstate/stacks/boot/pox_2_tests.rs | 332 ++++++++++++++++++- src/chainstate/stacks/db/blocks.rs | 44 ++- src/clarity_vm/database/mod.rs | 4 + stacks-common/src/util/log.rs | 2 +- 8 files changed, 437 insertions(+), 60 deletions(-) diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index b1524df93..c1729753a 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -491,6 +491,18 @@ impl Burnchain { ) } + /// Is this block the block in a reward cycle right before the reward phase + /// starts? This is the mod 0 block. + pub fn is_pre_reward_cycle_start( + first_block_ht: u64, + burn_ht: u64, + reward_cycle_length: u64, + ) -> bool { + let effective_height = burn_ht - first_block_ht; + // first block of the new reward cycle + (effective_height % reward_cycle_length) <= 1 + } + pub fn static_is_in_prepare_phase( first_block_height: u64, reward_cycle_length: u64, diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index 60c913fa7..76688db53 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -1443,7 +1443,7 @@ fn pox_2_delegate_extend_units() { .unwrap() .0.to_string(), "(err 21)".to_string(), "Delegate cannot stack-extend for User0 for 10 cycles", -); + ); assert_eq!( env.execute_transaction( diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 77f37fc39..0f79af1a0 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -229,13 +229,11 @@ impl StacksChainState { cycle_number: u64, cycle_info: Option, ) -> Result<(), Error> { + clarity.with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number)))?; + let cycle_info = match cycle_info { Some(x) => x, - None => { - return clarity - .with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number))) - .map_err(Error::from) - } + None => return Ok(()), }; let pox_contract = boot::boot_code_id(POX_2_NAME, clarity.is_mainnet()); @@ -253,7 +251,7 @@ impl StacksChainState { // lookup the Stacks account and alter their unlock height to next block let mut balance = db.get_stx_balance_snapshot(&principal); if balance.canonical_balance_repr().amount_locked() < *amount_locked { - panic!("Principal missed reward slots, but did not have as many locked tokens as expected"); + panic!("Principal missed reward slots, but did not have as many locked tokens as expected. Actual: {}, Expected: {}", balance.canonical_balance_repr().amount_locked(), *amount_locked); } balance.accelerate_unlock(); @@ -266,7 +264,7 @@ impl StacksChainState { &pox_contract, "stacking-state", &lookup_tuple - )? + ).unwrap() .expect_tuple(); let first_reward_cycle_locked = stacking_state_entry @@ -302,7 +300,7 @@ impl StacksChainState { &pox_contract, "reward-cycle-pox-address-list", &target_key - )?.expect_tuple(); + ).unwrap().expect_tuple(); let reward_cycle_entry_principal = reward_cycle_entry .get("stacker") @@ -328,7 +326,7 @@ impl StacksChainState { &pox_contract, "reward-cycle-pox-address-list-len", &reward_cycle_len_key, - )? + ).unwrap() .expect_tuple() .get("len") .expect("Malformed tuple returned by PoX contract") @@ -347,7 +345,7 @@ impl StacksChainState { &pox_contract, "reward-cycle-pox-address-list", &move_reward_cycle_map_key - )?.expect_tuple(); + ).unwrap().expect_tuple(); let stacker = move_reward_cycle_entry .get("stacker") @@ -361,7 +359,7 @@ impl StacksChainState { "reward-cycle-pox-address-list", target_key, move_reward_cycle_entry.into() - )?; + ).unwrap(); // if the last entry in `reward-cycle-pox-address-list` had an associated stacker, // we must also update that stacker's `stacking-state` @@ -374,7 +372,7 @@ impl StacksChainState { &pox_contract, "stacking-state", &moved_state_key, - )? + ).unwrap() .expect_tuple(); // calculate the index into the reward-set-indexes list that // this reward cycle is at @@ -397,8 +395,8 @@ impl StacksChainState { .clone() .expect_list(); assert!(moved_reward_indexes.len() > moved_cycle_index, "FATAL: Calculated bad move index"); - - moved_reward_indexes[moved_cycle_index] = Value::UInt(last_cycle_entry_index); + // we moved the entry to the "target" location + moved_reward_indexes[moved_cycle_index] = Value::UInt(target_entry_index); moved_state_entry.data_map.insert("reward-set-indexes".into(), Value::list_from(moved_reward_indexes) @@ -409,7 +407,7 @@ impl StacksChainState { "stacking-state", moved_state_key, moved_state_entry.into() - )?; + ).unwrap(); } } @@ -419,12 +417,12 @@ impl StacksChainState { &pox_contract, "reward-cycle-pox-address-list", &move_reward_cycle_map_key, - )?; + ).unwrap(); db.set_entry_unknown_descriptor( &pox_contract, "reward-cycle-pox-address-list-len", reward_cycle_len_key, TupleData::from_data_static(vec![("len".into(), Value::UInt(last_cycle_entry_index))]).into() - )?; + ).unwrap(); // Finally, update `reward-cycle-total-stacked` let total_stacked_key = TupleData::from_data_static(vec![("reward-cycle".into(), Value::UInt(reward_cycle_to_update))]) @@ -432,9 +430,9 @@ impl StacksChainState { let next_total_stacked_amount = db .expect_fetch_entry( &pox_contract, - "reward-cycle-pox-address-list", + "reward-cycle-total-stacked", &total_stacked_key, - )? + ).unwrap() .expect_tuple() .get_owned("total-ustx") .expect("Malformed tuple returned by PoX contract") @@ -443,18 +441,18 @@ impl StacksChainState { .expect("FATAL: Unlocked more STX in a cycle than were stacked in that cycle"); db.set_entry_unknown_descriptor( &pox_contract, - "reward-cycle-pox-address-list", + "reward-cycle-total-stacked", total_stacked_key, TupleData::from_data_static(vec![("total-ustx".into(), Value::UInt(next_total_stacked_amount))]).into(), - )?; + ).unwrap(); } - // Now that we've cleaned up all the reward set entries for the user, lets delete the user's stacking-state + // Now that we've cleaned up all the reward set entries for the user, delete the user's stacking-state db.delete_entry_unknown_descriptor( &pox_contract, "stacking-state", &lookup_tuple - )?; + ).unwrap(); Ok(()) })?; @@ -636,11 +634,11 @@ impl StacksChainState { let slots_taken = u32::try_from(stacked_amt / threshold) .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); info!( - "Slots taken by {} = {}, on stacked_amt = {}, threshold = {}", - &address.clone(), - slots_taken, - stacked_amt, - threshold + "Reward slots taken"; + "reward_address" => %address, + "slots_taken" => slots_taken, + "stacked_amt" => stacked_amt, + "pox_threshold" => threshold, ); for _i in 0..slots_taken { test_debug!("Add to PoX reward set: {:?}", &address); @@ -649,13 +647,14 @@ impl StacksChainState { // if stacker did not qualify for a slot *and* they have a stacker // pointer set by the PoX contract, then add them to auto-unlock list if slots_taken == 0 && !contributed_stackers.is_empty() { - debug!( - "Stacker missed reward slot, added to unlock list"; - // "stackers" => %VecDisplay(&contributed_stackers), - "reward_address" => %address.clone().to_b58(), - "threshold" => threshold, - "stacked_amount" => stacked_amt - ); + info!( + "Stacker missed reward slot, added to unlock list"; + // "stackers" => %VecDisplay(&contributed_stackers), + "reward_address" => %address.clone().to_b58(), + "threshold" => threshold, + "stacked_amount" => stacked_amt + ); + // todo: we need to consolidate these stackers for (contributor, amt) in contributed_stackers { missed_slots.push((contributor, amt)); } @@ -870,8 +869,10 @@ impl StacksChainState { .map(|value| value.expect_principal()); debug!( - "PoX reward address (for {} ustx): {}", - total_ustx, &reward_address, + "Parsed PoX reward address"; + "stacked_ustx" => total_ustx, + "reward_address" => %reward_address, + "stacker" => ?stacker, ); ret.push(RawRewardSetEntry { reward_address, @@ -903,8 +904,6 @@ impl StacksChainState { .pox_constants .active_pox_contract(reward_cycle_start_height); - debug!("Using pox_contract = {}", pox_contract_name); - match pox_contract_name { x if x == POX_1_NAME => self.get_reward_addresses_pox_1(sortdb, block_id, reward_cycle), x if x == POX_2_NAME => self.get_reward_addresses_pox_2(sortdb, block_id, reward_cycle), @@ -1677,14 +1676,28 @@ pub mod test { block_id: &StacksBlockId, ) -> Result, Error> { let burn_block_height = get_par_burn_block_height(state, block_id); + get_reward_set_entries_at_block(state, burnchain, sortdb, block_id, burn_block_height).map( + |addrs| { + addrs + .into_iter() + .map(|x| (x.reward_address, x.amount_stacked)) + .collect() + }, + ) + } + + pub fn get_reward_set_entries_at_block( + state: &mut StacksChainState, + burnchain: &Burnchain, + sortdb: &SortitionDB, + block_id: &StacksBlockId, + burn_block_height: u64, + ) -> Result, Error> { state .get_reward_addresses(burnchain, sortdb, burn_block_height, block_id) .and_then(|mut addrs| { addrs.sort_by_key(|k| k.reward_address.bytes()); - Ok(addrs - .into_iter() - .map(|x| (x.reward_address, x.amount_stacked)) - .collect()) + Ok(addrs) }) } diff --git a/src/chainstate/stacks/boot/pox-2.clar b/src/chainstate/stacks/boot/pox-2.clar index 41121767b..2115dabab 100644 --- a/src/chainstate/stacks/boot/pox-2.clar +++ b/src/chainstate/stacks/boot/pox-2.clar @@ -393,7 +393,7 @@ (num-cycles uint)) (begin ;; minimum uSTX must be met - (asserts! (<= (print (get-stacking-minimum)) amount-ustx) + (asserts! (<= (get-stacking-minimum) amount-ustx) (err ERR_STACKING_THRESHOLD_NOT_MET)) (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) diff --git a/src/chainstate/stacks/boot/pox_2_tests.rs b/src/chainstate/stacks/boot/pox_2_tests.rs index 1b69ac544..e46383c3c 100644 --- a/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/src/chainstate/stacks/boot/pox_2_tests.rs @@ -12,9 +12,11 @@ use crate::chainstate::stacks::boot::{ use crate::chainstate::stacks::db::{ MinerPaymentSchedule, StacksHeaderInfo, MINER_REWARD_MATURITY, }; +use crate::chainstate::stacks::index::marf::MarfConnection; use crate::chainstate::stacks::index::MarfTrieId; use crate::chainstate::stacks::*; use crate::clarity_vm::database::marf::MarfedKV; +use crate::clarity_vm::database::HeadersDBConn; use crate::core::*; use crate::util_lib::db::{DBConn, FromRow}; use clarity::vm::contexts::OwnedEnvironment; @@ -51,7 +53,7 @@ use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId, VRFSeed, }; -use super::test::*; +use super::{test::*, RawRewardSetEntry}; use crate::clarity_vm::clarity::Error as ClarityError; use crate::chainstate::burn::operations::*; @@ -66,6 +68,84 @@ fn get_tip(sortdb: Option<&SortitionDB>) -> BlockSnapshot { SortitionDB::get_canonical_burn_chain_tip(&sortdb.unwrap().conn()).unwrap() } +/// Get the reward set entries if evaluated at the given StacksBlock +pub fn get_reward_set_entries_at( + peer: &mut TestPeer, + tip: &StacksBlockId, + at_burn_ht: u64, +) -> Vec { + let burnchain = peer.config.burnchain.clone(); + with_sortdb(peer, |ref mut c, ref sortdb| { + get_reward_set_entries_at_block(c, &burnchain, sortdb, tip, at_burn_ht).unwrap() + }) +} + +/// Get the STXBalance for `account` at the given chaintip +pub fn get_stx_account_at( + peer: &mut TestPeer, + tip: &StacksBlockId, + account: &PrincipalData, +) -> STXBalance { + with_clarity_db_ro(peer, tip, |db| db.get_account_stx_balance(account)) +} + +/// Get the STXBalance for `account` at the given chaintip +pub fn get_stacking_state_pox_2( + peer: &mut TestPeer, + tip: &StacksBlockId, + account: &PrincipalData, +) -> Option { + with_clarity_db_ro(peer, tip, |db| { + let lookup_tuple = Value::Tuple(TupleData::from_data_static(vec![( + "stacker".into(), + account.clone().into(), + )])); + db.fetch_entry_unknown_descriptor( + &boot_code_id(boot::POX_2_NAME, false), + "stacking-state", + &lookup_tuple, + ) + .unwrap() + .expect_optional() + }) +} + +/// Get the `cycle_number`'s total stacked amount at the given chaintip +pub fn get_reward_cycle_total(peer: &mut TestPeer, tip: &StacksBlockId, cycle_number: u64) -> u128 { + with_clarity_db_ro(peer, tip, |db| { + let total_stacked_key = TupleData::from_data_static(vec![( + "reward-cycle".into(), + Value::UInt(cycle_number.into()), + )]) + .into(); + db.expect_fetch_entry( + &boot_code_id(boot::POX_2_NAME, false), + "reward-cycle-total-stacked", + &total_stacked_key, + ) + .unwrap() + .expect_tuple() + .get_owned("total-ustx") + .expect("Malformed tuple returned by PoX contract") + .expect_u128() + }) +} + +/// Allows you to do something read-only with the ClarityDB at the given chaintip +pub fn with_clarity_db_ro(peer: &mut TestPeer, tip: &StacksBlockId, todo: F) -> R +where + F: FnOnce(&mut ClarityDatabase) -> R, +{ + with_sortdb(peer, |ref mut c, ref sortdb| { + let headers_db = HeadersDBConn(c.state_index.sqlite_conn()); + let burn_db = sortdb.index_conn(); + let mut read_only_clar = c + .clarity_state + .read_only_connection(tip, &headers_db, &burn_db); + read_only_clar.with_clarity_db_readonly(todo) + }) +} + /// In this test case, two Stackers, Alice and Bob stack and interact with the /// PoX v1 contract and PoX v2 contract across the epoch transition. /// @@ -505,6 +585,256 @@ fn test_simple_pox_lockup_transition_pox_2() { ); } +#[test] +fn test_simple_pox_2_auto_unlock_ab() { + test_simple_pox_2_auto_unlock(true) +} + +#[test] +fn test_simple_pox_2_auto_unlock_ba() { + test_simple_pox_2_auto_unlock(false) +} + +/// In this test case, two Stackers, Alice and Bob stack and interact with the +/// PoX v1 contract and PoX v2 contract across the epoch transition. +/// +/// Alice: stacks via PoX v1 for 4 cycles. The third of these cycles occurs after +/// the PoX v1 -> v2 transition, and so Alice gets "early unlocked". +/// After the early unlock, Alice re-stacks in PoX v2 +/// Alice tries to stack again via PoX v1, which is allowed by the contract, +/// but forbidden by the VM (because PoX has transitioned to v2) +/// Bob: stacks via PoX v2 for 6 cycles. He attempted to stack via PoX v1 as well, +/// but is forbidden because he has already placed an account lock via PoX v2. +/// +/// Note: this test is symmetric over the order of alice and bob's stacking calls. +/// when alice goes first, the auto-unlock code doesn't need to perform a "move" +/// when bob goes first, the auto-unlock code does need to perform a "move" +fn test_simple_pox_2_auto_unlock(alice_first: bool) { + // this is the number of blocks after the first sortition any V1 + // PoX locks will automatically unlock at. + let AUTO_UNLOCK_HEIGHT = 12; + let EXPECTED_FIRST_V2_CYCLE = 8; + // the sim environment produces 25 empty sortitions before + // tenures start being tracked. + let EMPTY_SORTITIONS = 25; + + let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); + burnchain.pox_constants.reward_cycle_length = 5; + burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; + burnchain.pox_constants.pox_participation_threshold_pct = 1; + burnchain.pox_constants.v1_unlock_height = AUTO_UNLOCK_HEIGHT + EMPTY_SORTITIONS; + + let first_v2_cycle = burnchain + .block_height_to_reward_cycle(burnchain.pox_constants.v1_unlock_height as u64) + .unwrap() + + 1; + + assert_eq!(first_v2_cycle, EXPECTED_FIRST_V2_CYCLE); + + eprintln!("First v2 cycle = {}", first_v2_cycle); + + let epochs = StacksEpoch::all(0, 0, EMPTY_SORTITIONS as u64 + 10); + + let observer = TestEventObserver::new(); + + let (mut peer, mut keys) = instantiate_pox_peer_with_epoch( + &burnchain, + &format!("test_simple_pox_2_auto_unlock_{}", alice_first), + 6002, + Some(epochs.clone()), + Some(&observer), + ); + + let num_blocks = 35; + + let alice = keys.pop().unwrap(); + let bob = keys.pop().unwrap(); + let charlie = keys.pop().unwrap(); + + let mut coinbase_nonce = 0; + + // produce blocks until the epoch switch + for _i in 0..10 { + peer.tenure_with_txs(&[], &mut coinbase_nonce); + } + + // in the next tenure, PoX 2 should now exist. + // Lets have Bob lock up for v2 + // this will lock for cycles 8, 9, 10, and 11 + // the first v2 cycle will be 8 + let tip = get_tip(peer.sortdb.as_ref()); + + let alice_lockup = make_pox_2_lockup( + &alice, + 0, + 1024 * POX_THRESHOLD_STEPS_USTX, + AddressHashMode::SerializeP2PKH, + key_to_stacks_addr(&alice).bytes, + 6, + tip.block_height, + ); + + let bob_lockup = make_pox_2_lockup( + &bob, + 0, + 1 * POX_THRESHOLD_STEPS_USTX, + AddressHashMode::SerializeP2PKH, + key_to_stacks_addr(&bob).bytes, + 6, + tip.block_height, + ); + + // our "tenure counter" is now at 10 + assert_eq!(tip.block_height, 10 + EMPTY_SORTITIONS as u64); + + let txs = if alice_first { + [alice_lockup, bob_lockup] + } else { + [bob_lockup, alice_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 + // at the cycle start + for cycle_number in EXPECTED_FIRST_V2_CYCLE..(EXPECTED_FIRST_V2_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(), + key_to_stacks_addr(&bob).bytes.0.to_vec() + ); + assert_eq!( + reward_set_entries[1].reward_address.bytes(), + key_to_stacks_addr(&alice).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(EXPECTED_FIRST_V2_CYCLE) + 1; + + // but first, check that bob has locked tokens at (height_target + 1) + let (bob_bal, _) = get_stx_account_at( + &mut peer, + &latest_block, + &key_to_stacks_addr(&bob).to_account_principal(), + ) + .canonical_repr_at_block(height_target + 1, burnchain.pox_constants.v1_unlock_height); + 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 just contains entries for alice + // at the cycle start + for cycle_number in EXPECTED_FIRST_V2_CYCLE..(EXPECTED_FIRST_V2_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); + assert_eq!( + reward_set_entries[0].reward_address.bytes(), + key_to_stacks_addr(&alice).bytes.0.to_vec() + ); + } + + // now check that bob has no locked tokens at (height_target + 1) + let (bob_bal, _) = get_stx_account_at( + &mut peer, + &latest_block, + &key_to_stacks_addr(&bob).to_account_principal(), + ) + .canonical_repr_at_block(height_target + 1, burnchain.pox_constants.v1_unlock_height); + assert_eq!(bob_bal.amount_locked(), 0); + + // but bob's still locked at (height_target): the unlock is accelerated to the "next" burn block + let (bob_bal, _) = get_stx_account_at( + &mut peer, + &latest_block, + &key_to_stacks_addr(&bob).to_account_principal(), + ) + .canonical_repr_at_block(height_target + 1, burnchain.pox_constants.v1_unlock_height); + assert_eq!(bob_bal.amount_locked(), 0); + + // check that the total reward cycle amounts have decremented correctly + for cycle_number in EXPECTED_FIRST_V2_CYCLE..(EXPECTED_FIRST_V2_CYCLE + 6) { + assert_eq!( + get_reward_cycle_total(&mut peer, &latest_block, cycle_number), + 1024 * POX_THRESHOLD_STEPS_USTX + ); + } + + // check that bob's stacking-state is gone and alice's stacking-state is correct + assert!( + get_stacking_state_pox_2( + &mut peer, + &latest_block, + &key_to_stacks_addr(&bob).to_account_principal() + ) + .is_none(), + "Bob should not have a stacking-state entry" + ); + + let alice_state = get_stacking_state_pox_2( + &mut peer, + &latest_block, + &key_to_stacks_addr(&alice).to_account_principal(), + ) + .expect("Alice should have stacking-state entry") + .expect_tuple(); + let reward_indexes_str = format!("{}", alice_state.get("reward-set-indexes").unwrap()); + assert_eq!(reward_indexes_str, "(u0 u0 u0 u0 u0 u0)"); + + // now let's check some tx receipts + + let alice_address = key_to_stacks_addr(&alice); + let bob_address = key_to_stacks_addr(&bob); + let blocks = observer.get_blocks(); + + let mut alice_txs = HashMap::new(); + let mut bob_txs = HashMap::new(); + let mut charlie_txs = HashMap::new(); + + eprintln!("Alice addr: {}", alice_address); + eprintln!("Bob addr: {}", bob_address); + + for b in blocks.into_iter() { + for r in b.receipts.into_iter() { + if let TransactionOrigin::Stacks(ref t) = r.transaction { + let addr = t.auth.origin().address_testnet(); + eprintln!("TX addr: {}", addr); + 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); + } else if addr == key_to_stacks_addr(&charlie) { + assert!( + r.execution_cost != ExecutionCost::zero(), + "Execution cost is not zero!" + ); + charlie_txs.insert(t.auth.get_origin_nonce(), r); + } + } + } + } + + assert_eq!(alice_txs.len(), 1); + assert_eq!(charlie_txs.len(), 0); + + assert_eq!(bob_txs.len(), 1); + + // TX0 -> Bob's initial lockup in PoX 2 + assert!( + match bob_txs.get(&0).unwrap().result { + Value::Response(ref r) => r.committed, + _ => false, + }, + "Bob tx0 should have committed okay" + ); +} + /// In this test case, two Stackers, Alice and Bob stack and interact with the /// PoX v1 contract and PoX v2 contract across the epoch transition. This test /// covers the two different ways a Stacker can validly extend via `stack-extend` -- diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 68acd177a..cf0dd4111 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -5147,24 +5147,42 @@ impl StacksChainState { let evaluated_epoch = clarity_tx.get_epoch(); clarity_tx.reset_cost(parent_block_cost.clone()); + info!("Evaluated epoch = {}", evaluated_epoch); if evaluated_epoch >= StacksEpochId::Epoch21 { - let pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( + let mut pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( burn_tip_height.into(), burn_dbconn.get_burn_start_height().into(), burn_dbconn.get_pox_reward_cycle_length().into(), ).expect("FATAL: Unrecoverable chainstate corruption: Epoch 2.1 code evaluated before first burn block height"); - let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { - Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) - }); - if !handled { - let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( - &parent_sortition_id, - chain_tip.burn_header_height.into(), - pox_reward_cycle, - )?; - clarity_tx.block.as_transaction(|clarity_tx| { - Self::handle_pox_cycle_start(clarity_tx, pox_reward_cycle, pox_start_cycle_info) - })?; + // Do not try to handle auto-unlocks on pox_reward_cycle 0 + // This cannot even occur in the mainchain, because 2.1 starts much + // after the 1st reward cycle, however, this could come up in mocknets or regtest. + if pox_reward_cycle > 1 { + if Burnchain::is_pre_reward_cycle_start( + burn_dbconn.get_burn_start_height().into(), + burn_tip_height.into(), + burn_dbconn.get_pox_reward_cycle_length().into(), + ) { + pox_reward_cycle -= 1; + } + let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { + Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) + }); + info!("Check handled"; "pox_cycle" => pox_reward_cycle, "handled" => handled); + if !handled { + let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( + &parent_sortition_id, + chain_tip.burn_header_height.into(), + pox_reward_cycle, + )?; + clarity_tx.block.as_transaction(|clarity_tx| { + Self::handle_pox_cycle_start( + clarity_tx, + pox_reward_cycle, + pox_start_cycle_info, + ) + })?; + } } } diff --git a/src/clarity_vm/database/mod.rs b/src/clarity_vm/database/mod.rs index 00b992109..53f592937 100644 --- a/src/clarity_vm/database/mod.rs +++ b/src/clarity_vm/database/mod.rs @@ -255,6 +255,10 @@ fn get_pox_start_cycle_info( } let start_info = handle.get_reward_cycle_unlocks(cycle_index)?; + info!( + "get_pox_start_cycle_info"; + "start_info" => ?start_info, + ); Ok(start_info) } diff --git a/stacks-common/src/util/log.rs b/stacks-common/src/util/log.rs index 26de14e67..5f2095c7e 100644 --- a/stacks-common/src/util/log.rs +++ b/stacks-common/src/util/log.rs @@ -65,7 +65,7 @@ fn print_msg_header(mut rd: &mut dyn RecordDecorator, record: &Record) -> io::Re write!(rd, " ")?; match thread::current().name() { None => write!(rd, "[{:?}]", thread::current().id())?, - Some(name) => write!(rd, "[{}]", name)?, + Some(name) => write!(rd, "[{:.15}]", name)?, } rd.start_whitespace()?; From 0014b0f3f3c4f6428fb0d069bcbab730154d3a77 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 22 Aug 2022 12:54:50 -0500 Subject: [PATCH 06/25] consolidate contributed stackers --- src/chainstate/stacks/boot/mod.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 0f79af1a0..87fed1a41 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -31,7 +31,9 @@ use crate::clarity_vm::clarity::ClarityConnection; use crate::clarity_vm::clarity::ClarityTransactionConnection; use crate::core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX}; use crate::util_lib::strings::VecDisplay; +use clarity::codec::StacksMessageCodec; use clarity::types::chainstate::BlockHeaderHash; +use clarity::util::hash::to_hex; use clarity::vm::contexts::ContractContext; use clarity::vm::costs::{ cost_functions::ClarityCostFunction, ClarityCostFunctionReference, CostStateSummary, @@ -654,9 +656,22 @@ impl StacksChainState { "threshold" => threshold, "stacked_amount" => stacked_amt ); - // todo: we need to consolidate these stackers - for (contributor, amt) in contributed_stackers { - missed_slots.push((contributor, amt)); + contributed_stackers + .sort_by_cached_key(|(stacker, ..)| to_hex(&stacker.serialize_to_vec())); + while let Some((contributor, amt)) = contributed_stackers.pop() { + let mut total_amount = amt; + while contributed_stackers.last().map(|(stacker, ..)| stacker) + == Some(&contributor) + { + let (add_stacker, additional) = contributed_stackers + .pop() + .expect("BUG: last() returned some, but pop() is none."); + assert_eq!(&add_stacker, &contributor); + total_amount = total_amount + .checked_add(additional) + .expect("CORRUPTION: Stacked stacked > u128 max amount"); + } + missed_slots.push((contributor, total_amount)); } } } From 10244bfa0031a065d8ed78b1dc58a3682fb5d96e Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 26 Aug 2022 09:25:28 -0500 Subject: [PATCH 07/25] address PR feedback --- clarity/src/vm/database/structures.rs | 5 ++--- src/chainstate/stacks/boot/mod.rs | 18 ++---------------- src/chainstate/stacks/boot/pox-2.clar | 2 +- src/chainstate/stacks/db/blocks.rs | 4 ++-- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index f1c8bc151..89507523d 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -474,9 +474,8 @@ impl<'db, 'conn> STXBalanceSnapshot<'db, 'conn> { }; } - /// Lock `amount_to_lock` tokens on this account until `unlock_burn_height`. - /// After calling, this method will set the balance to a "LockedPoxTwo" balance, - /// because this method is only invoked as a result of PoX2 interactions + /// If this snapshot is locked, then alter the lock height to be + /// the next burn block (i.e., `self.burn_block_height + 1`) pub fn accelerate_unlock(&mut self) { let unlocked = self.unlock_available_tokens_if_any(); if unlocked > 0 { diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 87fed1a41..fe241a66d 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -233,6 +233,8 @@ impl StacksChainState { ) -> Result<(), Error> { clarity.with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number)))?; + debug!("Handling PoX reward cycle start"; "reward_cycle" => cycle_number, "cycle_active" => cycle_info.is_some()); + let cycle_info = match cycle_info { Some(x) => x, None => return Ok(()), @@ -463,22 +465,6 @@ impl StacksChainState { Ok(()) } - /// After Stacks 2.1, invoke to process any Stacks chainstate operations required - /// at the start of a reward cycle (i.e., qualifying unlocks) - pub fn process_pox_cycle_start_2_1(last_anchor_block: Option) { - // Step 1: determine if a PoX anchor block was chosen for this cycle - // *and* that this Stacks block descends from that block - match last_anchor_block { - Some(_) => {} - None => { - debug!( - "No anchor block chosen in this PoX cycle, so no need to process cycle start" - ); - return; - } - } - } - fn eval_boot_code_read_only( &mut self, sortdb: &SortitionDB, diff --git a/src/chainstate/stacks/boot/pox-2.clar b/src/chainstate/stacks/boot/pox-2.clar index 2115dabab..c7838167d 100644 --- a/src/chainstate/stacks/boot/pox-2.clar +++ b/src/chainstate/stacks/boot/pox-2.clar @@ -296,7 +296,7 @@ { total-ustx: (+ (get amount-ustx params) total-ustx) }) (some reward-index)) none)) - (next-i (if (< i num-cycles) (+ i u1) (+ i u0)))) + (next-i (if (< i num-cycles) (+ i u1) i))) { pox-addr: (get pox-addr params), first-reward-cycle: (get first-reward-cycle params), diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index cf0dd4111..b07a677ad 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -5147,7 +5147,7 @@ impl StacksChainState { let evaluated_epoch = clarity_tx.get_epoch(); clarity_tx.reset_cost(parent_block_cost.clone()); - info!("Evaluated epoch = {}", evaluated_epoch); + debug!("Evaluating block with epoch = {}", evaluated_epoch); if evaluated_epoch >= StacksEpochId::Epoch21 { let mut pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( burn_tip_height.into(), @@ -5168,7 +5168,7 @@ impl StacksChainState { let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) }); - info!("Check handled"; "pox_cycle" => pox_reward_cycle, "handled" => handled); + if !handled { let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( &parent_sortition_id, From 9e676926c0c8ceef58e12eab65f116beb9fc1c55 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 26 Aug 2022 13:46:50 -0500 Subject: [PATCH 08/25] refactor: add SortitionHandle trait to capture tx+conn --- src/burnchains/burnchain.rs | 1 + src/chainstate/burn/db/sortdb.rs | 175 +++++++++++------- .../burn/operations/leader_block_commit.rs | 1 + 3 files changed, 105 insertions(+), 72 deletions(-) diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index c1729753a..4d752abe3 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -46,6 +46,7 @@ use crate::burnchains::{ BurnchainStateTransition, BurnchainStateTransitionOps, BurnchainTransaction, Error as burnchain_error, PoxConstants, }; +use crate::chainstate::burn::db::sortdb::SortitionHandle; use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleConn, SortitionHandleTx}; use crate::chainstate::burn::distribution::BurnSamplePoint; use crate::chainstate::burn::operations::{ diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 3cf377c5b..2aa208745 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -842,6 +842,81 @@ impl db_keys { } } +/// Trait for structs that provide a chaintip-indexed handle into the +/// SortitionDB (i.e., a MARF view from a particular SortitionId) +pub trait SortitionHandle { + /// Returns a connection to the SQLite db. If this handle is wrapping + /// a transaction, this should point to the open transaction. + fn sqlite(&self) -> &Connection; + + /// Returns the snapshot of the burnchain block at burnchain height `block_height`. + /// Returns None if there is no block at this height. + fn get_block_snapshot_by_height( + &mut self, + block_height: u64, + ) -> Result, db_error>; + + /// is the given block a descendant of `potential_ancestor`? + /// * block_at_burn_height: the burn height of the sortition that chose the stacks block to check + /// * potential_ancestor: the stacks block hash of the potential ancestor + fn descended_from( + &mut self, + block_at_burn_height: u64, + potential_ancestor: &BlockHeaderHash, + ) -> Result { + let earliest_block_height = self.sqlite().query_row( + "SELECT block_height FROM snapshots WHERE winning_stacks_block_hash = ? ORDER BY block_height ASC LIMIT 1", + &[potential_ancestor], + |row| Ok(u64::from_row(row).expect("Expected u64 in database")))?; + + let mut sn = self + .get_block_snapshot_by_height(block_at_burn_height)? + .ok_or_else(|| { + test_debug!("No snapshot at height {}", block_at_burn_height); + db_error::NotFoundError + })?; + + while sn.block_height >= earliest_block_height { + if !sn.sortition { + return Ok(false); + } + if &sn.winning_stacks_block_hash == potential_ancestor { + return Ok(true); + } + + // step back to the parent + match SortitionDB::get_block_commit_parent_sortition_id( + self.sqlite(), + &sn.winning_block_txid, + &sn.sortition_id, + )? { + Some(parent_sortition_id) => { + // we have the block_commit parent memoization data + test_debug!( + "Parent sortition of {} memoized as {}", + &sn.winning_block_txid, + &parent_sortition_id + ); + sn = SortitionDB::get_block_snapshot(self.sqlite(), &parent_sortition_id)? + .ok_or_else(|| db_error::NotFoundError)?; + } + None => { + // we do not have the block_commit parent memoization data + // step back to the parent + test_debug!("No parent sortition memo for {}", &sn.winning_block_txid); + let block_commit = + get_block_commit_by_txid(&self.sqlite(), &sn.winning_block_txid)? + .expect("CORRUPTION: winning block commit for snapshot not found"); + sn = self + .get_block_snapshot_by_height(block_commit.parent_block_ptr as u64)? + .ok_or_else(|| db_error::NotFoundError)?; + } + } + } + return Ok(false); + } +} + impl<'a> SortitionHandleTx<'a> { /// begin a MARF transaction with this connection /// this is used by _writing_ contexts @@ -1152,6 +1227,34 @@ impl<'a> SortitionHandleTx<'a> { } } +impl SortitionHandle for SortitionHandleTx<'_> { + fn get_block_snapshot_by_height( + &mut self, + block_height: u64, + ) -> Result, db_error> { + assert!(block_height < BLOCK_HEIGHT_MAX); + let chain_tip = self.context.chain_tip.clone(); + SortitionDB::get_ancestor_snapshot_tx(self, block_height, &chain_tip) + } + + fn sqlite(&self) -> &Connection { + self.tx() + } +} + +impl SortitionHandle for SortitionHandleConn<'_> { + fn get_block_snapshot_by_height( + &mut self, + block_height: u64, + ) -> Result, db_error> { + SortitionHandleConn::get_block_snapshot_by_height(self, block_height) + } + + fn sqlite(&self) -> &Connection { + self.conn() + } +} + impl<'a> SortitionHandleTx<'a> { pub fn set_stacks_block_accepted( &mut self, @@ -1321,75 +1424,6 @@ impl<'a> SortitionHandleTx<'a> { self.get_reward_set_size_at(&self.context.chain_tip.clone()) } - /// is the given block a descendant of `potential_ancestor`? - /// * block_at_burn_height: the burn height of the sortition that chose the stacks block to check - /// * potential_ancestor: the stacks block hash of the potential ancestor - pub fn descended_from( - &mut self, - block_at_burn_height: u64, - potential_ancestor: &BlockHeaderHash, - ) -> Result { - let earliest_block_height = self.tx().query_row( - "SELECT block_height FROM snapshots WHERE winning_stacks_block_hash = ? ORDER BY block_height ASC LIMIT 1", - &[potential_ancestor], - |row| Ok(u64::from_row(row).expect("Expected u64 in database")))?; - - let mut sn = self - .get_block_snapshot_by_height(block_at_burn_height)? - .ok_or_else(|| { - test_debug!("No snapshot at height {}", block_at_burn_height); - db_error::NotFoundError - })?; - - while sn.block_height >= earliest_block_height { - if !sn.sortition { - return Ok(false); - } - if &sn.winning_stacks_block_hash == potential_ancestor { - return Ok(true); - } - - // step back to the parent - match SortitionDB::get_block_commit_parent_sortition_id( - self.tx(), - &sn.winning_block_txid, - &sn.sortition_id, - )? { - Some(parent_sortition_id) => { - // we have the block_commit parent memoization data - test_debug!( - "Parent sortition of {} memoized as {}", - &sn.winning_block_txid, - &parent_sortition_id - ); - sn = SortitionDB::get_block_snapshot(self.tx(), &parent_sortition_id)? - .ok_or_else(|| db_error::NotFoundError)?; - } - None => { - // we do not have the block_commit parent memoization data - // step back to the parent - test_debug!("No parent sortition memo for {}", &sn.winning_block_txid); - let block_commit = - get_block_commit_by_txid(&self.tx(), &sn.winning_block_txid)? - .expect("CORRUPTION: winning block commit for snapshot not found"); - sn = self - .get_block_snapshot_by_height(block_commit.parent_block_ptr as u64)? - .ok_or_else(|| db_error::NotFoundError)?; - } - } - } - return Ok(false); - } - - pub fn get_block_snapshot_by_height( - &mut self, - block_height: u64, - ) -> Result, db_error> { - assert!(block_height < BLOCK_HEIGHT_MAX); - let chain_tip = self.context.chain_tip.clone(); - SortitionDB::get_ancestor_snapshot_tx(self, block_height, &chain_tip) - } - pub fn get_last_anchor_block_hash(&mut self) -> Result, db_error> { let chain_tip = self.context.chain_tip.clone(); let anchor_block_hash = SortitionDB::parse_last_anchor_block_hash( @@ -1713,9 +1747,6 @@ impl<'a> SortitionHandleConn<'a> { } } - /// Returns the snapshot of the burnchain block at burnchain height `block_height`. - /// - /// Returns None if there is no block at this height. pub fn get_block_snapshot_by_height( &self, block_height: u64, diff --git a/src/chainstate/burn/operations/leader_block_commit.rs b/src/chainstate/burn/operations/leader_block_commit.rs index 162119069..130d46a5a 100644 --- a/src/chainstate/burn/operations/leader_block_commit.rs +++ b/src/chainstate/burn/operations/leader_block_commit.rs @@ -23,6 +23,7 @@ use crate::burnchains::BurnchainBlockHeader; use crate::burnchains::Txid; use crate::burnchains::{BurnchainRecipient, BurnchainSigner}; use crate::burnchains::{BurnchainTransaction, PublicKey}; +use crate::chainstate::burn::db::sortdb::SortitionHandle; use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleTx}; use crate::chainstate::burn::operations::Error as op_error; use crate::chainstate::burn::operations::{ From 9d00a6cfd48fe557dc524a6935e7ab9c1af74d79 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 30 Aug 2022 15:55:28 -0500 Subject: [PATCH 09/25] address PR feedback, use `define-private` method for handling unlock updates --- clarity/src/vm/clarity.rs | 8 + clarity/src/vm/contexts.rs | 26 ++- clarity/src/vm/database/clarity_db.rs | 27 --- clarity/src/vm/types/mod.rs | 7 - src/chainstate/burn/db/sortdb.rs | 60 ----- src/chainstate/stacks/boot/mod.rs | 253 +++------------------- src/chainstate/stacks/boot/pox-2.clar | 59 +++++ src/chainstate/stacks/boot/pox_2_tests.rs | 16 +- src/chainstate/stacks/db/blocks.rs | 98 ++++++--- src/clarity_vm/database/mod.rs | 2 +- 10 files changed, 196 insertions(+), 360 deletions(-) diff --git a/clarity/src/vm/clarity.rs b/clarity/src/vm/clarity.rs index eb9d1d912..69667925c 100644 --- a/clarity/src/vm/clarity.rs +++ b/clarity/src/vm/clarity.rs @@ -149,6 +149,14 @@ pub trait ClarityConnection { } pub trait TransactionConnection: ClarityConnection { + /// Do something with this connection's Clarity environment that can be aborted + /// with `abort_call_back`. + /// This returns the return value of `to_do`: + /// * the generic term `R` + /// * the asset changes during `to_do` in an `AssetMap` + /// * the Stacks events during the transaction + /// and a `bool` value which is `true` if the `abort_call_back` caused the changes to abort + /// If `to_do` returns an `Err` variant, then the changes are aborted. fn with_abort_callback( &mut self, to_do: F, diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index de013fe9b..11ac9a1de 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -1061,11 +1061,35 @@ impl<'a, 'b> Environment<'a, 'b> { } pub fn execute_contract( + &mut self, + contract: &QualifiedContractIdentifier, + tx_name: &str, + args: &[SymbolicExpression], + read_only: bool, + ) -> Result { + self.inner_execute_contract(contract, tx_name, args, read_only, false) + } + + /// This method is exposed for callers that need to invoke a private method directly. + /// For example, this is used by the Stacks chainstate for invoking private methods + /// on the pox-2 contract. This should not be called by user transaction processing. + pub fn execute_contract_allow_private( + &mut self, + contract: &QualifiedContractIdentifier, + tx_name: &str, + args: &[SymbolicExpression], + read_only: bool, + ) -> Result { + self.inner_execute_contract(contract, tx_name, args, read_only, true) + } + + fn inner_execute_contract( &mut self, contract_identifier: &QualifiedContractIdentifier, tx_name: &str, args: &[SymbolicExpression], read_only: bool, + allow_private: bool, ) -> Result { let contract_size = self .global_context @@ -1080,7 +1104,7 @@ impl<'a, 'b> Environment<'a, 'b> { let func = contract.contract_context.lookup_function(tx_name) .ok_or_else(|| { CheckErrors::UndefinedFunction(tx_name.to_string()) })?; - if !func.is_public() { + if !allow_private && !func.is_public() { return Err(CheckErrors::NoSuchPublicFunction(contract_identifier.to_string(), tx_name.to_string()).into()); } else if read_only && !func.is_read_only() { return Err(CheckErrors::PublicFunctionNotReadOnly(contract_identifier.to_string(), tx_name.to_string()).into()); diff --git a/clarity/src/vm/database/clarity_db.rs b/clarity/src/vm/database/clarity_db.rs index 8fd190e7b..7871ba04e 100644 --- a/clarity/src/vm/database/clarity_db.rs +++ b/clarity/src/vm/database/clarity_db.rs @@ -1205,23 +1205,6 @@ impl<'a> ClarityDatabase<'a> { ) } - /// Like fetch_entry_unknown_descriptor, except that it expects - /// to receive a value (i.e., it expects the state to exist) - /// This should only ever be invoked outside of Clarity, e.g. by the VM - /// when loading state from a known-contract - pub fn expect_fetch_entry( - &mut self, - contract_identifier: &QualifiedContractIdentifier, - map_name: &str, - key_value: &Value, - ) -> Result { - self.fetch_entry_unknown_descriptor(contract_identifier, map_name, key_value) - .map(|v| { - v.expect_optional() - .expect("Expected fetch_entry to return a value") - }) - } - pub fn fetch_entry_unknown_descriptor( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -1417,16 +1400,6 @@ impl<'a> ClarityDatabase<'a> { }) } - pub fn delete_entry_unknown_descriptor( - &mut self, - contract_identifier: &QualifiedContractIdentifier, - map_name: &str, - key_value: &Value, - ) -> Result { - let descriptor = self.load_map(contract_identifier, map_name)?; - self.delete_entry(contract_identifier, map_name, key_value, &descriptor) - } - pub fn delete_entry( &mut self, contract_identifier: &QualifiedContractIdentifier, diff --git a/clarity/src/vm/types/mod.rs b/clarity/src/vm/types/mod.rs index 2ee062319..e36cd5a21 100644 --- a/clarity/src/vm/types/mod.rs +++ b/clarity/src/vm/types/mod.rs @@ -1359,13 +1359,6 @@ impl TupleData { self.data_map.len() as u64 } - /// This is like `from_data` for constructing a tuple, but is to be used in contexts - /// where the constructed tuple is *known* to be a valid Clarity value (e.g., static contexts) - /// This panics if the tuple is not a valid Clarity value. - pub fn from_data_static(data: Vec<(ClarityName, Value)>) -> TupleData { - Self::from_data(data).expect("FATAL: static Clarity tuple initialization failed") - } - pub fn from_data(mut data: Vec<(ClarityName, Value)>) -> Result { let mut type_map = BTreeMap::new(); let mut data_map = BTreeMap::new(); diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 2aa208745..596a88118 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -1637,66 +1637,6 @@ impl<'a> SortitionHandleConn<'a> { }) } - /// is the given block a descendant of `potential_ancestor`? - /// * block_at_burn_height: the burn height of the sortition that chose the stacks block to check - /// * potential_ancestor: the stacks block hash of the potential ancestor - pub fn descended_from( - &mut self, - block_at_burn_height: u64, - potential_ancestor: &BlockHeaderHash, - ) -> Result { - let earliest_block_height = self.conn().query_row( - "SELECT block_height FROM snapshots WHERE winning_stacks_block_hash = ? ORDER BY block_height ASC LIMIT 1", - &[potential_ancestor], - |row| Ok(u64::from_row(row).expect("Expected u64 in database")))?; - - let mut sn = self - .get_block_snapshot_by_height(block_at_burn_height)? - .ok_or_else(|| { - test_debug!("No snapshot at height {}", block_at_burn_height); - db_error::NotFoundError - })?; - - while sn.block_height >= earliest_block_height { - if !sn.sortition { - return Ok(false); - } - if &sn.winning_stacks_block_hash == potential_ancestor { - return Ok(true); - } - - // step back to the parent - match SortitionDB::get_block_commit_parent_sortition_id( - self.conn(), - &sn.winning_block_txid, - &sn.sortition_id, - )? { - Some(parent_sortition_id) => { - // we have the block_commit parent memoization data - test_debug!( - "Parent sortition of {} memoized as {}", - &sn.winning_block_txid, - &parent_sortition_id - ); - sn = SortitionDB::get_block_snapshot(self.conn(), &parent_sortition_id)? - .ok_or_else(|| db_error::NotFoundError)?; - } - None => { - // we do not have the block_commit parent memoization data - // step back to the parent - test_debug!("No parent sortition memo for {}", &sn.winning_block_txid); - let block_commit = - get_block_commit_by_txid(&self.conn(), &sn.winning_block_txid)? - .expect("CORRUPTION: winning block commit for snapshot not found"); - sn = self - .get_block_snapshot_by_height(block_commit.parent_block_ptr as u64)? - .ok_or_else(|| db_error::NotFoundError)?; - } - } - } - return Ok(false); - } - fn get_tip_indexed(&self, key: &str) -> Result, db_error> { self.get_indexed(&self.context.chain_tip, key) } diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index fe241a66d..54a58b4cc 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -34,6 +34,7 @@ use crate::util_lib::strings::VecDisplay; use clarity::codec::StacksMessageCodec; use clarity::types::chainstate::BlockHeaderHash; use clarity::util::hash::to_hex; +use clarity::vm::clarity::TransactionConnection; use clarity::vm::contexts::ContractContext; use clarity::vm::costs::{ cost_functions::ClarityCostFunction, ClarityCostFunctionReference, CostStateSummary, @@ -131,31 +132,6 @@ pub fn make_contract_id(addr: &StacksAddress, name: &str) -> QualifiedContractId ) } -/// Extract a PoX address from its tuple representation -fn tuple_to_pox_addr(tuple_data: TupleData) -> (AddressHashMode, Hash160) { - let version_value = tuple_data - .get("version") - .expect("FATAL: no 'version' field in pox-addr") - .to_owned(); - let hashbytes_value = tuple_data - .get("hashbytes") - .expect("FATAL: no 'hashbytes' field in pox-addr") - .to_owned(); - - let version_u8 = version_value.expect_buff_padded(1, 0)[0]; - let version: AddressHashMode = version_u8 - .try_into() - .expect("FATAL: PoX version is not a supported version byte"); - - let hashbytes_vec = hashbytes_value.expect_buff_padded(20, 0); - - let mut hashbytes_20 = [0u8; 20]; - hashbytes_20.copy_from_slice(&hashbytes_vec[0..20]); - let hashbytes = Hash160(hashbytes_20); - - (version, hashbytes) -} - pub struct RawRewardSetEntry { pub reward_address: PoxAddress, pub amount_stacked: u128, @@ -240,6 +216,7 @@ impl StacksChainState { None => return Ok(()), }; + let sender_addr = PrincipalData::from(boot::boot_code_addr(clarity.is_mainnet())); let pox_contract = boot::boot_code_id(POX_2_NAME, clarity.is_mainnet()); for (principal, amount_locked) in cycle_info.missed_reward_slots.iter() { @@ -260,206 +237,34 @@ impl StacksChainState { balance.accelerate_unlock(); balance.save(); - - // get the user's stacking-state entry so that we can update the reward-cycle entries - let lookup_tuple = Value::Tuple(TupleData::from_data_static(vec![("stacker".into(), principal.clone().into())])); - let stacking_state_entry = db - .expect_fetch_entry( - &pox_contract, - "stacking-state", - &lookup_tuple - ).unwrap() - .expect_tuple(); - - let first_reward_cycle_locked = stacking_state_entry - .get("first-reward-cycle") - .expect("Malformed return tuple from stacking-state") - .clone() - .expect_u128(); - if (cycle_number as u128) < first_reward_cycle_locked { - panic!("Unlocking for a cycle before this stacker has stacked"); - } - - let skip_reward_cycle_resets = (cycle_number as u128) - first_reward_cycle_locked; - - let reward_set_indexes = stacking_state_entry.get("reward-set-indexes") - .expect("Malformed return tuple from stacking-state") - .clone() - .expect_list(); - for (reward_set_offset, reward_set_index) in reward_set_indexes.into_iter().enumerate() { - if (reward_set_offset as u128) < skip_reward_cycle_resets { - continue - } - // zero out `reward-cycle-pox-address-list` entries and update `reward-cycle-total-stacked` - let reward_cycle_to_update = (reward_set_offset as u128) + (cycle_number as u128); - // this is the index of the entry we want to remove from the list - let target_entry_index = reward_set_index.expect_u128(); - - let target_key: Value = TupleData::from_data_static(vec![ - ("reward-cycle".into(), Value::UInt(reward_cycle_to_update)), - ("index".into(), Value::UInt(target_entry_index)) - ]).into(); - - let reward_cycle_entry = db.expect_fetch_entry( - &pox_contract, - "reward-cycle-pox-address-list", - &target_key - ).unwrap().expect_tuple(); - - let reward_cycle_entry_principal = reward_cycle_entry - .get("stacker") - .expect("Malformed tuple returned by PoX contract") - .clone() - .expect_optional() - .expect("Reward set entry for auto-unlock should have associated stacker") - .expect_principal(); - - assert_eq!(&reward_cycle_entry_principal, principal); - - let reward_cycle_entry_total_ustx = reward_cycle_entry - .get("total-ustx") - .expect("Malformed tuple returned by PoX contract") - .clone() - .expect_u128(); - - // compress the list: - // (a) move the last entry in `reward-cycle-pox-address-list` to this index - let reward_cycle_len_key = TupleData::from_data_static(vec![("reward-cycle".into(), Value::UInt(reward_cycle_to_update))]).into(); - let last_cycle_entry_index = db - .expect_fetch_entry( - &pox_contract, - "reward-cycle-pox-address-list-len", - &reward_cycle_len_key, - ).unwrap() - .expect_tuple() - .get("len") - .expect("Malformed tuple returned by PoX contract") - .clone() - .expect_u128() - .checked_sub(1) - .expect("Reward set size was 0 even though we unlocked an existing entry"); - - let move_reward_cycle_map_key: Value = TupleData::from_data_static(vec![ - ("reward-cycle".into(), Value::UInt(reward_cycle_to_update)), - ("index".into(), Value::UInt(last_cycle_entry_index)) - ]).into(); - // only need to move if the entry we want to remove is not the last entry - if last_cycle_entry_index != target_entry_index { - let move_reward_cycle_entry = db.expect_fetch_entry( - &pox_contract, - "reward-cycle-pox-address-list", - &move_reward_cycle_map_key - ).unwrap().expect_tuple(); - - let stacker = move_reward_cycle_entry - .get("stacker") - .expect("Malformed tuple return by PoX contract") - .clone() - .expect_optional(); - - // overwrite the targeted entry with the last entry in the list - db.set_entry_unknown_descriptor( - &pox_contract, - "reward-cycle-pox-address-list", - target_key, - move_reward_cycle_entry.into() - ).unwrap(); - - // if the last entry in `reward-cycle-pox-address-list` had an associated stacker, - // we must also update that stacker's `stacking-state` - if let Some(stacker_val) = stacker { - let moved_stacker = stacker_val.expect_principal(); - // load the `stacking-state` - let moved_state_key = TupleData::from_data_static(vec![("stacker".into(), moved_stacker.clone().into())]).into(); - let mut moved_state_entry = db - .expect_fetch_entry( - &pox_contract, - "stacking-state", - &moved_state_key, - ).unwrap() - .expect_tuple(); - // calculate the index into the reward-set-indexes list that - // this reward cycle is at - let moved_cycle_index: usize = reward_cycle_to_update - .checked_sub( - moved_state_entry - .get("first-reward-cycle") - .expect("Malformed tuple return by PoX contract") - .clone() - .expect_u128() - ) - .expect("FATAL: Moved a reward set entry for a stacker whose first-reward-cycle was after the current cycle") - .try_into() - .expect("FATAL: list size is greater than usize"); - - // update the list of reward set indexes - let mut moved_reward_indexes = moved_state_entry - .get("reward-set-indexes") - .expect("Malformed tuple return by PoX contract") - .clone() - .expect_list(); - assert!(moved_reward_indexes.len() > moved_cycle_index, "FATAL: Calculated bad move index"); - // we moved the entry to the "target" location - moved_reward_indexes[moved_cycle_index] = Value::UInt(target_entry_index); - - moved_state_entry.data_map.insert("reward-set-indexes".into(), - Value::list_from(moved_reward_indexes) - .expect("Failed to reconstruct Clarity list")); - // store the new state back into the stacking-state map - db.set_entry_unknown_descriptor( - &pox_contract, - "stacking-state", - moved_state_key, - moved_state_entry.into() - ).unwrap(); - } - } - - // always delete the last entry and decrement the list length - - db.delete_entry_unknown_descriptor( - &pox_contract, - "reward-cycle-pox-address-list", - &move_reward_cycle_map_key, - ).unwrap(); - - db.set_entry_unknown_descriptor( - &pox_contract, "reward-cycle-pox-address-list-len", reward_cycle_len_key, - TupleData::from_data_static(vec![("len".into(), Value::UInt(last_cycle_entry_index))]).into() - ).unwrap(); - - // Finally, update `reward-cycle-total-stacked` - let total_stacked_key = TupleData::from_data_static(vec![("reward-cycle".into(), Value::UInt(reward_cycle_to_update))]) - .into(); - let next_total_stacked_amount = db - .expect_fetch_entry( - &pox_contract, - "reward-cycle-total-stacked", - &total_stacked_key, - ).unwrap() - .expect_tuple() - .get_owned("total-ustx") - .expect("Malformed tuple returned by PoX contract") - .expect_u128() - .checked_sub(reward_cycle_entry_total_ustx) - .expect("FATAL: Unlocked more STX in a cycle than were stacked in that cycle"); - db.set_entry_unknown_descriptor( - &pox_contract, - "reward-cycle-total-stacked", - total_stacked_key, - TupleData::from_data_static(vec![("total-ustx".into(), Value::UInt(next_total_stacked_amount))]).into(), - ).unwrap(); - } - - // Now that we've cleaned up all the reward set entries for the user, delete the user's stacking-state - db.delete_entry_unknown_descriptor( - &pox_contract, - "stacking-state", - &lookup_tuple - ).unwrap(); - Ok(()) - })?; + }).expect("FATAL: failed to accelerate PoX unlock"); + + let result = clarity + .with_abort_callback( + |vm_env| { + vm_env.execute_in_env(sender_addr.clone(), None, None, |env| { + env.execute_contract_allow_private( + &pox_contract, + "handle-unlock", + &[ + SymbolicExpression::atom_value(principal.clone().into()), + SymbolicExpression::atom_value(Value::UInt(*amount_locked)), + SymbolicExpression::atom_value(Value::UInt( + cycle_number.into(), + )), + ], + false, + ) + }) + }, + |_, _| false, + ) + .expect("FATAL: failed to handle PoX unlock"); + + info!("Handled principal unlock"; + "principal" => %principal, + "result" => %result.0); } Ok(()) diff --git a/src/chainstate/stacks/boot/pox-2.clar b/src/chainstate/stacks/boot/pox-2.clar index c7838167d..dc48e16ee 100644 --- a/src/chainstate/stacks/boot/pox-2.clar +++ b/src/chainstate/stacks/boot/pox-2.clar @@ -1,6 +1,7 @@ ;; The .pox-2 contract ;; Error codes (define-constant ERR_STACKING_UNREACHABLE 255) +(define-constant ERR_STACKING_CORRUPTED_STATE 254) (define-constant ERR_STACKING_INSUFFICIENT_FUNDS 1) (define-constant ERR_STACKING_INVALID_LOCK_PERIOD 2) (define-constant ERR_STACKING_ALREADY_STACKED 3) @@ -263,6 +264,64 @@ (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 }))) + ;; todo: assert eq + (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-inner (print (concat (append (default-to (list) (slice moved-reward-list u0 moved-cycle-index)) + set-index) + (default-to (list) (slice moved-reward-list (+ moved-cycle-index u1) (len moved-reward-list)))))) + (update-list (unwrap-panic (as-max-len? update-list-inner u12)))) + (map-set stacking-state { stacker: moved-stacker } + (merge moved-state { reward-set-indexes: update-list }))) + ;; otherwise, we dont 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)} ))))) + +(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. diff --git a/src/chainstate/stacks/boot/pox_2_tests.rs b/src/chainstate/stacks/boot/pox_2_tests.rs index e46383c3c..3e3db189b 100644 --- a/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/src/chainstate/stacks/boot/pox_2_tests.rs @@ -96,10 +96,9 @@ pub fn get_stacking_state_pox_2( account: &PrincipalData, ) -> Option { with_clarity_db_ro(peer, tip, |db| { - let lookup_tuple = Value::Tuple(TupleData::from_data_static(vec![( - "stacker".into(), - account.clone().into(), - )])); + let lookup_tuple = Value::Tuple( + TupleData::from_data(vec![("stacker".into(), account.clone().into())]).unwrap(), + ); db.fetch_entry_unknown_descriptor( &boot_code_id(boot::POX_2_NAME, false), "stacking-state", @@ -113,16 +112,21 @@ pub fn get_stacking_state_pox_2( /// Get the `cycle_number`'s total stacked amount at the given chaintip pub fn get_reward_cycle_total(peer: &mut TestPeer, tip: &StacksBlockId, cycle_number: u64) -> u128 { with_clarity_db_ro(peer, tip, |db| { - let total_stacked_key = TupleData::from_data_static(vec![( + let total_stacked_key = TupleData::from_data(vec![( "reward-cycle".into(), Value::UInt(cycle_number.into()), )]) + .unwrap() .into(); - db.expect_fetch_entry( + db.fetch_entry_unknown_descriptor( &boot_code_id(boot::POX_2_NAME, false), "reward-cycle-total-stacked", &total_stacked_key, ) + .map(|v| { + v.expect_optional() + .expect("Expected fetch_entry to return a value") + }) .unwrap() .expect_tuple() .get_owned("total-ustx") diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index b07a677ad..b49b2e4d0 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -24,6 +24,7 @@ use std::io::prelude::*; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; +use clarity::types::chainstate::SortitionId; use rand::thread_rng; use rand::Rng; use rand::RngCore; @@ -5062,11 +5063,67 @@ impl StacksChainState { } } + /// Check if current PoX reward cycle (as of `burn_tip_height`) has handled any + /// Clarity VM work necessary at the start of the cycle (i.e., processing of accelerated unlocks + /// for failed stackers). + /// If it has not yet been handled, then perform that work now. + fn check_and_handle_reward_start( + burn_tip_height: u64, + burn_dbconn: &dyn BurnStateDB, + sortition_dbconn: &dyn SortitionDBRef, + clarity_tx: &mut ClarityTx, + chain_tip: &StacksHeaderInfo, + parent_sortition_id: &SortitionId, + ) -> Result<(), Error> { + let mut pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( + burn_tip_height, + burn_dbconn.get_burn_start_height().into(), + burn_dbconn.get_pox_reward_cycle_length().into(), + ).expect("FATAL: Unrecoverable chainstate corruption: Epoch 2.1 code evaluated before first burn block height"); + // Do not try to handle auto-unlocks on pox_reward_cycle 0 + // This cannot even occur in the mainchain, because 2.1 starts much + // after the 1st reward cycle, however, this could come up in mocknets or regtest. + if pox_reward_cycle > 1 { + if Burnchain::is_pre_reward_cycle_start( + burn_dbconn.get_burn_start_height().into(), + burn_tip_height, + burn_dbconn.get_pox_reward_cycle_length().into(), + ) { + pox_reward_cycle -= 1; + } + let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { + Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) + }); + + if !handled { + let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( + parent_sortition_id, + chain_tip.burn_header_height.into(), + pox_reward_cycle, + )?; + clarity_tx.block.as_transaction(|clarity_tx| { + Self::handle_pox_cycle_start(clarity_tx, pox_reward_cycle, pox_start_cycle_info) + })?; + } + } + + Ok(()) + } + /// Called in both follower and miner block assembly paths. + /// /// Returns clarity_tx, list of receipts, microblock execution cost, /// microblock fees, microblock burns, list of microblock tx receipts, /// miner rewards tuples, the stacks epoch id, and a boolean that /// represents whether the epoch transition has been applied. + /// + /// The `burn_dbconn`, `sortition_dbconn`, and `conn` arguments + /// all reference the same sortition database through different + /// interfaces. `burn_dbconn` and `sortition_dbconn` should + /// reference the same object. The reason to provide both is that + /// `SortitionDBRef` captures trait functions that Clarity does + /// not need, and Rust does not support trait upcasting (even + /// though it would theoretically be safe). pub fn setup_block<'a, 'b>( chainstate_tx: &'b mut ChainstateTx, clarity_instance: &'a mut ClarityInstance, @@ -5149,41 +5206,14 @@ impl StacksChainState { debug!("Evaluating block with epoch = {}", evaluated_epoch); if evaluated_epoch >= StacksEpochId::Epoch21 { - let mut pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( + Self::check_and_handle_reward_start( burn_tip_height.into(), - burn_dbconn.get_burn_start_height().into(), - burn_dbconn.get_pox_reward_cycle_length().into(), - ).expect("FATAL: Unrecoverable chainstate corruption: Epoch 2.1 code evaluated before first burn block height"); - // Do not try to handle auto-unlocks on pox_reward_cycle 0 - // This cannot even occur in the mainchain, because 2.1 starts much - // after the 1st reward cycle, however, this could come up in mocknets or regtest. - if pox_reward_cycle > 1 { - if Burnchain::is_pre_reward_cycle_start( - burn_dbconn.get_burn_start_height().into(), - burn_tip_height.into(), - burn_dbconn.get_pox_reward_cycle_length().into(), - ) { - pox_reward_cycle -= 1; - } - let handled = clarity_tx.with_clarity_db_readonly(|clarity_db| { - Self::handled_pox_cycle_start(clarity_db, pox_reward_cycle) - }); - - if !handled { - let pox_start_cycle_info = sortition_dbconn.get_pox_start_cycle_info( - &parent_sortition_id, - chain_tip.burn_header_height.into(), - pox_reward_cycle, - )?; - clarity_tx.block.as_transaction(|clarity_tx| { - Self::handle_pox_cycle_start( - clarity_tx, - pox_reward_cycle, - pox_start_cycle_info, - ) - })?; - } - } + burn_dbconn, + sortition_dbconn, + &mut clarity_tx, + chain_tip, + &parent_sortition_id, + )?; } let matured_miner_rewards_opt = match StacksChainState::find_mature_miner_rewards( diff --git a/src/clarity_vm/database/mod.rs b/src/clarity_vm/database/mod.rs index 53f592937..933bac184 100644 --- a/src/clarity_vm/database/mod.rs +++ b/src/clarity_vm/database/mod.rs @@ -2,7 +2,7 @@ use clarity::vm::types::PrincipalData; use rusqlite::{Connection, OptionalExtension}; use crate::chainstate::burn::db::sortdb::{ - get_ancestor_sort_id, get_ancestor_sort_id_tx, SortitionDB, SortitionDBConn, + get_ancestor_sort_id, get_ancestor_sort_id_tx, SortitionDB, SortitionDBConn, SortitionHandle, SortitionHandleConn, SortitionHandleTx, }; use crate::chainstate::stacks::boot::PoxStartCycleInfo; From 266caf2ba2c10e8085106cb107889aba26afe5d2 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 31 Aug 2022 16:26:13 -0500 Subject: [PATCH 10/25] make auto-unlocks free for block limit, more PR feedback --- src/burnchains/burnchain.rs | 9 +++++--- src/chainstate/stacks/boot/mod.rs | 21 +++++++++++------- src/chainstate/stacks/boot/pox-2.clar | 23 +++++++++++++------- src/chainstate/stacks/boot/pox_2_tests.rs | 3 ++- src/chainstate/stacks/db/blocks.rs | 9 ++++---- src/clarity_vm/clarity.rs | 26 +++++++++++++++++++++++ 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index 4d752abe3..d937f8d8e 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -492,9 +492,12 @@ impl Burnchain { ) } - /// Is this block the block in a reward cycle right before the reward phase - /// starts? This is the mod 0 block. - pub fn is_pre_reward_cycle_start( + /// Is this block either the first block in a reward cycle or + /// right before the reward phase starts? This is the mod 0 or mod 1 + /// block. Reward cycle start events (like auto-unlocks) process *after* + /// the first reward block, so this function is used to determine when + /// that has passed. + pub fn is_before_reward_cycle( first_block_ht: u64, burn_ht: u64, reward_cycle_length: u64, diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 54a58b4cc..4ea5a2956 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -42,6 +42,7 @@ use clarity::vm::costs::{ use clarity::vm::database::ClarityDatabase; use clarity::vm::database::{NULL_BURN_STATE_DB, NULL_HEADER_DB}; use clarity::vm::errors::InterpreterError; +use clarity::vm::events::StacksTransactionEvent; use clarity::vm::representations::ClarityName; use clarity::vm::representations::ContractName; use clarity::vm::types::{ @@ -206,19 +207,20 @@ impl StacksChainState { clarity: &mut ClarityTransactionConnection, cycle_number: u64, cycle_info: Option, - ) -> Result<(), Error> { + ) -> Result, Error> { clarity.with_clarity_db(|db| Ok(Self::mark_pox_cycle_handled(db, cycle_number)))?; debug!("Handling PoX reward cycle start"; "reward_cycle" => cycle_number, "cycle_active" => cycle_info.is_some()); let cycle_info = match cycle_info { Some(x) => x, - None => return Ok(()), + None => return Ok(vec![]), }; let sender_addr = PrincipalData::from(boot::boot_code_addr(clarity.is_mainnet())); let pox_contract = boot::boot_code_id(POX_2_NAME, clarity.is_mainnet()); + let mut total_events = vec![]; for (principal, amount_locked) in cycle_info.missed_reward_slots.iter() { // we have to do several things for each principal // 1. lookup their Stacks account and accelerate their unlock @@ -240,7 +242,7 @@ impl StacksChainState { Ok(()) }).expect("FATAL: failed to accelerate PoX unlock"); - let result = clarity + let (result, _, events, _) = clarity .with_abort_callback( |vm_env| { vm_env.execute_in_env(sender_addr.clone(), None, None, |env| { @@ -262,12 +264,12 @@ impl StacksChainState { ) .expect("FATAL: failed to handle PoX unlock"); - info!("Handled principal unlock"; - "principal" => %principal, - "result" => %result.0); + result.expect_result_ok(); + + total_events.extend(events.into_iter()); } - Ok(()) + Ok(total_events) } fn eval_boot_code_read_only( @@ -669,7 +671,10 @@ impl StacksChainState { let stacker = tuple .get("stacker") - .expect(&format!("FATAL: no 'total-ustx' in return value from (get-reward-set-pox-address u{} u{})", reward_cycle, i)) + .expect(&format!( + "FATAL: no 'stacker' in return value from (get-reward-set-pox-address u{} u{})", + reward_cycle, i + )) .to_owned() .expect_optional() .map(|value| value.expect_principal()); diff --git a/src/chainstate/stacks/boot/pox-2.clar b/src/chainstate/stacks/boot/pox-2.clar index dc48e16ee..4ac563ff0 100644 --- a/src/chainstate/stacks/boot/pox-2.clar +++ b/src/chainstate/stacks/boot/pox-2.clar @@ -264,6 +264,13 @@ (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 (set-uint-at (in-list (list 12 uint)) (index uint) (value uint)) + (unwrap-panic (as-max-len? + (concat (append (default-to (list) (slice in-list u0 index)) + value) + (default-to (list) (slice in-list (+ u1 index) (len in-list)))) + u12))) + (define-private (fold-unlock-reward-cycle (set-index uint) (data-res (response { cycle: uint, first-unlocked-cycle: uint, @@ -275,7 +282,6 @@ ;; 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 }))) - ;; todo: assert eq (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))) @@ -293,10 +299,7 @@ (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-inner (print (concat (append (default-to (list) (slice moved-reward-list u0 moved-cycle-index)) - set-index) - (default-to (list) (slice moved-reward-list (+ moved-cycle-index u1) (len moved-reward-list)))))) - (update-list (unwrap-panic (as-max-len? update-list-inner u12)))) + (update-list (set-uint-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 dont need to update stacking-state after move @@ -312,6 +315,9 @@ 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)) @@ -379,12 +385,13 @@ (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11)) (results (fold add-pox-addr-to-ith-reward-cycle cycle-indexes { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, - reward-set-indexes: (list), amount-ustx: amount-ustx, i: u0, stacker: (some stacker) }))) + reward-set-indexes: (list), amount-ustx: amount-ustx, i: u0, stacker: (some stacker) })) + (reward-set-indexes (get reward-set-indexes results))) ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. ;; It _should_ be equal to num-cycles. (asserts! (is-eq num-cycles (get i results)) (err ERR_STACKING_UNREACHABLE)) - (asserts! (is-eq num-cycles (len (get reward-set-indexes results))) (err ERR_STACKING_UNREACHABLE)) - (ok (get reward-set-indexes results)))) + (asserts! (is-eq num-cycles (len reward-set-indexes)) (err ERR_STACKING_UNREACHABLE)) + (ok reward-set-indexes))) (define-private (add-pox-partial-stacked-to-ith-cycle (cycle-index uint) diff --git a/src/chainstate/stacks/boot/pox_2_tests.rs b/src/chainstate/stacks/boot/pox_2_tests.rs index 3e3db189b..fcf914829 100644 --- a/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/src/chainstate/stacks/boot/pox_2_tests.rs @@ -795,6 +795,7 @@ fn test_simple_pox_2_auto_unlock(alice_first: bool) { let alice_address = key_to_stacks_addr(&alice); let bob_address = key_to_stacks_addr(&bob); + let charlie_address = key_to_stacks_addr(&charlie); let blocks = observer.get_blocks(); let mut alice_txs = HashMap::new(); @@ -813,7 +814,7 @@ fn test_simple_pox_2_auto_unlock(alice_first: bool) { alice_txs.insert(t.auth.get_origin_nonce(), r); } else if addr == bob_address { bob_txs.insert(t.auth.get_origin_nonce(), r); - } else if addr == key_to_stacks_addr(&charlie) { + } else if addr == charlie_address { assert!( r.execution_cost != ExecutionCost::zero(), "Execution cost is not zero!" diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index b49b2e4d0..8adf4885e 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -5074,7 +5074,7 @@ impl StacksChainState { clarity_tx: &mut ClarityTx, chain_tip: &StacksHeaderInfo, parent_sortition_id: &SortitionId, - ) -> Result<(), Error> { + ) -> Result, Error> { let mut pox_reward_cycle = Burnchain::static_block_height_to_reward_cycle( burn_tip_height, burn_dbconn.get_burn_start_height().into(), @@ -5084,7 +5084,7 @@ impl StacksChainState { // This cannot even occur in the mainchain, because 2.1 starts much // after the 1st reward cycle, however, this could come up in mocknets or regtest. if pox_reward_cycle > 1 { - if Burnchain::is_pre_reward_cycle_start( + if Burnchain::is_before_reward_cycle( burn_dbconn.get_burn_start_height().into(), burn_tip_height, burn_dbconn.get_pox_reward_cycle_length().into(), @@ -5101,13 +5101,14 @@ impl StacksChainState { chain_tip.burn_header_height.into(), pox_reward_cycle, )?; - clarity_tx.block.as_transaction(|clarity_tx| { + let events = clarity_tx.block.as_free_transaction(|clarity_tx| { Self::handle_pox_cycle_start(clarity_tx, pox_reward_cycle, pox_start_cycle_info) })?; + return Ok(events); } } - Ok(()) + Ok(vec![]) } /// Called in both follower and miner block assembly paths. diff --git a/src/clarity_vm/clarity.rs b/src/clarity_vm/clarity.rs index ac8ed833f..3e50d536a 100644 --- a/src/clarity_vm/clarity.rs +++ b/src/clarity_vm/clarity.rs @@ -942,6 +942,32 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> { } } + /// Execute `todo` as a transaction in this block. The execution + /// will use the "free" cost tracker. + /// This will unconditionally commit the edit log from the + /// transaction to the block, so any changes that should be + /// rolled back must be rolled back by `todo`. + pub fn as_free_transaction(&mut self, todo: F) -> R + where + F: FnOnce(&mut ClarityTransactionConnection) -> R, + { + // use the `using!` statement to ensure that the old cost_tracker is placed + // back in all branches after initialization + using!(self.cost_track, "cost tracker", |old_cost_tracker| { + // epoch initialization is *free* + self.cost_track.replace(LimitedCostTracker::new_free()); + + let mut tx = self.start_transaction_processing(); + let r = todo(&mut tx); + tx.commit(); + (old_cost_tracker, r) + }) + } + + /// Execute `todo` as a transaction in this block. + /// This will unconditionally commit the edit log from the + /// transaction to the block, so any changes that should be + /// rolled back must be rolled back by `todo`. pub fn as_transaction(&mut self, todo: F) -> R where F: FnOnce(&mut ClarityTransactionConnection) -> R, From e212b6d1e1b335320472c26db4e7501bfaaf04ba Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 2 Sep 2022 11:43:40 -0500 Subject: [PATCH 11/25] address PR feedback --- clarity/src/vm/database/structures.rs | 12 +++--------- src/chainstate/stacks/db/blocks.rs | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index 89507523d..f3727a734 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -485,15 +485,9 @@ impl<'db, 'conn> STXBalanceSnapshot<'db, 'conn> { let new_unlock_height = self.burn_block_height + 1; self.balance = match self.balance { STXBalance::Unlocked { amount } => STXBalance::Unlocked { amount }, - STXBalance::LockedPoxOne { - amount_unlocked, - amount_locked, - .. - } => STXBalance::LockedPoxOne { - amount_unlocked, - amount_locked, - unlock_height: new_unlock_height, - }, + STXBalance::LockedPoxOne { .. } => { + unreachable!("Attempted to accelerate the unlock of a lockup created by PoX-1") + } STXBalance::LockedPoxTwo { amount_unlocked, amount_locked, diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 8adf4885e..63f8ffd03 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -5205,18 +5205,6 @@ impl StacksChainState { let evaluated_epoch = clarity_tx.get_epoch(); clarity_tx.reset_cost(parent_block_cost.clone()); - debug!("Evaluating block with epoch = {}", evaluated_epoch); - if evaluated_epoch >= StacksEpochId::Epoch21 { - Self::check_and_handle_reward_start( - burn_tip_height.into(), - burn_dbconn, - sortition_dbconn, - &mut clarity_tx, - chain_tip, - &parent_sortition_id, - )?; - } - let matured_miner_rewards_opt = match StacksChainState::find_mature_miner_rewards( &mut clarity_tx, conn, @@ -5290,6 +5278,18 @@ impl StacksChainState { // epoch defined by this miner. clarity_tx.reset_cost(ExecutionCost::zero()); + debug!("Evaluating block with epoch = {}", evaluated_epoch); + if evaluated_epoch >= StacksEpochId::Epoch21 { + Self::check_and_handle_reward_start( + burn_tip_height.into(), + burn_dbconn, + sortition_dbconn, + &mut clarity_tx, + chain_tip, + &parent_sortition_id, + )?; + } + // is this stacks block the first of a new epoch? let (applied_epoch_transition, mut tx_receipts) = StacksChainState::process_epoch_transition(&mut clarity_tx, burn_tip_height)?; From c35f35a6ce3107735aaeb269a861e0117d85521a Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sat, 3 Sep 2022 17:59:17 +0200 Subject: [PATCH 12/25] feat: return PoX-2 info in /v2/pox RPC response --- src/net/mod.rs | 4 ++++ src/net/rpc.rs | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index cf5753ad5..7582f6cb4 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1093,6 +1093,10 @@ pub struct RPCPoxInfoData { pub reward_cycle_length: u64, pub rejection_votes_left_required: u64, pub next_reward_cycle_in: u64, + + // Stacks 2.1 / PoX-2 info + pub pox_2_activation_burnchain_block_height: u64, + pub pox_2_first_cycle_id: u64, } /// Headers response payload diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 5aa330a8e..b973ac779 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -268,17 +268,42 @@ impl RPCPoxInfoData { ) -> Result { let mainnet = chainstate.mainnet; let chain_id = chainstate.chain_id; - let contract_identifier = boot_code_id("pox", mainnet); + + let current_burn_height = chainstate.maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { + clarity_tx.with_clarity_db_readonly(|clarity_db| { + clarity_db.get_current_burnchain_block_height() as u64 + }) + }).map_err(|_| net_error::NotFoundError)?.ok_or_else(|| net_error::NotFoundError)?; + + let reward_cycle = burnchain + .block_height_to_reward_cycle(current_burn_height) + .ok_or_else(|| net_error::ChainstateError("PoxNoRewardCycle".to_string()))?; + let reward_cycle_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); + let pox_contract_name = burnchain + .pox_constants + .active_pox_contract(reward_cycle_start_height); + let clarity_ver = match pox_contract_name { + POX_2_NAME => ClarityVersion::Clarity1, + POX_2_NAME => ClarityVersion::Clarity2, + _ => return Err(net_error::ChainstateError(format!("Unexpected PoX contract: {}", pox_contract_name))) + }; + + let contract_identifier = boot_code_id(pox_contract_name, mainnet); let function = "get-pox-info"; let cost_track = LimitedCostTracker::new_free(); let sender = PrincipalData::Standard(StandardPrincipalData::transient()); + let pox_2_first_cycle = burnchain + .block_height_to_reward_cycle(burnchain.pox_constants.v1_unlock_height as u64) + .ok_or(net_error::ChainstateError("PoX-2 first reward cycle begins before first burn block height".to_string()))? + + 1; + let data = chainstate .maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { clarity_tx.with_readonly_clarity_env( mainnet, chain_id, - ClarityVersion::Clarity1, + clarity_ver, sender, None, cost_track, @@ -420,7 +445,7 @@ impl RPCPoxInfoData { let cur_cycle_pox_active = sortdb.is_pox_active(burnchain, &burnchain_tip)?; Ok(RPCPoxInfoData { - contract_id: boot_code_id("pox", chainstate.mainnet).to_string(), + contract_id: boot_code_id(next_cycle_pox_contract, chainstate.mainnet).to_string(), pox_activation_threshold_ustx, first_burnchain_block_height, prepare_phase_block_length: prepare_cycle_length, @@ -451,6 +476,8 @@ impl RPCPoxInfoData { reward_cycle_length, rejection_votes_left_required, next_reward_cycle_in, + pox_2_activation_burnchain_block_height: burnchain.pox_constants.v1_unlock_height as u64, + pox_2_first_cycle_id: pox_2_first_cycle, }) } } From 0c90ae8ee2bbc06d0344006d623a59be171e1675 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sat, 3 Sep 2022 18:06:33 +0200 Subject: [PATCH 13/25] chore: both PoX-1 and PoX-2 `get-pox-info` fn can be called within a Clarity1 context --- src/net/rpc.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/net/rpc.rs b/src/net/rpc.rs index b973ac779..b10fe0cff 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -282,11 +282,6 @@ impl RPCPoxInfoData { let pox_contract_name = burnchain .pox_constants .active_pox_contract(reward_cycle_start_height); - let clarity_ver = match pox_contract_name { - POX_2_NAME => ClarityVersion::Clarity1, - POX_2_NAME => ClarityVersion::Clarity2, - _ => return Err(net_error::ChainstateError(format!("Unexpected PoX contract: {}", pox_contract_name))) - }; let contract_identifier = boot_code_id(pox_contract_name, mainnet); let function = "get-pox-info"; @@ -303,7 +298,7 @@ impl RPCPoxInfoData { clarity_tx.with_readonly_clarity_env( mainnet, chain_id, - clarity_ver, + ClarityVersion::Clarity1, sender, None, cost_track, From acaf7fcaf571b6de9fd4f96af97a24ee5c3d6f8f Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sat, 3 Sep 2022 18:27:54 +0200 Subject: [PATCH 14/25] chore: lint --- src/net/rpc.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/net/rpc.rs b/src/net/rpc.rs index b10fe0cff..510982b4b 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -269,15 +269,18 @@ impl RPCPoxInfoData { let mainnet = chainstate.mainnet; let chain_id = chainstate.chain_id; - let current_burn_height = chainstate.maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { - clarity_tx.with_clarity_db_readonly(|clarity_db| { - clarity_db.get_current_burnchain_block_height() as u64 + let current_burn_height = chainstate + .maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { + clarity_tx.with_clarity_db_readonly(|clarity_db| { + clarity_db.get_current_burnchain_block_height() as u64 + }) }) - }).map_err(|_| net_error::NotFoundError)?.ok_or_else(|| net_error::NotFoundError)?; + .map_err(|_| net_error::NotFoundError)? + .ok_or(net_error::NotFoundError)?; let reward_cycle = burnchain .block_height_to_reward_cycle(current_burn_height) - .ok_or_else(|| net_error::ChainstateError("PoxNoRewardCycle".to_string()))?; + .ok_or(net_error::ChainstateError("PoxNoRewardCycle".to_string()))?; let reward_cycle_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); let pox_contract_name = burnchain .pox_constants @@ -290,7 +293,9 @@ impl RPCPoxInfoData { let pox_2_first_cycle = burnchain .block_height_to_reward_cycle(burnchain.pox_constants.v1_unlock_height as u64) - .ok_or(net_error::ChainstateError("PoX-2 first reward cycle begins before first burn block height".to_string()))? + .ok_or(net_error::ChainstateError( + "PoX-2 first reward cycle begins before first burn block height".to_string(), + ))? + 1; let data = chainstate @@ -471,7 +476,8 @@ impl RPCPoxInfoData { reward_cycle_length, rejection_votes_left_required, next_reward_cycle_in, - pox_2_activation_burnchain_block_height: burnchain.pox_constants.v1_unlock_height as u64, + pox_2_activation_burnchain_block_height: burnchain.pox_constants.v1_unlock_height + as u64, pox_2_first_cycle_id: pox_2_first_cycle, }) } From 462345ce9a382bb7d800e7dfdb021cdadfb800d6 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sun, 4 Sep 2022 09:52:37 +0200 Subject: [PATCH 15/25] chore: remove unnecessary unconfirmed microblock query in /v2/pox lookup --- src/net/rpc.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 510982b4b..004a2836b 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -270,12 +270,11 @@ impl RPCPoxInfoData { let chain_id = chainstate.chain_id; let current_burn_height = chainstate - .maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { + .with_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { clarity_tx.with_clarity_db_readonly(|clarity_db| { clarity_db.get_current_burnchain_block_height() as u64 }) }) - .map_err(|_| net_error::NotFoundError)? .ok_or(net_error::NotFoundError)?; let reward_cycle = burnchain From 654ad21aab1138cd6acc73af80bd9fdce23ce999 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Sun, 4 Sep 2022 10:29:57 +0200 Subject: [PATCH 16/25] feat: separate array objects for each PoX contract version in /v2/pox response --- src/net/mod.rs | 12 +++++++++--- src/net/rpc.rs | 26 ++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index 7582f6cb4..1f5bcc424 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1072,6 +1072,13 @@ pub struct RPCPoxNextCycleInfo { pub ustx_until_pox_rejection: u64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RPCPoxContractVersion { + pub contract_id: String, + pub activation_burnchain_block_height: u64, + pub first_reward_cycle_id: u64, +} + /// The data we return on GET /v2/pox #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RPCPoxInfoData { @@ -1094,9 +1101,8 @@ pub struct RPCPoxInfoData { pub rejection_votes_left_required: u64, pub next_reward_cycle_in: u64, - // Stacks 2.1 / PoX-2 info - pub pox_2_activation_burnchain_block_height: u64, - pub pox_2_first_cycle_id: u64, + // Information specific to each PoX contract version + pub contract_versions: Vec, } /// Headers response payload diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 004a2836b..b9a99aa35 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -89,7 +89,7 @@ use crate::net::{ use crate::net::{BlocksData, GetIsTraitImplementedResponse}; use crate::net::{ClientError, TipRequest}; use crate::net::{RPCNeighbor, RPCNeighborsInfo}; -use crate::net::{RPCPeerInfoData, RPCPoxInfoData}; +use crate::net::{RPCPeerInfoData, RPCPoxContractVersion, RPCPoxInfoData}; use crate::util_lib::db::DBConn; use crate::util_lib::db::Error as db_error; use clarity::vm::database::clarity_store::make_contract_hash_key; @@ -112,6 +112,7 @@ use stacks_common::util::get_epoch_time_secs; use stacks_common::util::hash::Hash160; use stacks_common::util::hash::{hex_bytes, to_hex}; +use crate::chainstate::stacks::boot::{POX_1_NAME, POX_2_NAME}; use crate::chainstate::stacks::StacksBlockHeader; use crate::clarity_vm::database::marf::MarfedKV; use stacks_common::types::chainstate::BlockHeaderHash; @@ -290,6 +291,13 @@ impl RPCPoxInfoData { let cost_track = LimitedCostTracker::new_free(); let sender = PrincipalData::Standard(StandardPrincipalData::transient()); + // Note: should always be 0 unless somehow configured to start later + let pox_1_first_cycle = burnchain + .block_height_to_reward_cycle(burnchain.first_block_height as u64) + .ok_or(net_error::ChainstateError( + "PoX-1 first reward cycle begins before first burn block height".to_string(), + ))?; + let pox_2_first_cycle = burnchain .block_height_to_reward_cycle(burnchain.pox_constants.v1_unlock_height as u64) .ok_or(net_error::ChainstateError( @@ -475,9 +483,19 @@ impl RPCPoxInfoData { reward_cycle_length, rejection_votes_left_required, next_reward_cycle_in, - pox_2_activation_burnchain_block_height: burnchain.pox_constants.v1_unlock_height - as u64, - pox_2_first_cycle_id: pox_2_first_cycle, + contract_versions: vec![ + RPCPoxContractVersion { + contract_id: boot_code_id(POX_1_NAME, chainstate.mainnet).to_string(), + activation_burnchain_block_height: burnchain.first_block_height, + first_reward_cycle_id: pox_1_first_cycle, + }, + RPCPoxContractVersion { + contract_id: boot_code_id(POX_2_NAME, chainstate.mainnet).to_string(), + activation_burnchain_block_height: burnchain.pox_constants.v1_unlock_height + as u64, + first_reward_cycle_id: pox_2_first_cycle, + }, + ], }) } } From 4526cee3fbe01ded3cea8d559b5d5045daba75e9 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Mon, 5 Sep 2022 11:32:46 +0200 Subject: [PATCH 17/25] fix: off-by-one with burn height to active PoX contract --- src/net/rpc.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/net/rpc.rs b/src/net/rpc.rs index b9a99aa35..554be3a39 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -278,13 +278,9 @@ impl RPCPoxInfoData { }) .ok_or(net_error::NotFoundError)?; - let reward_cycle = burnchain - .block_height_to_reward_cycle(current_burn_height) - .ok_or(net_error::ChainstateError("PoxNoRewardCycle".to_string()))?; - let reward_cycle_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); let pox_contract_name = burnchain .pox_constants - .active_pox_contract(reward_cycle_start_height); + .active_pox_contract(current_burn_height); let contract_identifier = boot_code_id(pox_contract_name, mainnet); let function = "get-pox-info"; From 9fd5c772c020f914e6e6cbd03ea4b62f1b01bc2a Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Fri, 9 Sep 2022 16:20:44 +0200 Subject: [PATCH 18/25] chore: use contract id for current pox cycle rather than next cycle --- src/net/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 554be3a39..ff526cf3d 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -448,7 +448,7 @@ impl RPCPoxInfoData { let cur_cycle_pox_active = sortdb.is_pox_active(burnchain, &burnchain_tip)?; Ok(RPCPoxInfoData { - contract_id: boot_code_id(next_cycle_pox_contract, chainstate.mainnet).to_string(), + contract_id: boot_code_id(cur_cycle_pox_contract, chainstate.mainnet).to_string(), pox_activation_threshold_ustx, first_burnchain_block_height, prepare_phase_block_length: prepare_cycle_length, From 9f797abf663b6b779817ce123ed292ee78410473 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Fri, 9 Sep 2022 16:21:49 +0200 Subject: [PATCH 19/25] feat: add `current_burchain_block_height` to /v2/pox response so clients do not have to perform an extra fetch to `/v2/info` for the value during PoX tx construction --- src/net/mod.rs | 1 + src/net/rpc.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/net/mod.rs b/src/net/mod.rs index 1f5bcc424..815192817 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1085,6 +1085,7 @@ pub struct RPCPoxInfoData { pub contract_id: String, pub pox_activation_threshold_ustx: u64, pub first_burnchain_block_height: u64, + pub current_burnchain_block_height: u64, pub prepare_phase_block_length: u64, pub reward_phase_block_length: u64, pub reward_slots: u64, diff --git a/src/net/rpc.rs b/src/net/rpc.rs index ff526cf3d..d9ede602e 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -451,6 +451,7 @@ impl RPCPoxInfoData { contract_id: boot_code_id(cur_cycle_pox_contract, chainstate.mainnet).to_string(), pox_activation_threshold_ustx, first_burnchain_block_height, + current_burnchain_block_height: burnchain_tip.block_height, prepare_phase_block_length: prepare_cycle_length, reward_phase_block_length: reward_cycle_length - prepare_cycle_length, reward_slots, From 98298be54fd8081a1b2ecdb78bc31e62a3ad7f2e Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 9 Sep 2022 10:00:12 -0500 Subject: [PATCH 20/25] PR feedback: panicking subtraction --- src/burnchains/burnchain.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index d937f8d8e..2cfbeaee9 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -502,7 +502,9 @@ impl Burnchain { burn_ht: u64, reward_cycle_length: u64, ) -> bool { - let effective_height = burn_ht - first_block_ht; + let effective_height = burn_ht + .checked_sub(first_block_ht) + .expect("FATAL: attempted to check reward cycle start before first block height"); // first block of the new reward cycle (effective_height % reward_cycle_length) <= 1 } From daf57ba6ea09a7830ab79d9075d96329c515b9bb Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 9 Sep 2022 14:36:37 -0500 Subject: [PATCH 21/25] add comments for reward set handling --- clarity/src/vm/contexts.rs | 6 ++++++ src/chainstate/stacks/boot/mod.rs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 11ac9a1de..640301a70 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -1083,6 +1083,12 @@ impl<'a, 'b> Environment<'a, 'b> { self.inner_execute_contract(contract, tx_name, args, read_only, true) } + /// This method handles actual execution of contract-calls on a contract. + /// + /// `allow_private` should always be set to `false` for user transactions: + /// this ensures that only `define-public` and `define-read-only` methods can + /// be invoked. The `allow_private` mode should only be used by + /// `Environment::execute_contract_allow_private`. fn inner_execute_contract( &mut self, contract_identifier: &QualifiedContractIdentifier, diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 4ea5a2956..7a3d16736 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -141,6 +141,12 @@ pub struct RawRewardSetEntry { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct PoxStartCycleInfo { + /// This data contains the set of principals who missed a reward slot + /// in this reward cycle. + /// + /// The first element of the tuple is the principal whose microSTX + /// were locked, and the second element is the amount of microSTX + /// that were locked pub missed_reward_slots: Vec<(PrincipalData, u128)>, } @@ -411,7 +417,14 @@ impl StacksChainState { if let Some(stacker) = stacker.as_ref() { contributed_stackers.push((stacker.clone(), stacked_amt)); } - // peak at the next address in the set, and see if we need to sum + // Here we check if we should combine any entries with the same + // reward address together in the reward set. + // The outer while loop pops the last element of the + // addresses vector, and here we peak at the last item in + // the vector (via last()). Because the items in the + // vector are sorted by address, we know that any entry + // with the same `reward_address` as `address` will be at the end of + // the list (and therefore found by this loop) while addresses.last().map(|x| &x.reward_address) == Some(&address) { let next_contrib = addresses .pop() From a5a5f94f5f1c5e3876791c1011e8cff90441cc65 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 12 Sep 2022 10:51:47 -0500 Subject: [PATCH 22/25] cargo fmt --- src/burnchains/burnchain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index 6f3105d6d..10bcc9631 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -73,11 +73,11 @@ use crate::util_lib::db::Error as db_error; use stacks_common::address::public_keys_to_address_hash; use stacks_common::address::AddressHashMode; use stacks_common::deps_common::bitcoin::util::hash::Sha256dHash as BitcoinSha256dHash; -use stacks_common::util::{get_epoch_time_ms, sleep_ms}; use stacks_common::util::get_epoch_time_secs; use stacks_common::util::hash::to_hex; use stacks_common::util::log; use stacks_common::util::vrf::VRFPublicKey; +use stacks_common::util::{get_epoch_time_ms, sleep_ms}; use crate::burnchains::bitcoin::indexer::BitcoinIndexer; use crate::chainstate::stacks::boot::POX_2_MAINNET_CODE; From 8ed1b36217582fa5c9572c8cb83b5658df8bf084 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 12 Sep 2022 10:56:59 -0500 Subject: [PATCH 23/25] ci: use rust stable for code coverage tests --- .github/actions/bitcoin-int-tests/Dockerfile.code-cov | 7 +++---- .../bitcoin-int-tests/Dockerfile.generic.bitcoin-tests | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/actions/bitcoin-int-tests/Dockerfile.code-cov b/.github/actions/bitcoin-int-tests/Dockerfile.code-cov index 733f879b7..209b80473 100644 --- a/.github/actions/bitcoin-int-tests/Dockerfile.code-cov +++ b/.github/actions/bitcoin-int-tests/Dockerfile.code-cov @@ -4,13 +4,12 @@ WORKDIR /build ENV CARGO_MANIFEST_DIR="$(pwd)" -RUN rustup override set nightly-2022-01-14 && \ - rustup component add llvm-tools-preview && \ +RUN rustup component add llvm-tools-preview && \ cargo install grcov -ENV RUSTFLAGS="-Zinstrument-coverage" \ +ENV RUSTFLAGS="-Cinstrument-coverage" \ LLVM_PROFILE_FILE="stacks-blockchain-%p-%m.profraw" - + COPY . . RUN cargo build --workspace && \ diff --git a/.github/actions/bitcoin-int-tests/Dockerfile.generic.bitcoin-tests b/.github/actions/bitcoin-int-tests/Dockerfile.generic.bitcoin-tests index 42a0235cf..2fd43a589 100644 --- a/.github/actions/bitcoin-int-tests/Dockerfile.generic.bitcoin-tests +++ b/.github/actions/bitcoin-int-tests/Dockerfile.generic.bitcoin-tests @@ -6,11 +6,10 @@ COPY . . WORKDIR /src/testnet/stacks-node -RUN rustup override set nightly-2022-01-14 && \ - rustup component add llvm-tools-preview && \ +RUN rustup component add llvm-tools-preview && \ cargo install grcov -ENV RUSTFLAGS="-Zinstrument-coverage" \ +ENV RUSTFLAGS="-Cinstrument-coverage" \ LLVM_PROFILE_FILE="stacks-blockchain-%p-%m.profraw" RUN cargo test --no-run && \ From 4270820f43ae2980215318fd7ce163eb3f0f6d2d Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Mon, 12 Sep 2022 11:05:41 -0500 Subject: [PATCH 24/25] ci: use rust stable for code coverage tests (large genesis) --- .github/actions/bitcoin-int-tests/Dockerfile.large-genesis | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actions/bitcoin-int-tests/Dockerfile.large-genesis b/.github/actions/bitcoin-int-tests/Dockerfile.large-genesis index 4f96fd304..1350a6ed8 100644 --- a/.github/actions/bitcoin-int-tests/Dockerfile.large-genesis +++ b/.github/actions/bitcoin-int-tests/Dockerfile.large-genesis @@ -9,11 +9,10 @@ RUN cd / && tar -xvzf bitcoin-0.20.0-x86_64-linux-gnu.tar.gz RUN ln -s /bitcoin-0.20.0/bin/bitcoind /bin/ -RUN rustup override set nightly-2022-01-14 && \ - rustup component add llvm-tools-preview && \ +RUN rustup component add llvm-tools-preview && \ cargo install grcov -ENV RUSTFLAGS="-Zinstrument-coverage" \ +ENV RUSTFLAGS="-Cinstrument-coverage" \ LLVM_PROFILE_FILE="stacks-blockchain-%p-%m.profraw" RUN cargo test --no-run --workspace && \ From c23c8fa4ec2f7f471ced76defbf528cc5e171425 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Tue, 13 Sep 2022 15:58:30 +0200 Subject: [PATCH 25/25] docs: update OpenAPI schema with the /v2/pox changes --- docs/rpc/api/core-node/get-pox.example.json | 15 ++++++- docs/rpc/api/core-node/get-pox.schema.json | 43 ++++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/rpc/api/core-node/get-pox.example.json b/docs/rpc/api/core-node/get-pox.example.json index 6be67ccec..2f8815481 100644 --- a/docs/rpc/api/core-node/get-pox.example.json +++ b/docs/rpc/api/core-node/get-pox.example.json @@ -2,6 +2,7 @@ "contract_id": "SP000000000000000000002Q6VF78.pox", "pox_activation_threshold_ustx": 52329761604388, "first_burnchain_block_height": 666050, + "current_burnchain_block_height": 812345, "prepare_phase_block_length": 100, "reward_phase_block_length": 2000, "reward_slots": 4000, @@ -29,5 +30,17 @@ "reward_cycle_id": 2, "reward_cycle_length": 2100, "rejection_votes_left_required": 261648808021925, - "next_reward_cycle_in": 507 + "next_reward_cycle_in": 507, + "contract_versions": [ + { + "contract_id": "SP000000000000000000002Q6VF78.pox", + "activation_burnchain_block_height": 666050, + "first_reward_cycle_id": 0 + }, + { + "contract_id": "SP000000000000000000002Q6VF78.pox-2", + "activation_burnchain_block_height": 712345, + "first_reward_cycle_id": 123 + } + ] } diff --git a/docs/rpc/api/core-node/get-pox.schema.json b/docs/rpc/api/core-node/get-pox.schema.json index a4856f732..b0fffacb9 100644 --- a/docs/rpc/api/core-node/get-pox.schema.json +++ b/docs/rpc/api/core-node/get-pox.schema.json @@ -6,6 +6,7 @@ "additionalProperties": false, "required": [ "contract_id", + "current_burnchain_block_height", "first_burnchain_block_height", "pox_activation_threshold_ustx", "prepare_phase_block_length", @@ -19,7 +20,8 @@ "min_amount_ustx", "reward_cycle_id", "prepare_cycle_length", - "rejection_votes_left_required" + "rejection_votes_left_required", + "contract_versions" ], "properties": { "contract_id": { @@ -30,6 +32,10 @@ "type": "integer", "description": "The first burn block evaluated in this Stacks chain" }, + "current_burnchain_block_height": { + "type": "integer", + "description": "The latest Bitcoin chain block height" + }, "pox_activation_threshold_ustx": { "type": "integer", "description": "The threshold of stacking participation that must be reached for PoX to activate in any cycle" @@ -83,7 +89,7 @@ "is_pox_active": { "type": "boolean", "description": "Whether or not PoX is active during this reward cycle." - }, + } } }, "next_cycle": { @@ -146,15 +152,42 @@ }, "min_amount_ustx": { "type": "integer", - "deprecated": true, + "deprecated": true }, "prepare_cycle_length": { "type": "integer", - "deprecated": true, + "deprecated": true }, "rejection_votes_left_required": { "type": "integer", - "deprecated": true, + "deprecated": true + }, + "contract_versions": { + "type": "array", + "description": "Versions of each PoX", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "contract_id", + "activation_burnchain_block_height", + "first_reward_cycle_id" + ], + "properties": { + "contract_id": { + "type": "string", + "description": "The contract identifier for the PoX contract" + }, + "activation_burnchain_block_height": { + "type": "integer", + "description": "The burn block height at which this version of PoX is activated" + }, + "first_reward_cycle_id": { + "type": "integer", + "description": "The first reward cycle number that uses this version of PoX" + } + } + } } } }