Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/burnchains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ impl PoxConstants {
PoxConstants::new(10, 5, 3, 25)
}

pub fn reward_slots(&self) -> u32 {
self.reward_cycle_length
}

pub fn mainnet_default() -> PoxConstants {
PoxConstants::new(1000, 240, 192, 25)
}
Expand Down
58 changes: 52 additions & 6 deletions src/chainstate/coordinator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::VecDeque;
use std::convert::TryInto;
use std::convert::{TryFrom, TryInto};
use std::time::Duration;

use burnchains::{
Expand All @@ -12,13 +12,18 @@ use chainstate::burn::{
BlockHeaderHash, BlockSnapshot, ConsensusHash,
};
use chainstate::stacks::{
boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS,
db::{ClarityTx, StacksChainState, StacksHeaderInfo},
events::StacksTransactionReceipt,
Error as ChainstateError, StacksAddress, StacksBlock, StacksBlockHeader, StacksBlockId,
};
use monitoring::increment_stx_blocks_processed_counter;
use util::db::Error as DBError;
use vm::{costs::ExecutionCost, types::PrincipalData};
use vm::{
costs::ExecutionCost,
types::{PrincipalData, QualifiedContractIdentifier},
Value,
};

pub mod comm;
use chainstate::stacks::index::MarfTrieId;
Expand Down Expand Up @@ -161,10 +166,26 @@ impl RewardSetProvider for OnChainRewardSetProvider {
sortdb: &SortitionDB,
block_id: &StacksBlockId,
) -> Result<Vec<StacksAddress>, Error> {
let res =
let registered_addrs =
chainstate.get_reward_addresses(burnchain, sortdb, current_burn_height, block_id)?;
let addresses = res.iter().map(|a| a.0).collect::<Vec<StacksAddress>>();
Ok(addresses)

let liquid_ustx = StacksChainState::get_stacks_block_header_info_by_index_block_hash(
chainstate.headers_db(),
block_id,
)?
.expect("CORRUPTION: Failed to look up block header info for PoX anchor block")
.total_liquid_ustx;

let threshold = StacksChainState::get_reward_threshold(
&burnchain.pox_constants,
&registered_addrs,
liquid_ustx,
);

Ok(StacksChainState::make_reward_set(
threshold,
registered_addrs,
))
}
}

Expand Down Expand Up @@ -196,7 +217,32 @@ impl<'a, T: BlockEventDispatcher>
stacks_chain_id,
chain_state_path,
initial_balances,
boot_block_exec,
|clarity_tx| {
let burnchain = burnchain.clone();
let contract = QualifiedContractIdentifier::parse(&format!(
"{}.pox",
STACKS_BOOT_CODE_CONTRACT_ADDRESS
))
.expect("Failed to construct boot code contract address");
let sender = PrincipalData::from(contract.clone());

clarity_tx.connection().as_transaction(|conn| {
conn.run_contract_call(
&sender,
&contract,
"set-burnchain-parameters",
&[
Value::UInt(burnchain.first_block_height as u128),
Value::UInt(burnchain.pox_constants.prepare_length as u128),
Value::UInt(burnchain.pox_constants.reward_cycle_length as u128),
Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128),
],
|_, _| false,
)
.expect("Failed to set burnchain parameters in PoX contract");
});
boot_block_exec(clarity_tx)
},
block_limit,
)
.unwrap();
Expand Down
183 changes: 179 additions & 4 deletions src/chainstate/stacks/boot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ use chainstate::stacks::StacksBlockHeader;

use address::AddressHashMode;
use burnchains::bitcoin::address::BitcoinAddress;
use burnchains::Address;
use burnchains::{Address, PoxConstants};

use chainstate::burn::db::sortdb::SortitionDB;
use core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX};

