diff --git a/.github/actions/docsgen/Dockerfile.docsgen b/.github/actions/docsgen/Dockerfile.docsgen
index 5f073a65869..9bed9ff4621 100644
--- a/.github/actions/docsgen/Dockerfile.docsgen
+++ b/.github/actions/docsgen/Dockerfile.docsgen
@@ -4,13 +4,15 @@ WORKDIR /src
COPY . .
-RUN apt-get update && apt-get install -y git
+RUN apt-get update && apt-get install -y git jq
RUN cargo build
RUN mkdir /out
-RUN /src/target/debug/blockstack-core docgen > /out/clarity-reference.json
+RUN /src/target/debug/blockstack-core docgen | jq . > /out/clarity-reference.json
+RUN /src/target/debug/blockstack-core docgen_boot | jq . > /out/boot-contracts-reference.json
FROM scratch AS export-stage
COPY --from=build /out/clarity-reference.json /
+COPY --from=build /out/boot-contracts-reference.json /
diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml
index 05c2c6216c2..6f43ecb9c4f 100644
--- a/.github/workflows/docs-pr.yml
+++ b/.github/workflows/docs-pr.yml
@@ -43,18 +43,20 @@ jobs:
id: push
run: |
cd docs.blockstack
- git config user.email "robots@robots.actions"
+ git config user.email "kantai+robot@gmail.com"
git config user.name "PR Robot"
git fetch --unshallow
git checkout -b $ROBOT_BRANCH
cp ../docs-output/clarity-reference.json ./src/_data/clarity-reference.json
- if $(git diff --quiet --exit-code); then
- echo "No clarity-reference.json changes, stopping"
+ cp ../docs-output/boot-contracts-reference.json ./src/_data/boot-contracts-reference.json
+ git add src/_data/clarity-reference.json
+ git add src/_data/boot-contracts-reference.json
+ if $(git diff --staged --quiet --exit-code); then
+ echo "No reference.json changes, stopping"
echo "::set-output name=open_pr::0"
else
git remote add robot https://github.com/$ROBOT_OWNER/$ROBOT_REPO
- git add src/_data/clarity-reference.json
- git commit -m "auto: update clarity-reference.json from stacks-blockchain@${GITHUB_SHA}"
+ git commit -m "auto: update Clarity references JSONs from stacks-blockchain@${GITHUB_SHA}"
git push robot $ROBOT_BRANCH
echo "::set-output name=open_pr::1"
fi
diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs
index 5f19fe6f81d..07daad27949 100644
--- a/src/burnchains/mod.rs
+++ b/src/burnchains/mod.rs
@@ -280,6 +280,9 @@ pub struct PoxConstants {
/// fraction of liquid STX that must vote to reject PoX for
/// it to revert to PoB in the next reward cycle
pub pox_rejection_fraction: u64,
+ /// percentage of liquid STX that must participate for PoX
+ /// to occur
+ pub pox_participation_threshold_pct: u64,
_shadow: PhantomData<()>,
}
@@ -289,6 +292,7 @@ impl PoxConstants {
prepare_length: u32,
anchor_threshold: u32,
pox_rejection_fraction: u64,
+ pox_participation_threshold_pct: u64,
) -> PoxConstants {
assert!(anchor_threshold > (prepare_length / 2));
@@ -297,20 +301,35 @@ impl PoxConstants {
prepare_length,
anchor_threshold,
pox_rejection_fraction,
+ pox_participation_threshold_pct,
_shadow: PhantomData,
}
}
#[cfg(test)]
pub fn test_default() -> PoxConstants {
- PoxConstants::new(10, 5, 3, 25)
+ PoxConstants::new(10, 5, 3, 25, 5)
+ }
+
+ pub fn reward_slots(&self) -> u32 {
+ self.reward_cycle_length
+ }
+
+ /// is participating_ustx enough to engage in PoX in the next reward cycle?
+ pub fn enough_participation(&self, participating_ustx: u128, liquid_ustx: u128) -> bool {
+ participating_ustx
+ .checked_mul(100)
+ .expect("OVERFLOW: uSTX overflowed u128")
+ > liquid_ustx
+ .checked_mul(self.pox_participation_threshold_pct as u128)
+ .expect("OVERFLOW: uSTX overflowed u128")
}
pub fn mainnet_default() -> PoxConstants {
- PoxConstants::new(1000, 240, 192, 25)
+ PoxConstants::new(1000, 240, 192, 25, 5)
}
pub fn testnet_default() -> PoxConstants {
- PoxConstants::new(120, 30, 20, 3333333333333333) // total liquid supply is 40000000000000000 µSTX
+ PoxConstants::new(120, 30, 20, 3333333333333333, 5) // total liquid supply is 40000000000000000 µSTX
}
}
diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs
index 85bfd81b426..ea358d09b9c 100644
--- a/src/chainstate/coordinator/mod.rs
+++ b/src/chainstate/coordinator/mod.rs
@@ -15,7 +15,7 @@
// along with this program. If not, see .
use std::collections::VecDeque;
-use std::convert::TryInto;
+use std::convert::{TryFrom, TryInto};
use std::time::Duration;
use burnchains::{
@@ -28,13 +28,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;
@@ -177,10 +182,35 @@ impl RewardSetProvider for OnChainRewardSetProvider {
sortdb: &SortitionDB,
block_id: &StacksBlockId,
) -> Result, 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::>();
- 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, participation) = StacksChainState::get_reward_threshold_and_participation(
+ &burnchain.pox_constants,
+ ®istered_addrs,
+ liquid_ustx,
+ );
+
+ if !burnchain
+ .pox_constants
+ .enough_participation(participation, liquid_ustx)
+ {
+ info!("PoX reward cycle did not have enough participation. Defaulting to burn. participation={}, liquid_ustx={}, burn_height={}",
+ participation, liquid_ustx, current_burn_height);
+ return Ok(vec![]);
+ }
+
+ Ok(StacksChainState::make_reward_set(
+ threshold,
+ registered_addrs,
+ ))
}
}
@@ -212,7 +242,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();
diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs
index 5051e1dedfb..c8c74770e1b 100644
--- a/src/chainstate/coordinator/tests.rs
+++ b/src/chainstate/coordinator/tests.rs
@@ -268,7 +268,7 @@ fn make_reward_set_coordinator<'a>(
pub fn get_burnchain(path: &str) -> Burnchain {
let mut b = Burnchain::new(&format!("{}/burnchain/db/", path), "bitcoin", "regtest").unwrap();
- b.pox_constants = PoxConstants::new(5, 3, 3, 25);
+ b.pox_constants = PoxConstants::new(5, 3, 3, 25, 5);
b
}
diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs
new file mode 100644
index 00000000000..26f24523df0
--- /dev/null
+++ b/src/chainstate/stacks/boot/contract_tests.rs
@@ -0,0 +1,847 @@
+use std::collections::{HashMap, VecDeque};
+use std::convert::TryFrom;
+
+use vm::contracts::Contract;
+use vm::errors::{
+ CheckErrors, Error, IncomparableError, InterpreterError, InterpreterResult as Result,
+ RuntimeErrorType,
+};
+use vm::types::{
+ OptionalData, PrincipalData, QualifiedContractIdentifier, StandardPrincipalData,
+ TupleTypeSignature, TypeSignature, Value, NONE,
+};
+
+use std::convert::TryInto;
+
+use burnchains::BurnchainHeaderHash;
+use chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed};
+use chainstate::stacks::boot::STACKS_BOOT_CODE_CONTRACT_ADDRESS;
+use chainstate::stacks::db::{MinerPaymentSchedule, StacksHeaderInfo};
+use chainstate::stacks::index::proofs::TrieMerkleProof;
+use chainstate::stacks::index::MarfTrieId;
+use chainstate::stacks::*;
+
+use util::db::{DBConn, FromRow};
+use util::hash::{Sha256Sum, Sha512Trunc256Sum};
+use vm::contexts::OwnedEnvironment;
+use vm::costs::CostOverflowingMath;
+use vm::database::*;
+use vm::representations::SymbolicExpression;
+
+use util::hash::to_hex;
+use vm::eval;
+use vm::tests::{execute, is_committed, is_err_code, symbols_from_values};
+
+use core::{
+ FIRST_BURNCHAIN_BLOCK_HASH, FIRST_BURNCHAIN_BLOCK_HEIGHT, FIRST_BURNCHAIN_BLOCK_TIMESTAMP,
+ FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, POX_REWARD_CYCLE_LENGTH,
+};
+
+const BOOT_CODE_POX_BODY: &'static str = std::include_str!("pox.clar");
+const BOOT_CODE_POX_TESTNET_CONSTS: &'static str = std::include_str!("pox-testnet.clar");
+const BOOT_CODE_POX_MAINNET_CONSTS: &'static str = std::include_str!("pox-mainnet.clar");
+const BOOT_CODE_LOCKUP: &'static str = std::include_str!("lockup.clar");
+
+const USTX_PER_HOLDER: u128 = 1_000_000;
+
+lazy_static! {
+ static ref BOOT_CODE_POX_MAINNET: String =
+ format!("{}\n{}", BOOT_CODE_POX_MAINNET_CONSTS, BOOT_CODE_POX_BODY);
+ static ref BOOT_CODE_POX_TESTNET: String =
+ format!("{}\n{}", BOOT_CODE_POX_TESTNET_CONSTS, BOOT_CODE_POX_BODY);
+ static ref FIRST_INDEX_BLOCK_HASH: StacksBlockId = StacksBlockHeader::make_index_block_hash(
+ &FIRST_BURNCHAIN_CONSENSUS_HASH,
+ &FIRST_STACKS_BLOCK_HASH
+ );
+ static ref POX_CONTRACT: QualifiedContractIdentifier =
+ QualifiedContractIdentifier::parse(&format!("{}.pox", STACKS_BOOT_CODE_CONTRACT_ADDRESS))
+ .unwrap();
+ static ref USER_KEYS: Vec =
+ (0..50).map(|_| StacksPrivateKey::new()).collect();
+ static ref POX_ADDRS: Vec = (0..50u64)
+ .map(|ix| execute(&format!(
+ "{{ version: 0x00, hashbytes: 0x000000000000000000000000{} }}",
+ &to_hex(&ix.to_le_bytes())
+ )))
+ .collect();
+ static ref LIQUID_SUPPLY: u128 = USTX_PER_HOLDER * (POX_ADDRS.len() as u128);
+ static ref MIN_THRESHOLD: u128 = *LIQUID_SUPPLY / 480;
+}
+
+impl From<&StacksPrivateKey> for StandardPrincipalData {
+ fn from(o: &StacksPrivateKey) -> StandardPrincipalData {
+ let stacks_addr = StacksAddress::from_public_keys(
+ C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
+ &AddressHashMode::SerializeP2PKH,
+ 1,
+ &vec![StacksPublicKey::from_private(o)],
+ )
+ .unwrap();
+ StandardPrincipalData::from(stacks_addr)
+ }
+}
+
+impl From<&StacksPrivateKey> for Value {
+ fn from(o: &StacksPrivateKey) -> Value {
+ Value::from(StandardPrincipalData::from(o))
+ }
+}
+
+struct ClarityTestSim {
+ marf: MarfedKV,
+ height: u64,
+}
+
+struct TestSimHeadersDB {
+ height: u64,
+}
+
+impl ClarityTestSim {
+ pub fn new() -> ClarityTestSim {
+ let mut marf = MarfedKV::temporary();
+ marf.begin(
+ &StacksBlockId::sentinel(),
+ &StacksBlockId(test_sim_height_to_hash(0)),
+ );
+ {
+ marf.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB)
+ .initialize();
+
+ let mut owned_env =
+ OwnedEnvironment::new(marf.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB));
+
+ for user_key in USER_KEYS.iter() {
+ owned_env.stx_faucet(
+ &StandardPrincipalData::from(user_key).into(),
+ USTX_PER_HOLDER,
+ );
+ }
+ }
+ marf.test_commit();
+
+ ClarityTestSim { marf, height: 0 }
+ }
+
+ pub fn execute_next_block(&mut self, f: F) -> R
+ where
+ F: FnOnce(&mut OwnedEnvironment) -> R,
+ {
+ self.marf.begin(
+ &StacksBlockId(test_sim_height_to_hash(self.height)),
+ &StacksBlockId(test_sim_height_to_hash(self.height + 1)),
+ );
+
+ let r = {
+ let headers_db = TestSimHeadersDB {
+ height: self.height + 1,
+ };
+ let mut owned_env =
+ OwnedEnvironment::new(self.marf.as_clarity_db(&headers_db, &NULL_BURN_STATE_DB));
+ f(&mut owned_env)
+ };
+
+ self.marf.test_commit();
+ self.height += 1;
+
+ r
+ }
+}
+
+fn test_sim_height_to_hash(burn_height: u64) -> [u8; 32] {
+ let mut out = [0; 32];
+ out[0..8].copy_from_slice(&burn_height.to_le_bytes());
+ out
+}
+
+fn test_sim_hash_to_height(in_bytes: &[u8; 32]) -> Option {
+ if &in_bytes[8..] != &[0; 24] {
+ None
+ } else {
+ let mut bytes = [0; 8];
+ bytes.copy_from_slice(&in_bytes[0..8]);
+ Some(u64::from_le_bytes(bytes))
+ }
+}
+
+impl HeadersDB for TestSimHeadersDB {
+ fn get_burn_header_hash_for_block(
+ &self,
+ id_bhh: &StacksBlockId,
+ ) -> Option {
+ if *id_bhh == *FIRST_INDEX_BLOCK_HASH {
+ Some(FIRST_BURNCHAIN_BLOCK_HASH)
+ } else {
+ self.get_burn_block_height_for_block(id_bhh)?;
+ Some(BurnchainHeaderHash(id_bhh.0.clone()))
+ }
+ }
+
+ fn get_vrf_seed_for_block(&self, _bhh: &StacksBlockId) -> Option {
+ None
+ }
+
+ fn get_stacks_block_header_hash_for_block(
+ &self,
+ id_bhh: &StacksBlockId,
+ ) -> Option {
+ if *id_bhh == *FIRST_INDEX_BLOCK_HASH {
+ Some(FIRST_STACKS_BLOCK_HASH)
+ } else {
+ self.get_burn_block_height_for_block(id_bhh)?;
+ Some(BlockHeaderHash(id_bhh.0.clone()))
+ }
+ }
+
+ fn get_burn_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option {
+ if *id_bhh == *FIRST_INDEX_BLOCK_HASH {
+ Some(FIRST_BURNCHAIN_BLOCK_TIMESTAMP)
+ } else {
+ let burn_block_height = self.get_burn_block_height_for_block(id_bhh)? as u64;
+ Some(
+ FIRST_BURNCHAIN_BLOCK_TIMESTAMP + burn_block_height
+ - FIRST_BURNCHAIN_BLOCK_HEIGHT as u64,
+ )
+ }
+ }
+ fn get_burn_block_height_for_block(&self, id_bhh: &StacksBlockId) -> Option {
+ if *id_bhh == *FIRST_INDEX_BLOCK_HASH {
+ Some(FIRST_BURNCHAIN_BLOCK_HEIGHT)
+ } else {
+ let input_height = test_sim_hash_to_height(&id_bhh.0)?;
+ if input_height > self.height {
+ eprintln!("{} > {}", input_height, self.height);
+ None
+ } else {
+ Some(
+ (FIRST_BURNCHAIN_BLOCK_HEIGHT as u64 + input_height)
+ .try_into()
+ .unwrap(),
+ )
+ }
+ }
+ }
+ fn get_miner_address(&self, _id_bhh: &StacksBlockId) -> Option {
+ None
+ }
+ fn get_total_liquid_ustx(&self, _id_bhh: &StacksBlockId) -> u128 {
+ *LIQUID_SUPPLY
+ }
+}
+
+#[test]
+fn recency_tests() {
+ let mut sim = ClarityTestSim::new();
+ let delegator = StacksPrivateKey::new();
+
+ sim.execute_next_block(|env| {
+ env.initialize_contract(POX_CONTRACT.clone(), &BOOT_CODE_POX_TESTNET)
+ .unwrap()
+ });
+ sim.execute_next_block(|env| {
+ // try to issue a far future stacking tx
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[0]).into(),
+ POX_CONTRACT.clone(),
+ "stack-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ POX_ADDRS[0].clone(),
+ Value::UInt(3000),
+ Value::UInt(3),
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 24)".to_string()
+ );
+ // let's delegate, and check if the delegate can issue a far future
+ // stacking tx
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[0]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(2 * USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[0]).into(),
+ Value::UInt(USTX_PER_HOLDER),
+ POX_ADDRS[1].clone(),
+ Value::UInt(3000),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 24)".to_string()
+ );
+ });
+}
+
+#[test]
+fn delegation_tests() {
+ let mut sim = ClarityTestSim::new();
+ let delegator = StacksPrivateKey::new();
+
+ sim.execute_next_block(|env| {
+ env.initialize_contract(POX_CONTRACT.clone(), &BOOT_CODE_POX_TESTNET)
+ .unwrap()
+ });
+ sim.execute_next_block(|env| {
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[0]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(2 * USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+
+ // already delegating...
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[0]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::error(Value::Int(20)).unwrap()
+ );
+
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[1]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::some(POX_ADDRS[0].clone()).unwrap()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[2]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::some(Value::UInt(300)).unwrap(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[3]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[4]).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stx",
+ &symbols_from_values(vec![
+ Value::UInt(USTX_PER_HOLDER),
+ (&delegator).into(),
+ Value::none(),
+ Value::none()
+ ])
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+ });
+ // let's do some delegated stacking!
+ sim.execute_next_block(|env| {
+ // try to stack more than [0]'s delegated amount!
+ let burn_height = env.eval_raw("burn-block-height").unwrap().0;
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[0]).into(),
+ Value::UInt(3 * USTX_PER_HOLDER),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 22)".to_string()
+ );
+
+ // try to stack more than [0] has!
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[0]).into(),
+ Value::UInt(2 * USTX_PER_HOLDER),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 1)".to_string()
+ );
+
+ // let's stack less than the threshold
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[0]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})",
+ Value::from(&USER_KEYS[0]),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ Value::UInt(360)
+ ))
+ );
+
+ assert_eq!(
+ env.eval_read_only(
+ &POX_CONTRACT,
+ &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[0]))
+ )
+ .unwrap()
+ .0,
+ Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1)
+ );
+
+ // try to commit our partial stacking...
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "stack-aggregation-commit",
+ &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 11)".to_string()
+ );
+ // not enough! we need to stack more...
+ // but POX_ADDR[1] cannot be used for USER_KEYS[1]...
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[1]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 23)".to_string()
+ );
+
+ // And USER_KEYS[0] is already stacking...
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[0]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 3)".to_string()
+ );
+
+ // USER_KEYS[2] won't want to stack past the delegation expiration...
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[2]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 21)".to_string()
+ );
+
+ // but for just one block will be fine
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[2]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(1)
+ ])
+ )
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})",
+ Value::from(&USER_KEYS[2]),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ Value::UInt(240)
+ ))
+ );
+
+ assert_eq!(
+ env.eval_read_only(
+ &POX_CONTRACT,
+ &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[2]))
+ )
+ .unwrap()
+ .0,
+ Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1)
+ );
+
+ assert_eq!(
+ env.eval_read_only(
+ &POX_CONTRACT,
+ &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[0]))
+ )
+ .unwrap()
+ .0,
+ Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD + 1)
+ );
+
+ assert_eq!(
+ env.eval_read_only(
+ &POX_CONTRACT,
+ &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[1]))
+ )
+ .unwrap()
+ .0,
+ Value::UInt(USTX_PER_HOLDER)
+ );
+
+ // try to commit our partial stacking again!
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "stack-aggregation-commit",
+ &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(ok true)".to_string()
+ );
+
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u1)")
+ .unwrap()
+ .0
+ .to_string(),
+ "u1"
+ );
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u0)")
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(some {{ pox-addr: {}, total-ustx: {} }})",
+ &POX_ADDRS[1],
+ &Value::UInt(2 * (*MIN_THRESHOLD - 1))
+ ))
+ );
+
+ // can we double commit? I don't think so!
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "stack-aggregation-commit",
+ &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 4)".to_string()
+ );
+
+ // okay, let's try some more delegation situations...
+ // 1. we already locked user[0] up for round 2, so let's add some more stacks for round 2 from
+ // user[3]. in the process, this will add more stacks for lockup in round 1, so lets commit
+ // that as well.
+
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[3]).into(),
+ Value::UInt(*MIN_THRESHOLD),
+ POX_ADDRS[1].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})",
+ Value::from(&USER_KEYS[3]),
+ Value::UInt(*MIN_THRESHOLD),
+ Value::UInt(360)
+ ))
+ );
+
+ assert_eq!(
+ env.eval_read_only(
+ &POX_CONTRACT,
+ &format!("(stx-get-balance '{})", &Value::from(&USER_KEYS[3]))
+ )
+ .unwrap()
+ .0,
+ Value::UInt(USTX_PER_HOLDER - *MIN_THRESHOLD)
+ );
+
+ // let's commit to round 2 now.
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "stack-aggregation-commit",
+ &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(2)])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(ok true)".to_string()
+ );
+
+ // and we can commit to round 1 again as well!
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "stack-aggregation-commit",
+ &symbols_from_values(vec![POX_ADDRS[1].clone(), Value::UInt(1)])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(ok true)".to_string()
+ );
+
+ // check reward sets for round 2 and round 1...
+
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u2)")
+ .unwrap()
+ .0
+ .to_string(),
+ "u1"
+ );
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u2 u0)")
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(some {{ pox-addr: {}, total-ustx: {} }})",
+ &POX_ADDRS[1],
+ &Value::UInt(2 * (*MIN_THRESHOLD) - 1)
+ ))
+ );
+
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-size u1)")
+ .unwrap()
+ .0
+ .to_string(),
+ "u2"
+ );
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u0)")
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(some {{ pox-addr: {}, total-ustx: {} }})",
+ &POX_ADDRS[1],
+ &Value::UInt(2 * (*MIN_THRESHOLD - 1))
+ ))
+ );
+ assert_eq!(
+ env.eval_read_only(&POX_CONTRACT, "(get-reward-set-pox-address u1 u1)")
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(some {{ pox-addr: {}, total-ustx: {} }})",
+ &POX_ADDRS[1],
+ &Value::UInt(*MIN_THRESHOLD)
+ ))
+ );
+
+ // 2. lets make sure we can lock up for user[1] so long as it goes to pox[0].
+
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[1]).into(),
+ Value::UInt(*MIN_THRESHOLD),
+ POX_ADDRS[0].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0,
+ execute(&format!(
+ "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})",
+ Value::from(&USER_KEYS[1]),
+ Value::UInt(*MIN_THRESHOLD),
+ Value::UInt(360)
+ ))
+ );
+
+ // 3. lets try to lock up user[4], but do some revocation first.
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[4]).into(),
+ POX_CONTRACT.clone(),
+ "revoke-delegate-stx",
+ &[]
+ )
+ .unwrap()
+ .0,
+ Value::okay_true()
+ );
+
+ // will run a second time, but return false
+ assert_eq!(
+ env.execute_transaction(
+ (&USER_KEYS[4]).into(),
+ POX_CONTRACT.clone(),
+ "revoke-delegate-stx",
+ &[]
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(ok false)".to_string()
+ );
+
+ assert_eq!(
+ env.execute_transaction(
+ (&delegator).into(),
+ POX_CONTRACT.clone(),
+ "delegate-stack-stx",
+ &symbols_from_values(vec![
+ (&USER_KEYS[4]).into(),
+ Value::UInt(*MIN_THRESHOLD - 1),
+ POX_ADDRS[0].clone(),
+ burn_height.clone(),
+ Value::UInt(2)
+ ])
+ )
+ .unwrap()
+ .0
+ .to_string(),
+ "(err 9)".to_string()
+ );
+ });
+}
diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs
index 817e5ad26c7..da4873e2560 100644
--- a/src/chainstate/stacks/boot/mod.rs
+++ b/src/chainstate/stacks/boot/mod.rs
@@ -21,9 +21,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,
@@ -39,6 +40,7 @@ use vm::representations::ContractName;
use util::hash::Hash160;
use std::boxed::Box;
+use std::cmp;
use std::convert::TryFrom;
use std::convert::TryInto;
@@ -180,6 +182,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 {
+ 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_and_participation(
+ pox_settings: &PoxConstants,
+ addresses: &[(StacksAddress, u128)],
+ liquid_ustx: u128,
+ ) -> (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, participation)
+ }
+
/// Each address will have at least (get-stacking-minimum) tokens.
pub fn get_reward_addresses(
&mut self,
@@ -252,12 +322,13 @@ impl StacksChainState {
ret.push((StacksAddress::new(version, hash), total_ustx));
}
- ret.sort_by_key(|k| k.0.bytes.0);
-
Ok(ret)
}
}
+#[cfg(test)]
+mod contract_tests;
+
#[cfg(test)]
pub mod test {
use chainstate::burn::db::sortdb::*;
@@ -279,6 +350,7 @@ pub mod test {
use util::*;
+ use core::*;
use vm::contracts::Contract;
use vm::types::*;
@@ -287,6 +359,123 @@ 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() {
+ let test_pox_constants = PoxConstants::new(1000, 1, 1, 1, 5);
+ // 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_and_participation(
+ &test_pox_constants,
+ &[],
+ liquid
+ )
+ .0,
+ POX_THRESHOLD_STEPS_USTX
+ );
+ assert_eq!(
+ StacksChainState::get_reward_threshold_and_participation(
+ &test_pox_constants,
+ &[(rand_addr(), liquid)],
+ liquid
+ )
+ .0,
+ 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_and_participation(
+ &test_pox_constants,
+ &[],
+ liquid
+ )
+ .0,
+ 50_000 * MICROSTACKS_PER_STACKS as u128
+ );
+ // should be the same at 25% participation
+ assert_eq!(
+ StacksChainState::get_reward_threshold_and_participation(
+ &test_pox_constants,
+ &[(rand_addr(), liquid / 4)],
+ liquid
+ )
+ .0,
+ 50_000 * MICROSTACKS_PER_STACKS as u128
+ );
+ // but not at 30% participation
+ assert_eq!(
+ StacksChainState::get_reward_threshold_and_participation(
+ &test_pox_constants,
+ &[
+ (rand_addr(), liquid / 4),
+ (rand_addr(), 10_000_000 * (MICROSTACKS_PER_STACKS as u128))
+ ],
+ liquid
+ )
+ .0,
+ 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_and_participation(
+ &test_pox_constants,
+ &[
+ (rand_addr(), liquid / 4),
+ (rand_addr(), (MICROSTACKS_PER_STACKS as u128))
+ ],
+ liquid
+ )
+ .0,
+ 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_and_participation(
+ &test_pox_constants,
+ &[(rand_addr(), liquid)],
+ liquid
+ )
+ .0,
+ 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,
@@ -506,38 +695,56 @@ pub mod test {
addr_version: AddressHashMode,
addr_bytes: Hash160,
lock_period: u128,
+ burn_ht: u64,
) -> StacksTransaction {
// (define-public (stack-stx (amount-ustx uint)
// (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))
// (lock-period uint))
+ make_pox_contract_call(
+ key,
+ nonce,
+ "stack-stx",
+ vec![
+ Value::UInt(amount),
+ make_pox_addr(addr_version, addr_bytes),
+ Value::UInt(burn_ht as u128),
+ Value::UInt(lock_period),
+ ],
+ )
+ }
+ fn make_tx(
+ key: &StacksPrivateKey,
+ nonce: u64,
+ fee_rate: u64,
+ payload: TransactionPayload,
+ ) -> StacksTransaction {
let auth = TransactionAuth::from_p2pkh(key).unwrap();
let addr = auth.origin().address_testnet();
- let mut pox_lockup = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_contract_call(
- boot_code_addr(),
- "pox",
- "stack-stx",
- vec![
- Value::UInt(amount),
- make_pox_addr(addr_version, addr_bytes),
- Value::UInt(lock_period),
- ],
- )
- .unwrap(),
- );
- pox_lockup.chain_id = 0x80000000;
- pox_lockup.auth.set_origin_nonce(nonce);
- pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow);
- pox_lockup.set_fee_rate(0);
+ let mut tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload);
+ tx.chain_id = 0x80000000;
+ tx.auth.set_origin_nonce(nonce);
+ tx.set_post_condition_mode(TransactionPostConditionMode::Allow);
+ tx.set_fee_rate(fee_rate);
- let mut tx_signer = StacksTransactionSigner::new(&pox_lockup);
+ let mut tx_signer = StacksTransactionSigner::new(&tx);
tx_signer.sign_origin(key).unwrap();
tx_signer.get_tx().unwrap()
}
+ fn make_pox_contract_call(
+ key: &StacksPrivateKey,
+ nonce: u64,
+ function_name: &str,
+ args: Vec,
+ ) -> StacksTransaction {
+ let payload =
+ TransactionPayload::new_contract_call(boot_code_addr(), "pox", function_name, args)
+ .unwrap();
+
+ make_tx(key, nonce, 0, payload)
+ }
+
// make a stream of invalid pox-lockup transactions
fn make_invalid_pox_lockups(key: &StacksPrivateKey, mut nonce: u64) -> Vec {
let mut ret = vec![];
@@ -563,27 +770,12 @@ pub mod test {
);
let generator = |amount, pox_addr, lock_period, nonce| {
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
- let mut pox_lockup = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_contract_call(
- boot_code_addr(),
- "pox",
- "stack-stx",
- vec![Value::UInt(amount), pox_addr, Value::UInt(lock_period)],
- )
- .unwrap(),
- );
- pox_lockup.chain_id = 0x80000000;
- pox_lockup.auth.set_origin_nonce(nonce);
- pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow);
- pox_lockup.set_fee_rate(0);
-
- let mut tx_signer = StacksTransactionSigner::new(&pox_lockup);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ make_pox_contract_call(
+ key,
+ nonce,
+ "stack-stx",
+ vec![Value::UInt(amount), pox_addr, Value::UInt(lock_period)],
+ )
};
let bad_pox_addr_tx = generator(amount, bad_pox_addr_version, lock_period, nonce);
@@ -626,21 +818,8 @@ pub mod test {
name: &str,
code: &str,
) -> StacksTransaction {
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
- let mut bare_code = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_smart_contract(&name.to_string(), &code.to_string()).unwrap(),
- );
- bare_code.chain_id = 0x80000000;
- bare_code.auth.set_origin_nonce(nonce);
- bare_code.set_post_condition_mode(TransactionPostConditionMode::Allow);
- bare_code.set_fee_rate(fee_rate);
-
- let mut tx_signer = StacksTransactionSigner::new(&bare_code);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ let payload = TransactionPayload::new_smart_contract(name, code).unwrap();
+ make_tx(key, nonce, fee_rate, payload)
}
fn make_token_transfer(
@@ -650,23 +829,8 @@ pub mod test {
dest: PrincipalData,
amount: u64,
) -> StacksTransaction {
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
-
- let mut txn = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::TokenTransfer(dest, amount, TokenTransferMemo([0u8; 34])),
- );
-
- txn.chain_id = 0x80000000;
- txn.auth.set_origin_nonce(nonce);
- txn.set_post_condition_mode(TransactionPostConditionMode::Allow);
- txn.set_fee_rate(fee_rate);
-
- let mut tx_signer = StacksTransactionSigner::new(&txn);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ let payload = TransactionPayload::TokenTransfer(dest, amount, TokenTransferMemo([0u8; 34]));
+ make_tx(key, nonce, fee_rate, payload)
}
fn make_pox_lockup_contract(
@@ -685,7 +849,7 @@ pub mod test {
;; this contract stacks the stx given to it
(as-contract
- (contract-call? '{}.pox stack-stx amount-ustx pox-addr lock-period))
+ (contract-call? '{}.pox stack-stx amount-ustx pox-addr burn-block-height lock-period))
))
)
@@ -716,31 +880,18 @@ pub mod test {
addr_bytes: Hash160,
lock_period: u128,
) -> StacksTransaction {
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
- let mut pox_lockup = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_contract_call(
- contract_addr.clone(),
- name,
- "do-contract-lockup",
- vec![
- Value::UInt(amount),
- make_pox_addr(addr_version, addr_bytes),
- Value::UInt(lock_period),
- ],
- )
- .unwrap(),
- );
- pox_lockup.chain_id = 0x80000000;
- pox_lockup.auth.set_origin_nonce(nonce);
- pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow);
- pox_lockup.set_fee_rate(0);
-
- let mut tx_signer = StacksTransactionSigner::new(&pox_lockup);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ let payload = TransactionPayload::new_contract_call(
+ contract_addr.clone(),
+ name,
+ "do-contract-lockup",
+ vec![
+ Value::UInt(amount),
+ make_pox_addr(addr_version, addr_bytes),
+ Value::UInt(lock_period),
+ ],
+ )
+ .unwrap();
+ make_tx(key, nonce, 0, payload)
}
// call after make_pox_lockup_contract gets mined
@@ -751,49 +902,19 @@ pub mod test {
name: &str,
amount: u128,
) -> StacksTransaction {
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
- let mut pox_lockup = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_contract_call(
- contract_addr.clone(),
- name,
- "withdraw-stx",
- vec![Value::UInt(amount)],
- )
- .unwrap(),
- );
- pox_lockup.chain_id = 0x80000000;
- pox_lockup.auth.set_origin_nonce(nonce);
- pox_lockup.set_post_condition_mode(TransactionPostConditionMode::Allow);
- pox_lockup.set_fee_rate(0);
-
- let mut tx_signer = StacksTransactionSigner::new(&pox_lockup);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ let payload = TransactionPayload::new_contract_call(
+ contract_addr.clone(),
+ name,
+ "withdraw-stx",
+ vec![Value::UInt(amount)],
+ )
+ .unwrap();
+ make_tx(key, nonce, 0, payload)
}
fn make_pox_reject(key: &StacksPrivateKey, nonce: u64) -> StacksTransaction {
// (define-public (reject-pox))
- let auth = TransactionAuth::from_p2pkh(key).unwrap();
- let addr = auth.origin().address_testnet();
-
- let mut tx = StacksTransaction::new(
- TransactionVersion::Testnet,
- auth,
- TransactionPayload::new_contract_call(boot_code_addr(), "pox", "reject-pox", vec![])
- .unwrap(),
- );
-
- tx.chain_id = 0x80000000;
- tx.auth.set_origin_nonce(nonce);
- tx.set_post_condition_mode(TransactionPostConditionMode::Allow);
- tx.set_fee_rate(0);
-
- let mut tx_signer = StacksTransactionSigner::new(&tx);
- tx_signer.sign_origin(key).unwrap();
- tx_signer.get_tx().unwrap()
+ make_pox_contract_call(key, nonce, "reject-pox", vec![])
}
fn get_reward_addresses_with_par_tip(
@@ -803,7 +924,12 @@ pub mod test {
block_id: &StacksBlockId,
) -> Result, 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(
@@ -928,35 +1054,43 @@ pub mod test {
];
if tenure_id == 1 {
- let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1);
+ let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, tip.block_height);
block_txs.push(alice_lockup_1);
}
if tenure_id == 2 {
let alice_test_tx = make_bare_contract(&alice, 1, 0, "nested-stacker", &format!(
"(define-public (nested-stack-stx)
- (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS));
+ (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS));
block_txs.push(alice_test_tx);
}
if tenure_id == 8 {
// alice locks 512_000_000 STX through her contract
- let auth = TransactionAuth::from_p2pkh(&alice).unwrap();
- let addr = auth.origin().address_testnet();
- let mut contract_call = StacksTransaction::new(TransactionVersion::Testnet, auth,
- TransactionPayload::new_contract_call(key_to_stacks_addr(&alice),
- "nested-stacker",
- "nested-stack-stx",
- vec![]).unwrap());
- contract_call.chain_id = 0x80000000;
- contract_call.auth.set_origin_nonce(2);
- contract_call.set_post_condition_mode(TransactionPostConditionMode::Allow);
- contract_call.set_fee_rate(0);
-
- let mut tx_signer = StacksTransactionSigner::new(&contract_call);
- tx_signer.sign_origin(&alice).unwrap();
- let tx = tx_signer.get_tx().unwrap();
+ let cc_payload = TransactionPayload::new_contract_call(key_to_stacks_addr(&alice),
+ "nested-stacker",
+ "nested-stack-stx",
+ vec![]).unwrap();
+ let tx = make_tx(&alice, 2, 0, cc_payload.clone());
block_txs.push(tx);
+
+ // the above tx _should_ error, because alice hasn't authorized that contract to stack
+ // try again with auth -> deauth -> auth
+ let alice_contract: Value = contract_id(&key_to_stacks_addr(&alice), "nested-stacker").into();
+
+ let alice_allowance = make_pox_contract_call(&alice, 3, "allow-contract-caller", vec![alice_contract.clone(), Value::none()]);
+ let alice_disallowance = make_pox_contract_call(&alice, 4, "disallow-contract-caller", vec![alice_contract.clone()]);
+ block_txs.push(alice_allowance);
+ block_txs.push(alice_disallowance);
+
+ let tx = make_tx(&alice, 5, 0, cc_payload.clone());
+ block_txs.push(tx);
+
+ let alice_allowance = make_pox_contract_call(&alice, 6, "allow-contract-caller", vec![alice_contract.clone(), Value::none()]);
+ let tx = make_tx(&alice, 7, 0, cc_payload.clone()); // should be allowed!
+ block_txs.push(alice_allowance);
+ block_txs.push(tx);
+
}
let block_builder = StacksBlockBuilder::make_block_builder(&parent_tip, vrf_proof, tip.total_burn, microblock_pubkeyhash).unwrap();
@@ -1139,6 +1273,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
12,
+ tip.block_height,
);
block_txs.push(alice_lockup);
}
@@ -1186,7 +1321,7 @@ pub mod test {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
@@ -1252,7 +1387,7 @@ pub mod test {
// miner rewards increased liquid supply, so less than 25% is locked.
// minimum participation decreases.
assert!(total_liquid_ustx > 4 * 1024 * 1000000);
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
} else {
// still at 25% or more locked
assert!(total_liquid_ustx <= 4 * 1024 * 1000000);
@@ -1379,12 +1514,11 @@ pub mod test {
let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into());
assert_eq!(alice_balance, 1024 * 1000000);
}
- // stacking minimum should be floor(total-liquid-ustx / 20000)
let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
@@ -1452,7 +1586,7 @@ pub mod test {
// miner rewards increased liquid supply, so less than 25% is locked.
// minimum participation decreases.
assert!(total_liquid_ustx > 4 * 1024 * 1000000);
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
} else {
// still at 25% or more locked
assert!(total_liquid_ustx <= 4 * 1024 * 1000000);
@@ -1590,6 +1724,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
12,
+ tip.block_height,
);
block_txs.push(alice_lockup);
@@ -1601,6 +1736,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&bob).bytes,
12,
+ tip.block_height,
);
block_txs.push(bob_lockup);
}
@@ -1644,12 +1780,11 @@ pub mod test {
assert_eq!(bob_balance, 1024 * 1000000);
}
- // stacking minimum should be floor(total-liquid-ustx / 20000)
let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
@@ -1723,7 +1858,7 @@ pub mod test {
}
// well over 25% locked, so this is always true
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// two reward addresses, and they're Alice's and Bob's.
// They are present in sorted order
@@ -1785,61 +1920,66 @@ pub mod test {
if tenure_id == 1 {
// Alice locks up exactly 12.5% of the liquid STX supply, twice.
// Only the first one succeeds.
- let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12);
+ let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height);
block_txs.push(alice_lockup_1);
// will be rejected
- let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12);
+ let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height);
block_txs.push(alice_lockup_2);
+
+ // let's make some allowances for contract-calls through smart contracts
+ // so that the tests in tenure_id == 3 don't just fail on permission checks
+ let alice_test = contract_id(&key_to_stacks_addr(&alice), "alice-test").into();
+ let alice_allowance = make_pox_contract_call(&alice, 2, "allow-contract-caller", vec![alice_test, Value::none()]);
+
+ let bob_test = contract_id(&key_to_stacks_addr(&bob), "bob-test").into();
+ let bob_allowance = make_pox_contract_call(&bob, 0, "allow-contract-caller", vec![bob_test, Value::none()]);
+
+ let charlie_test = contract_id(&key_to_stacks_addr(&charlie), "charlie-test").into();
+ let charlie_allowance = make_pox_contract_call(&charlie, 0, "allow-contract-caller", vec![charlie_test, Value::none()]);
+
+ block_txs.push(alice_allowance);
+ block_txs.push(bob_allowance);
+ block_txs.push(charlie_allowance);
}
if tenure_id == 2 {
- // should fail -- Alice's PoX address is already in use, so Bob can't use it.
- let bob_test_tx = make_bare_contract(&bob, 0, 0, "bob-test", &format!(
- "(define-data-var bob-test-run bool false)
- (let (
- (res
- (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) u12))
- )
- (begin
- (asserts! (is-eq (err 12) res)
- (err res))
-
- (var-set bob-test-run true)
- ))
+ // should pass -- there's no problem with Bob adding more stacking power to Alice's PoX address
+ let bob_test_tx = make_bare_contract(&bob, 1, 0, "bob-test", &format!(
+ "(define-data-var test-run bool false)
+ (define-data-var test-result int -1)
+ (let ((result
+ (contract-call? '{}.pox stack-stx u256000000 (tuple (version 0x00) (hashbytes 0xae1593226f85e49a7eaff5b633ff687695438cc9)) burn-block-height u12)))
+ (var-set test-result
+ (match result ok_value -1 err_value err_value))
+ (var-set test-run true))
", STACKS_BOOT_CODE_CONTRACT_ADDRESS));
block_txs.push(bob_test_tx);
// should fail -- Alice has already stacked.
- let alice_test_tx = make_bare_contract(&alice, 2, 0, "alice-test", &format!(
- "(define-data-var alice-test-run bool false)
- (let (
- (res
- (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) u12))
- )
- (begin
- (asserts! (is-eq (err 3) res)
- (err res))
-
- (var-set alice-test-run true)
- ))
+ // expect err 3
+ let alice_test_tx = make_bare_contract(&alice, 3, 0, "alice-test", &format!(
+ "(define-data-var test-run bool false)
+ (define-data-var test-result int -1)
+ (let ((result
+ (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u12)))
+ (var-set test-result
+ (match result ok_value -1 err_value err_value))
+ (var-set test-run true))
", STACKS_BOOT_CODE_CONTRACT_ADDRESS));
block_txs.push(alice_test_tx);
// should fail -- Charlie doesn't have enough uSTX
- let charlie_test_tx = make_bare_contract(&charlie, 0, 0, "charlie-test", &format!(
- "(define-data-var charlie-test-run bool false)
- (let (
- (res
- (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) u12))
- )
- (begin
- (asserts! (is-eq (err 1) res)
- (err res))
-
- (var-set charlie-test-run true)
- ))
+ // expect err 1
+ let charlie_test_tx = make_bare_contract(&charlie, 1, 0, "charlie-test", &format!(
+ "(define-data-var test-run bool false)
+ (define-data-var test-result int -1)
+ (let ((result
+ (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) burn-block-height u12)))
+ (var-set test-result
+ (match result ok_value -1 err_value err_value))
+ (var-set test-run true))
", STACKS_BOOT_CODE_CONTRACT_ADDRESS));
block_txs.push(charlie_test_tx);
@@ -1906,19 +2046,42 @@ pub mod test {
&mut peer,
&key_to_stacks_addr(&alice),
"alice-test",
- "(var-get alice-test-run)",
+ "(var-get test-run)",
+ );
+ let bob_test_result = eval_contract_at_tip(
+ &mut peer,
+ &key_to_stacks_addr(&bob),
+ "bob-test",
+ "(var-get test-run)",
+ );
+ let charlie_test_result = eval_contract_at_tip(
+ &mut peer,
+ &key_to_stacks_addr(&charlie),
+ "charlie-test",
+ "(var-get test-run)",
+ );
+
+ assert!(alice_test_result.expect_bool());
+ assert!(bob_test_result.expect_bool());
+ assert!(charlie_test_result.expect_bool());
+
+ let alice_test_result = eval_contract_at_tip(
+ &mut peer,
+ &key_to_stacks_addr(&alice),
+ "alice-test",
+ "(var-get test-result)",
);
let bob_test_result = eval_contract_at_tip(
&mut peer,
&key_to_stacks_addr(&bob),
"bob-test",
- "(var-get bob-test-run)",
+ "(var-get test-result)",
);
let charlie_test_result = eval_contract_at_tip(
&mut peer,
&key_to_stacks_addr(&charlie),
"charlie-test",
- "(var-get charlie-test-run)",
+ "(var-get test-result)",
);
eprintln!(
@@ -1926,9 +2089,9 @@ pub mod test {
&alice_test_result, &bob_test_result, &charlie_test_result
);
- assert!(alice_test_result.expect_bool());
- assert!(bob_test_result.expect_bool());
- assert!(charlie_test_result.expect_bool());
+ assert_eq!(bob_test_result, Value::Int(-1));
+ assert_eq!(alice_test_result, Value::Int(3));
+ assert_eq!(charlie_test_result, Value::Int(1));
}
}
}
@@ -1979,6 +2142,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
1,
+ tip.block_height,
);
block_txs.push(alice_lockup);
}
@@ -2018,12 +2182,11 @@ pub mod test {
assert_eq!(alice_balance, 1024 * 1000000);
}
- // stacking minimum should be floor(total-liquid-ustx / 20000)
let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
@@ -2087,7 +2250,7 @@ pub mod test {
// miner rewards increased liquid supply, so less than 25% is locked.
// minimum participation decreases.
assert!(total_liquid_ustx > 4 * 1024 * 1000000);
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
}
if cur_reward_cycle == alice_reward_cycle {
@@ -2138,7 +2301,7 @@ pub mod test {
assert_eq!(reward_addrs.len(), 0);
// min STX is reset
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// Unlock is lazy
let alice_account =
@@ -2214,6 +2377,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
1,
+ tip.block_height,
);
block_txs.push(alice_lockup);
@@ -2241,6 +2405,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
1,
+ tip.block_height,
);
block_txs.push(alice_lockup);
@@ -2319,12 +2484,11 @@ pub mod test {
assert_eq!(charlie_balance, 0);
}
- // stacking minimum should be floor(total-liquid-ustx / 20000)
let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
assert_eq!(reward_addrs.len(), 0);
@@ -2348,13 +2512,13 @@ pub mod test {
assert_eq!(charlie_balance, 1024 * 1000000);
} else if tenure_id == 11 {
// should have just re-locked
- // stacking minimum should be floor(total-liquid-ustx / 20000), since we haven't
+ // stacking minimum should be minimum, since we haven't
// locked up 25% of the tokens yet
let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
chainstate.get_stacking_minimum(sortdb, &tip_index_block)
})
.unwrap();
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
assert_eq!(reward_addrs.len(), 0);
@@ -2377,13 +2541,13 @@ pub mod test {
// miner rewards increased liquid supply, so less than 25% is locked.
// minimum participation decreases.
assert!(total_liquid_ustx > 4 * 1024 * 1000000);
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
} else if tenure_id >= 1 && cur_reward_cycle < first_reward_cycle {
// still at 25% or more locked
assert!(total_liquid_ustx <= 4 * 1024 * 1000000);
} else if tenure_id < 1 {
// nothing locked yet
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
}
if first_reward_cycle > 0 && second_reward_cycle == 0 {
@@ -2475,7 +2639,7 @@ pub mod test {
assert_eq!(reward_addrs.len(), 0);
// min STX is reset
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// Unlock is lazy
let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into());
@@ -2596,7 +2760,7 @@ pub mod test {
assert_eq!(reward_addrs.len(), 0);
// min STX is reset
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// Unlock is lazy
let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into());
@@ -2684,6 +2848,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&alice).bytes,
1,
+ tip.block_height,
);
block_txs.push(alice_lockup);
@@ -2694,6 +2859,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&bob).bytes,
1,
+ tip.block_height,
);
block_txs.push(bob_lockup);
@@ -2704,6 +2870,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&charlie).bytes,
1,
+ tip.block_height,
);
block_txs.push(charlie_lockup);
@@ -2714,6 +2881,7 @@ pub mod test {
AddressHashMode::SerializeP2PKH,
key_to_stacks_addr(&danielle).bytes,
1,
+ tip.block_height,
);
block_txs.push(danielle_lockup);
@@ -2922,8 +3090,7 @@ pub mod test {
assert_eq!(balance, expected_balance);
}
}
- // stacking minimum should be floor(total-liquid-ustx / 20000)
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| {
@@ -3039,7 +3206,7 @@ pub mod test {
assert_eq!(reward_addrs.len(), 0);
// min STX is reset
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
}
}
@@ -3111,25 +3278,33 @@ pub mod test {
if tenure_id == 1 {
// Alice locks up exactly 25% of the liquid STX supply, so this should succeed.
- let alice_lockup = make_pox_lockup(&alice, 0, 1024 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12);
+ let alice_lockup = make_pox_lockup(&alice, 0, 1024 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height);
block_txs.push(alice_lockup);
// Bob rejects with exactly 25% of the liquid STX supply (shouldn't affect
// anything).
let bob_reject = make_pox_reject(&bob, 0);
block_txs.push(bob_reject);
- }
- else if tenure_id == 2 {
+ } else if tenure_id == 2 {
// Charlie rejects
+ // this _should_ be included in the block
let charlie_reject = make_pox_reject(&charlie, 0);
block_txs.push(charlie_reject);
+ // allowance for the contract-caller
+ // this _should_ be included in the block
+ let charlie_contract: Value = contract_id(&key_to_stacks_addr(&charlie), "charlie-try-stack").into();
+ let charlie_allowance = make_pox_contract_call(&charlie, 1, "allow-contract-caller",
+ vec![charlie_contract, Value::none()]);
+ block_txs.push(charlie_allowance);
+
// Charlie tries to stack, but it should fail.
// Specifically, (stack-stx) should fail with (err 17).
// If it's the case, then this tx will NOT be mined.
- let charlie_stack = make_bare_contract(&charlie, 1, 0, "charlie-try-stack",
+ // Note: this behavior is a bug in the miner and block processor: see issue #?
+ let charlie_stack = make_bare_contract(&charlie, 2, 0, "charlie-try-stack",
&format!(
- "(asserts! (not (is-eq (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} u1) (err 17))) (err 1))",
+ "(asserts! (not (is-eq (print (contract-call? '{}.pox stack-stx u1 {{ version: 0x01, hashbytes: 0x1111111111111111111111111111111111111111 }} burn-block-height u1)) (err 17))) (err 1))",
boot_code_addr()));
block_txs.push(charlie_stack);
@@ -3140,7 +3315,7 @@ pub mod test {
// If it's the case, then this tx will NOT be mined
let alice_reject = make_bare_contract(&alice, 1, 0, "alice-try-reject",
&format!(
- "(asserts! (not (is-eq (contract-call? '{}.pox reject-pox) (err 3))) (err 1))",
+ "(asserts! (not (is-eq (print (contract-call? '{}.pox reject-pox)) (err 3))) (err 1))",
boot_code_addr()));
block_txs.push(alice_reject);
@@ -3148,9 +3323,9 @@ pub mod test {
// Charlie tries to reject again, but it should fail.
// Specifically, (reject-pox) should fail with (err 17).
// If it's the case, then this tx will NOT be mined.
- let charlie_reject = make_bare_contract(&charlie, 1, 0, "charlie-try-reject",
+ let charlie_reject = make_bare_contract(&charlie, 3, 0, "charlie-try-reject",
&format!(
- "(asserts! (not (is-eq (contract-call? '{}.pox reject-pox) (err 17))) (err 1))",
+ "(asserts! (not (is-eq (print (contract-call? '{}.pox reject-pox)) (err 17))) (err 1))",
boot_code_addr()));
block_txs.push(charlie_reject);
@@ -3160,7 +3335,8 @@ pub mod test {
let (anchored_block, _size, _cost) = StacksBlockBuilder::make_anchored_block_from_txs(block_builder, chainstate, &sortdb.index_conn(), block_txs).unwrap();
if tenure_id == 2 {
- assert_eq!(anchored_block.txs.len(), 2);
+ // block should be coinbase tx + 2 allowed txs
+ assert_eq!(anchored_block.txs.len(), 3);
}
(anchored_block, vec![])
@@ -3213,7 +3389,7 @@ pub mod test {
assert_eq!(alice_account.stx_balance.unlock_height, 0);
}
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
// no reward addresses
assert_eq!(reward_addrs.len(), 0);
@@ -3273,7 +3449,7 @@ pub mod test {
// miner rewards increased liquid supply, so less than 25% is locked.
// minimum participation decreases.
assert!(total_liquid_ustx > 4 * 1024 * 1000000);
- assert_eq!(min_ustx, total_liquid_ustx / 20000);
+ assert_eq!(min_ustx, total_liquid_ustx / 480);
} else {
// still at 25% or more locked
assert!(total_liquid_ustx <= 4 * 1024 * 1000000);
diff --git a/src/chainstate/stacks/boot/pox.clar b/src/chainstate/stacks/boot/pox.clar
index c3ad688e280..28db2338289 100644
--- a/src/chainstate/stacks/boot/pox.clar
+++ b/src/chainstate/stacks/boot/pox.clar
@@ -14,6 +14,11 @@
(define-constant ERR_STACKING_ALREADY_REJECTED 17)
(define-constant ERR_STACKING_INVALID_AMOUNT 18)
(define-constant ERR_NOT_ALLOWED 19)
+(define-constant ERR_STACKING_ALREADY_DELEGATED 20)
+(define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21)
+(define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22)
+(define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23)
+(define-constant ERR_INVALID_START_BURN_HEIGHT 24)
;; PoX disabling threshold (a percent)
(define-constant POX_REJECTION_FRACTION u25)
@@ -30,7 +35,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))
(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)
@@ -64,6 +69,22 @@
)
)
+;; Delegation relationships
+(define-map delegation-state
+ ((stacker principal))
+ ((amount-ustx uint) ;; how many uSTX delegated?
+ (delegated-to principal) ;; who are we delegating?
+ (until-burn-ht (optional uint)) ;; how long does the delegation last?
+ ;; does the delegate _need_ to use a specific
+ ;; pox recipient address?
+ (pox-addr (optional { version: (buff 1),
+ hashbytes: (buff 20) }))))
+
+;; allowed contract-callers
+(define-map allowance-contract-callers
+ ((sender principal) (contract-caller principal))
+ ((until-burn-ht (optional uint))))
+
;; How many uSTX are stacked in a given reward cycle.
;; Updated when a new PoX address is registered, or when more STX are granted
;; to it.
@@ -87,16 +108,15 @@
((len uint))
)
-;; When is a PoX address active?
-;; Used to check of a PoX address is registered or not in a given
-;; reward cycle.
-(define-map pox-addr-reward-cycles
- ((pox-addr (tuple (version (buff 1)) (hashbytes (buff 20)))))
- (
- (first-reward-cycle uint)
- (num-cycles uint)
- )
-)
+;; how much has been locked up for this address before
+;; committing?
+;; this map allows stackers to stack amounts < minimum
+;; by paying the cost of aggregation during the commit
+(define-map partial-stacked-by-cycle
+ ((pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))
+ (reward-cycle uint)
+ (sender principal))
+ ((stacked-amount uint)))
;; Amount of uSTX that reject PoX, by reward cycle
(define-map stacking-rejection
@@ -153,8 +173,30 @@
)
;; no state at all
none
- )
-)
+ ))
+
+(define-private (check-caller-allowed)
+ (or (is-eq tx-sender contract-caller)
+ (let ((caller-allowed
+ ;; if not in the caller map, return false
+ (unwrap! (map-get? allowance-contract-callers
+ { sender: tx-sender, contract-caller: contract-caller })
+ false)))
+ ;; is the caller allowance expired?
+ (if (< burn-block-height (unwrap! (get until-burn-ht caller-allowed) true))
+ false
+ true))))
+
+(define-private (get-check-delegation (stacker principal))
+ (let ((delegation-info (try! (map-get? delegation-state { stacker: stacker }))))
+ ;; did the existing delegation expire?
+ (if (match (get until-burn-ht delegation-info)
+ until-burn-ht (> burn-block-height until-burn-ht)
+ false)
+ ;; it expired, return none
+ none
+ ;; delegation is active
+ (some delegation-info))))
;; Get the size of the reward set for a reward cycle.
;; Note that this does _not_ return duplicate PoX addresses.
@@ -167,26 +209,6 @@
u0
(get len (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle }))))
-;; Is a PoX address registered anywhere in a given range of reward cycles?
-;; Checkes the integer range [reward-cycle-start, reward-cycle-start + num-cycles)
-;; Returns true if it's registered in at least one reward cycle in the given range.
-;; Returns false if it's not registered in any reward cycle in the given range.
-(define-private (is-pox-addr-registered (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))
- (reward-cycle-start uint)
- (num-cycles uint))
- (let (
- (pox-addr-range-opt (map-get? pox-addr-reward-cycles { pox-addr: pox-addr }))
- )
- (match pox-addr-range-opt
- ;; some range
- pox-addr-range
- (and (>= (+ reward-cycle-start num-cycles) (get first-reward-cycle pox-addr-range))
- (< reward-cycle-start (+ (get first-reward-cycle pox-addr-range) (get num-cycles pox-addr-range))))
- ;; none
- false
- ))
-)
-
;; How many rejection votes have we been accumulating for the next block
(define-private (next-cycle-rejection-votes)
(default-to
@@ -236,13 +258,14 @@
(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)))
{
pox-addr: (get pox-addr params),
first-reward-cycle: (get first-reward-cycle params),
- num-cycles: (get num-cycles params),
+ num-cycles: num-cycles,
amount-ustx: (get amount-ustx params),
- i: (if (< i (get num-cycles 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
@@ -263,36 +286,63 @@
;; Add a PoX address to a given sequence of reward cycle lists.
;; A PoX address can be added to at most 12 consecutive cycles.
;; No checking is done.
-;; Returns the number of reward cycles added
(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))
- )
+ (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11)))
;; 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))
-
- ;; mark address in use over this range
- (map-set pox-addr-reward-cycles
- { pox-addr: pox-addr }
- { first-reward-cycle: first-reward-cycle, num-cycles: num-cycles }
- )
- (ok true)
- )
-)
+ (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)))
+
+(define-private (add-pox-partial-stacked-to-ith-cycle
+ (cycle-index uint)
+ (params { pox-addr: { version: (buff 1), hashbytes: (buff 20) },
+ reward-cycle: uint,
+ num-cycles: uint,
+ amount-ustx: uint }))
+ (let ((pox-addr (get pox-addr params))
+ (num-cycles (get num-cycles params))
+ (reward-cycle (get reward-cycle params))
+ (amount-ustx (get amount-ustx params)))
+ (let ((current-amount
+ (default-to u0
+ (get stacked-amount
+ (map-get? partial-stacked-by-cycle { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle })))))
+ (if (>= cycle-index num-cycles)
+ ;; do not add to cycles >= cycle-index
+ false
+ ;; otherwise, add to the partial-stacked-by-cycle
+ (map-set partial-stacked-by-cycle
+ { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle }
+ { stacked-amount: (+ amount-ustx current-amount) }))
+ ;; produce the next params tuple
+ { pox-addr: pox-addr,
+ reward-cycle: (+ u1 reward-cycle),
+ num-cycles: num-cycles,
+ amount-ustx: amount-ustx })))
+
+;; Add a PoX address to a given sequence of partial reward cycle lists.
+;; A PoX address can be added to at most 12 consecutive cycles.
+;; No checking is done.
+(define-private (add-pox-partial-stacked (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)))
+ (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes
+ { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx })
+ true))
;; What is the minimum number of uSTX to be stacked in the given reward cycle?
;; Used internally by the Stacks node, and visible publicly.
(define-read-only (get-stacking-minimum)
- (/ stx-liquid-supply u20000)
-)
+ (/ stx-liquid-supply STACKING_THRESHOLD_25))
;; Is the address mode valid for a PoX burn address?
(define-private (check-pox-addr-version (version (buff 1)))
@@ -314,37 +364,58 @@
(amount-ustx uint)
(first-reward-cycle uint)
(num-cycles uint))
- (let ((is-registered (is-pox-addr-registered pox-addr first-reward-cycle num-cycles)))
- ;; amount must be valid
- (asserts! (> amount-ustx u0)
- (err ERR_STACKING_INVALID_AMOUNT))
-
- ;; tx-sender principal must not have rejected in this upcoming reward cycle
- (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle))
- (err ERR_STACKING_ALREADY_REJECTED))
-
- ;; can't be registered yet
- (asserts! (not is-registered)
- (err ERR_STACKING_POX_ADDRESS_IN_USE))
+ (begin
+ ;; minimum uSTX must be met
+ (asserts! (<= (print (get-stacking-minimum)) amount-ustx)
+ (err ERR_STACKING_THRESHOLD_NOT_MET))
- ;; the Stacker must have sufficient unlocked funds
- (asserts! (>= (stx-get-balance tx-sender) amount-ustx)
- (err ERR_STACKING_INSUFFICIENT_FUNDS))
-
- ;; minimum uSTX must be met
- (asserts! (<= (get-stacking-minimum) amount-ustx)
- (err ERR_STACKING_THRESHOLD_NOT_MET))
-
- ;; lock period must be in acceptable range.
- (asserts! (check-pox-lock-period num-cycles)
- (err ERR_STACKING_INVALID_LOCK_PERIOD))
+ (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles)))
- ;; address version must be valid
- (asserts! (check-pox-addr-version (get version pox-addr))
- (err ERR_STACKING_INVALID_POX_ADDRESS))
-
- (ok true))
-)
+;; Evaluate if a participant can stack an amount of STX for a given period.
+;; This method is designed as a read-only method so that it can be used as
+;; a set of guard conditions and also as a read-only RPC call that can be
+;; performed beforehand.
+(define-read-only (minimal-can-stack-stx
+ (pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))
+ (amount-ustx uint)
+ (first-reward-cycle uint)
+ (num-cycles uint))
+ (begin
+ ;; amount must be valid
+ (asserts! (> amount-ustx u0)
+ (err ERR_STACKING_INVALID_AMOUNT))
+
+ ;; sender principal must not have rejected in this upcoming reward cycle
+ (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle))
+ (err ERR_STACKING_ALREADY_REJECTED))
+
+ ;; lock period must be in acceptable range.
+ (asserts! (check-pox-lock-period num-cycles)
+ (err ERR_STACKING_INVALID_LOCK_PERIOD))
+
+ ;; address version must be valid
+ (asserts! (check-pox-addr-version (get version pox-addr))
+ (err ERR_STACKING_INVALID_POX_ADDRESS))
+ (ok true)))
+
+;; Revoke contract-caller authorization to call stacking methods
+(define-public (disallow-contract-caller (caller principal))
+ (begin
+ (asserts! (is-eq tx-sender contract-caller)
+ (err ERR_STACKING_PERMISSION_DENIED))
+ (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller }))))
+
+;; Give a contract-caller authorization to call stacking methods
+;; normally, stacking methods may only be invoked by _direct_ transactions
+;; (i.e., the tx-sender issues a direct contract-call to the stacking methods)
+;; by issuing an allowance, the tx-sender may call through the allowed contract
+(define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint)))
+ (begin
+ (asserts! (is-eq tx-sender contract-caller)
+ (err ERR_STACKING_PERMISSION_DENIED))
+ (ok (map-set allowance-contract-callers
+ { sender: tx-sender, contract-caller: caller }
+ { until-burn-ht: until-burn-ht }))))
;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX).
;; The STX will be locked for the given number of reward cycles (lock-period).
@@ -355,18 +426,39 @@
;; at the time this method is called.
;; * You may need to increase the amount of uSTX locked up later, since the minimum uSTX threshold
;; may increase between reward cycles.
+;; * The Stacker will receive rewards in the reward cycle following `start-burn-ht`.
+;; Importantly, `start-burn-ht` may not be further into the future than the next reward cycle,
+;; and in most cases should be set to the current burn block height.
;;
;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically.
(define-public (stack-stx (amount-ustx uint)
(pox-addr (tuple (version (buff 1)) (hashbytes (buff 20))))
+ (start-burn-ht uint)
(lock-period uint))
;; this stacker's first reward cycle is the _next_ reward cycle
- (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))))
+ (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle)))
+ (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))))
+ ;; the start-burn-ht must result in the next reward cycle, do not allow stackers
+ ;; to "post-date" their `stack-stx` transaction
+ (asserts! (is-eq first-reward-cycle specified-reward-cycle)
+ (err ERR_INVALID_START_BURN_HEIGHT))
+
+ ;; must be called directly by the tx-sender or by an allowed contract-caller
+ (asserts! (check-caller-allowed)
+ (err ERR_STACKING_PERMISSION_DENIED))
;; tx-sender principal must not be stacking
(asserts! (is-none (get-stacker-info tx-sender))
(err ERR_STACKING_ALREADY_STACKED))
+ ;; tx-sender must not be delegating
+ (asserts! (is-none (get-check-delegation tx-sender))
+ (err ERR_STACKING_ALREADY_DELEGATED))
+
+ ;; the Stacker must have sufficient unlocked funds
+ (asserts! (>= (stx-get-balance tx-sender) amount-ustx)
+ (err ERR_STACKING_INSUFFICIENT_FUNDS))
+
;; ensure that stacking can be performed
(try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period))
@@ -376,8 +468,7 @@
;; add stacker record
(map-set stacking-state
{ stacker: tx-sender }
- {
- amount-ustx: amount-ustx,
+ { amount-ustx: amount-ustx,
pox-addr: pox-addr,
first-reward-cycle: first-reward-cycle,
lock-period: lock-period })
@@ -386,6 +477,149 @@
(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
+ ;; must be called directly by the tx-sender or by an allowed contract-caller
+ (asserts! (check-caller-allowed)
+ (err ERR_STACKING_PERMISSION_DENIED))
+ (ok (map-delete delegation-state { stacker: tx-sender }))))
+
+;; Delegate to `delegate-to` the ability to stack from a given address.
+;; This method _does not_ lock the funds, rather, it allows the delegate
+;; to issue the stacking lock.
+;; The caller specifies:
+;; * amount-ustx: the total amount of ustx the delegate may be allowed to lock
+;; * until-burn-ht: an optional burn height at which this delegation expiration
+;; * pox-addr: an optional address to which any rewards *must* be sent
+(define-public (delegate-stx (amount-ustx uint)
+ (delegate-to principal)
+ (until-burn-ht (optional uint))
+ (pox-addr (optional { version: (buff 1),
+ hashbytes: (buff 20) })))
+ (begin
+ ;; must be called directly by the tx-sender or by an allowed contract-caller
+ (asserts! (check-caller-allowed)
+ (err ERR_STACKING_PERMISSION_DENIED))
+
+ ;; tx-sender principal must not be stacking
+ (asserts! (is-none (get-stacker-info tx-sender))
+ (err ERR_STACKING_ALREADY_STACKED))
+
+ ;; tx-sender must not be delegating
+ (asserts! (is-none (get-check-delegation tx-sender))
+ (err ERR_STACKING_ALREADY_DELEGATED))
+
+ ;; add delegation record
+ (map-set delegation-state
+ { stacker: tx-sender }
+ { amount-ustx: amount-ustx,
+ delegated-to: delegate-to,
+ until-burn-ht: until-burn-ht,
+ pox-addr: pox-addr })
+
+ (ok true)))
+
+;; Commit partially stacked STX.
+;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions,
+;; so long as: 1. The pox-addr is the same.
+;; 2. This "commit" transaction is called _before_ the PoX anchor block.
+;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold,
+;; but does not require it be all locked up within a single transaction
+(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 20) })
+ (reward-cycle uint))
+ (let ((partial-stacked
+ ;; fetch the partial commitments
+ (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle })
+ (err ERR_STACKING_NO_SUCH_PRINCIPAL))))
+ ;; must be called directly by the tx-sender or by an allowed contract-caller
+ (asserts! (check-caller-allowed)
+ (err ERR_STACKING_PERMISSION_DENIED))
+ (let ((amount-ustx (get stacked-amount partial-stacked)))
+ (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1))
+ ;; add the pox addr to the reward cycle
+ (add-pox-addr-to-ith-reward-cycle
+ u0
+ { pox-addr: pox-addr,
+ first-reward-cycle: reward-cycle,
+ num-cycles: u1,
+ amount-ustx: amount-ustx,
+ i: u0 })
+ ;; don't update the stacking-state map,
+ ;; because it _already has_ this stacker's state
+ ;; don't lock the STX, because the STX is already locked
+ ;;
+ ;; clear the partial-stacked state
+ (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle })
+ (ok true))))
+
+;; As a delegate, stack the given principal's STX using partial-stacked-by-cycle
+;; Once the delegate has stacked > minimum, the delegate should call stack-aggregation-commit
+(define-public (delegate-stack-stx (stacker principal)
+ (amount-ustx uint)
+ (pox-addr { version: (buff 1), hashbytes: (buff 20) })
+ (start-burn-ht uint)
+ (lock-period uint))
+ ;; this stacker's first reward cycle is the _next_ reward cycle
+ (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle)))
+ (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht)))
+ (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period))))
+ ;; the start-burn-ht must result in the next reward cycle, do not allow stackers
+ ;; to "post-date" their `stack-stx` transaction
+ (asserts! (is-eq first-reward-cycle specified-reward-cycle)
+ (err ERR_INVALID_START_BURN_HEIGHT))
+
+ ;; must be called directly by the tx-sender or by an allowed contract-caller
+ (asserts! (check-caller-allowed)
+ (err ERR_STACKING_PERMISSION_DENIED))
+
+ ;; stacker must have delegated to the caller
+ (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED))))
+ ;; must have delegated to tx-sender
+ (asserts! (is-eq (get delegated-to delegation-info) tx-sender)
+ (err ERR_STACKING_PERMISSION_DENIED))
+ ;; must have delegated enough stx
+ (asserts! (>= (get amount-ustx delegation-info) amount-ustx)
+ (err ERR_DELEGATION_TOO_MUCH_LOCKED))
+ ;; if pox-addr is set, must be equal to pox-addr
+ (asserts! (match (get pox-addr delegation-info)
+ specified-pox-addr (is-eq pox-addr specified-pox-addr)
+ true)
+ (err ERR_DELEGATION_POX_ADDR_REQUIRED))
+ ;; delegation must not expire before lock period
+ (asserts! (match (get until-burn-ht delegation-info)
+ until-burn-ht (>= until-burn-ht
+ unlock-burn-height)
+ true)
+ (err ERR_DELEGATION_EXPIRES_DURING_LOCK)))
+
+ ;; stacker principal must not be stacking
+ (asserts! (is-none (get-stacker-info stacker))
+ (err ERR_STACKING_ALREADY_STACKED))
+
+ ;; the Stacker must have sufficient unlocked funds
+ (asserts! (>= (stx-get-balance stacker) amount-ustx)
+ (err ERR_STACKING_INSUFFICIENT_FUNDS))
+
+ ;; ensure that stacking can be performed
+ (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period))
+
+ ;; register the PoX address with the amount stacked via partial stacking
+ ;; before it can be included in the reward set, this must be committed!
+ (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx)
+
+ ;; add stacker record
+ (map-set stacking-state
+ { stacker: stacker }
+ { 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: stacker,
+ lock-amount: amount-ustx,
+ unlock-burn-height: unlock-burn-height })))
+
;; Reject Stacking for this reward cycle.
;; tx-sender votes all its uSTX for rejection.
;; Note that unlike PoX, rejecting PoX does not lock the tx-sender's
@@ -431,4 +665,4 @@
current-rejection-votes: (next-cycle-rejection-votes),
total-liquid-supply-ustx: stx-liquid-supply,
})
-)
\ No newline at end of file
+)
diff --git a/src/chainstate/stacks/db/headers.rs b/src/chainstate/stacks/db/headers.rs
index f299673c448..a3920f6d66d 100644
--- a/src/chainstate/stacks/db/headers.rs
+++ b/src/chainstate/stacks/db/headers.rs
@@ -33,7 +33,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;
@@ -275,14 +276,10 @@ impl StacksChainState {
index_block_hash: &StacksBlockId,
) -> Result