use vm::types::{
PrincipalData, QualifiedContractIdentifier, SequenceData, StandardPrincipalData, TupleData,
Expand All @@ -42,6 +43,7 @@ use vm::representations::ContractName;
use util::hash::Hash160;

use std::boxed::Box;
use std::cmp;
use std::convert::TryFrom;
use std::convert::TryInto;

Expand Down Expand Up @@ -183,6 +185,74 @@ impl StacksChainState {
.map(|value| value.expect_bool())
}

/// Given a threshold and set of registered addresses, return a reward set where
/// every entry address has stacked more than the threshold, and addresses
/// 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<(StacksAddress, u128)>,
) -> Vec<StacksAddress> {
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() {
// 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
.pop()
.expect("BUG: first() returned some, but pop() is none.");
stacked_amt = stacked_amt
.checked_add(additional_amt)
.expect("CORRUPTION: Stacker stacked > u128 max amount");
}
let slots_taken = u32::try_from(stacked_amt / threshold)
.expect("CORRUPTION: Stacker claimed > u32::max() reward slots");
info!(
"Slots taken by {} = {}, on stacked_amt = {}",
&address, slots_taken, stacked_amt
);
for _i in 0..slots_taken {
reward_set.push(address.clone());
}
}
reward_set
}

pub fn get_reward_threshold(
pox_settings: &PoxConstants,
addresses: &[(StacksAddress, u128)],
liquid_ustx: u128,
) -> u128 {
let participation = addresses
.iter()
.fold(0, |agg, (_, stacked_amt)| agg + stacked_amt);

assert!(
participation <= liquid_ustx,
"CORRUPTION: More stacking participation than liquid STX"
);

// set the lower limit on reward scaling at 25% of liquid_ustx
// (i.e., liquid_ustx / POX_MAXIMAL_SCALING)
let scale_by = cmp::max(participation, liquid_ustx / POX_MAXIMAL_SCALING as u128);

let reward_slots = pox_settings.reward_slots() as u128;
let threshold_precise = scale_by / reward_slots;
// compute the threshold as nearest 10k > threshold_precise
let ceil_amount = match threshold_precise % POX_THRESHOLD_STEPS_USTX {
0 => 0,
remainder => POX_THRESHOLD_STEPS_USTX - remainder,
};
let threshold = threshold_precise + ceil_amount;
info!(
"PoX participation threshold is {}, from {}",
threshold, threshold_precise
);
threshold
}

/// Each address will have at least (get-stacking-minimum) tokens.
pub fn get_reward_addresses(
&mut self,
Expand Down Expand Up @@ -255,8 +325,6 @@ impl StacksChainState {
ret.push((StacksAddress::new(version, hash), total_ustx));
}

ret.sort_by_key(|k| k.0.bytes.0);

Ok(ret)
}
}
Expand Down Expand Up @@ -285,6 +353,7 @@ pub mod test {

use util::*;

use core::*;
use vm::contracts::Contract;
use vm::types::*;

Expand All @@ -293,6 +362,107 @@ pub mod test {

use util::hash::to_hex;

#[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,
),
];
assert_eq!(
StacksChainState::make_reward_set(threshold, addresses).len(),
3
);
}

#[test]
fn get_reward_threshold_units() {
// when the liquid amount = the threshold step,
// the threshold should always be the step size.
let liquid = POX_THRESHOLD_STEPS_USTX;
assert_eq!(
StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid),
POX_THRESHOLD_STEPS_USTX
);
assert_eq!(
StacksChainState::get_reward_threshold(
&PoxConstants::new(1000, 1, 1, 1),
&[(rand_addr(), liquid)],
liquid
),
POX_THRESHOLD_STEPS_USTX
);

let liquid = 200_000_000 * MICROSTACKS_PER_STACKS as u128;
// with zero participation, should scale to 25% of liquid
assert_eq!(
StacksChainState::get_reward_threshold(&PoxConstants::new(1000, 1, 1, 1), &[], liquid),
50_000 * MICROSTACKS_PER_STACKS as u128
);
// should be the same at 25% participation
assert_eq!(
StacksChainState::get_reward_threshold(
&PoxConstants::new(1000, 1, 1, 1),
&[(rand_addr(), liquid / 4)],
liquid
),
50_000 * MICROSTACKS_PER_STACKS as u128
);
// but not at 30% participation
assert_eq!(
StacksChainState::get_reward_threshold(
&PoxConstants::new(1000, 1, 1, 1),
&[
(rand_addr(), liquid / 4),
(rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128))
],
liquid
),
60_000 * MICROSTACKS_PER_STACKS as u128
);

// bump by just a little bit, should go to the next threshold step
assert_eq!(
StacksChainState::get_reward_threshold(
&PoxConstants::new(1000, 1, 1, 1),
&[
(rand_addr(), liquid / 4),
(rand_addr(), (MICROSTACKS_PER_STACKS as u128))
],
liquid
),
60_000 * MICROSTACKS_PER_STACKS as u128
);

// bump by just a little bit, should go to the next threshold step
assert_eq!(
StacksChainState::get_reward_threshold(
&PoxConstants::new(1000, 1, 1, 1),
&[(rand_addr(), liquid)],
liquid
),
200_000 * MICROSTACKS_PER_STACKS as u128
);
}

fn rand_addr() -> StacksAddress {
key_to_stacks_addr(&StacksPrivateKey::new())
}

fn key_to_stacks_addr(key: &StacksPrivateKey) -> StacksAddress {
StacksAddress::from_public_keys(
C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
Expand Down Expand Up @@ -739,7 +909,12 @@ pub mod test {
block_id: &StacksBlockId,
) -> Result<Vec<(StacksAddress, u128)>, Error> {
let burn_block_height = get_par_burn_block_height(state, block_id);
state.get_reward_addresses(burnchain, sortdb, burn_block_height, block_id)
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)
})
}

fn get_parent_tip(
Expand Down
2 changes: 1 addition & 1 deletion src/chainstate/stacks/boot/pox.clar
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
;; This function can only be called once, when it boots up
(define-public (set-burnchain-parameters (first-burn-height uint) (prepare-cycle-length uint) (reward-cycle-length uint) (rejection-fraction uint))
(begin
(asserts! (and is-in-regtest (not (var-get configured))) (err ERR_NOT_ALLOWED))
(asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're getting rid of is-in-regtest, should we kill it completely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep it for the time being -- it may be useful elsewhere.

(var-set first-burnchain-block-height first-burn-height)
(var-set pox-prepare-cycle-length prepare-cycle-length)
(var-set pox-reward-cycle-length reward-cycle-length)
Expand Down
15 changes: 6 additions & 9 deletions src/chainstate/stacks/db/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ use vm::costs::ExecutionCost;

use util::db::Error as db_error;
use util::db::{
query_count, query_row, query_row_columns, query_rows, DBConn, FromColumn, FromRow,
query_count, query_row, query_row_columns, query_row_panic, query_rows, DBConn, FromColumn,
FromRow,
};

use core::FIRST_BURNCHAIN_CONSENSUS_HASH;
Expand Down Expand Up @@ -278,14 +279,10 @@ impl StacksChainState {
index_block_hash: &StacksBlockId,
) -> Result<Option<StacksHeaderInfo>, Error> {
let sql = "SELECT * FROM block_headers WHERE index_block_hash = ?1".to_string();
let mut rows = query_rows::<StacksHeaderInfo, _>(conn, &sql, &[&index_block_hash])
.map_err(Error::DBError)?;
let cnt = rows.len();
if cnt > 1 {
unreachable!("FATAL: multiple rows for the same block hash") // should be unreachable, since index_block_hash is unique
}

Ok(rows.pop())
query_row_panic(conn, &sql, &[&index_block_hash], || {
"FATAL: multiple rows for the same block hash".to_string()
})
.map_err(Error::DBError)
}

/// Get an ancestor block header
Expand Down
10 changes: 10 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,18 @@ pub const BURNCHAIN_BOOT_CONSENSUS_HASH: ConsensusHash = ConsensusHash([0xff; 20

pub const CHAINSTATE_VERSION: &'static str = "23.0.0.0";

pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000;

pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240;
pub const POX_REWARD_CYCLE_LENGTH: u32 = 1000;
/// The maximum amount that PoX rewards can be scaled by.
/// That is, if participation is very low, rewards are:
/// POX_MAXIMAL_SCALING x (rewards with 100% participation)
/// Set a 4x, this implies the lower bound of participation for scaling
/// is 25%
pub const POX_MAXIMAL_SCALING: u128 = 4;
/// This is the amount that PoX threshold adjustments are stepped by.
pub const POX_THRESHOLD_STEPS_USTX: u128 = 10_000 * (MICROSTACKS_PER_STACKS as u128);

/// Synchronize burn transactions from the Bitcoin blockchain
pub fn sync_burnchain_bitcoin(
Expand Down
Loading