Skip to content

Commit 878bf5b

Browse files
authored
Merge pull request #1985 from blockstack/fix/1984
Fix 1984
2 parents e9e3cfb + ed87aac commit 878bf5b

File tree

15 files changed

+805
-277
lines changed

15 files changed

+805
-277
lines changed

sip/sip-007-stacking-consensus.md

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ Address validity is determined according to two different rules:
191191
descendant of the anchor block*, all of the miner's commitment
192192
funds must be burnt.
193193
2. If a miner is building off a descendant of the anchor block, the
194-
miner must send commitment funds to 5 addresses from the reward
194+
miner must send commitment funds to 2 addresses from the reward
195195
set, chosen as follows:
196196
* Use the verifiable random function (also used by sortition) to
197-
choose 5 addresses from the reward set. These 5 addresses are
197+
choose 2 addresses from the reward set. These 2 addresses are
198198
the reward addresses for this block.
199199
* Once addresses have been chosen for a block, these addresses are
200200
removed from the reward set, so that future blocks in the reward
@@ -217,24 +217,25 @@ addresses for a reward cycle, then each miner commitment would have
217217

218218
## Adjusting Reward Threshold Based on Participation
219219

220-
Each reward cycle may transfer miner funds to up to 5000 Bitcoin
221-
addresses. To ensure that this number of addresses is sufficient to
222-
cover the pool of participants (given 100% participation of liquid
223-
STX), the threshold for participation must be 0.02% (1/5000th) of the
224-
liquid supply of STX. However, if participation is _lower_ than 100%,
225-
the reward pool could admit lower STX holders. The Stacking protocol
226-
specifies **2 operating levels**:
220+
Each reward cycle may transfer miner funds to up to 4000 Bitcoin
221+
addresses (2 addresses in a 2000 burn block cycle). To ensure that
222+
this number of addresses is sufficient to cover the pool of
223+
participants (given 100% participation of liquid STX), the threshold
224+
for participation must be 0.025% (1/4000th) of the liquid supply of
225+
STX. However, if participation is _lower_ than 100%, the reward pool
226+
could admit lower STX holders. The Stacking protocol specifies **2
227+
operating levels**:
227228

228229
* **25%** If fewer than `0.25 * STX_LIQUID_SUPPLY` STX participate in
229230
a reward cycle, participant wallets controlling `x` STX may include
230-
`floor(x / (0.00005*STX_LIQUID_SUPPLY))` addresses in the reward set.
231-
That is, the minimum participation threshold is 1/20,000th of the liquid
231+
`floor(x / (0.0000625*STX_LIQUID_SUPPLY))` addresses in the reward set.
232+
That is, the minimum participation threshold is 1/16,000th of the liquid
232233
supply.
233234
* **25%-100%** If between `0.25 * STX_LIQUID_SUPPLY` and `1.0 *
234235
STX_LIQUID_SUPPLY` STX participate in a reward cycle, the reward
235236
threshold is optimized in order to maximize the number of slots that
236237
are filled. That is, the minimum threshold `T` for participation will be
237-
roughly 1/5,000th of the participating STX (adjusted in increments
238+
roughly 1/4,000th of the participating STX (adjusted in increments
238239
of 10,000 STX). Participant wallets controlling `x` STX may
239240
include `floor(x / T)` addresses in the
240241
reward set.
@@ -517,10 +518,6 @@ the second through nth outputs:
517518
The order of these addresses does not matter. Each of these outputs must receive the
518519
same amount of BTC.
519520
c. If the number of remaining addresses in the reward set N is less than M, then the leader
520-
block commit transaction must burn BTC:
521-
i. If N > 0, then the (N+2)nd output must be a burn output, and it must burn
522-
(M-N) * (the amount of BTC transfered to each of the first N outputs)
523-
ii. If N == 0, then the 2nd output must be a burn output, and the amount burned
524-
by this output will be counted as the amount committed to by the block commit.
525-
2. Otherwise, the 2nd output must be a burn output, and the amount burned by this output will be
526-
counted as the amount committed to by the block commit.
521+
block commit transaction must burn BTC by including (M-N) burn outputs.
522+
2. Otherwise, the second through (M+1)th output must be burn addresses, and the amount burned by
523+
these outputs will be counted as the amount committed to by the block commit.

src/burnchains/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use chainstate::stacks::StacksPublicKey;
5151

5252
use chainstate::burn::db::sortdb::PoxId;
5353
use chainstate::burn::distribution::BurnSamplePoint;
54+
use chainstate::burn::operations::leader_block_commit::OUTPUTS_PER_COMMIT;
5455
use chainstate::burn::operations::BlockstackOperationType;
5556
use chainstate::burn::operations::Error as op_error;
5657
use chainstate::burn::operations::LeaderKeyRegisterOp;
@@ -311,7 +312,7 @@ impl PoxConstants {
311312
}
312313

313314
pub fn reward_slots(&self) -> u32 {
314-
self.reward_cycle_length
315+
self.reward_cycle_length * (OUTPUTS_PER_COMMIT as u32)
315316
}
316317

317318
/// is participating_ustx enough to engage in PoX in the next reward cycle?

src/chainstate/burn/db/sortdb.rs

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ impl<'a> SortitionHandleTx<'a> {
973973
/// * The reward cycle had an anchor block, but it isn't known by this node.
974974
/// * The reward cycle did not have anchor block
975975
/// * The Stacking recipient set is empty (either because this reward cycle has already exhausted the set of addresses or because no one ever Stacked).
976-
fn pick_recipient(
976+
fn pick_recipients(
977977
&mut self,
978978
reward_set_vrf_seed: &SortitionHash,
979979
next_pox_info: Option<&RewardCycleInfo>,
@@ -986,20 +986,26 @@ impl<'a> SortitionHandleTx<'a> {
986986
return Ok(None);
987987
}
988988

989-
let chosen_recipient = reward_set_vrf_seed.choose(
989+
if OUTPUTS_PER_COMMIT != 2 {
990+
unreachable!("BUG: PoX reward address selection only implemented for OUTPUTS_PER_COMMIT = 2");
991+
}
992+
993+
let chosen_recipients = reward_set_vrf_seed.choose_two(
990994
reward_set
991995
.len()
992996
.try_into()
993997
.expect("BUG: u32 overflow in PoX outputs per commit"),
994998
);
995999

996-
let recipient = (
997-
reward_set[chosen_recipient as usize],
998-
u16::try_from(chosen_recipient).unwrap(),
999-
);
10001000
Ok(Some(RewardSetInfo {
10011001
anchor_block: anchor_block.clone(),
1002-
recipient,
1002+
recipients: chosen_recipients
1003+
.into_iter()
1004+
.map(|ix| {
1005+
let recipient = reward_set[ix as usize].clone();
1006+
(recipient, u16::try_from(ix).unwrap())
1007+
})
1008+
.collect(),
10031009
}))
10041010
} else {
10051011
Ok(None)
@@ -1013,12 +1019,16 @@ impl<'a> SortitionHandleTx<'a> {
10131019
if reward_set_size == 0 {
10141020
Ok(None)
10151021
} else {
1016-
let chosen_recipient = reward_set_vrf_seed.choose(reward_set_size as u32);
1017-
let ix = u16::try_from(chosen_recipient).unwrap();
1018-
let recipient = (self.get_reward_set_entry(ix)?, ix);
1022+
let chosen_recipients = reward_set_vrf_seed.choose_two(reward_set_size as u32);
1023+
let mut recipients = vec![];
1024+
for ix in chosen_recipients.into_iter() {
1025+
let ix = u16::try_from(ix).unwrap();
1026+
let recipient = self.get_reward_set_entry(ix)?;
1027+
recipients.push((recipient, ix));
1028+
}
10191029
Ok(Some(RewardSetInfo {
10201030
anchor_block,
1021-
recipient,
1031+
recipients,
10221032
}))
10231033
}
10241034
} else {
@@ -2336,7 +2346,7 @@ impl SortitionDB {
23362346
.mix_burn_header(&parent_snapshot.burn_header_hash);
23372347

23382348
let reward_set_info =
2339-
sortition_db_handle.pick_recipient(&reward_set_vrf_hash, next_pox_info.as_ref())?;
2349+
sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info.as_ref())?;
23402350

23412351
let new_snapshot = sortition_db_handle.process_block_txs(
23422352
&parent_snapshot,
@@ -2375,7 +2385,7 @@ impl SortitionDB {
23752385

23762386
let mut sortition_db_handle =
23772387
SortitionHandleTx::begin(self, &parent_snapshot.sortition_id)?;
2378-
sortition_db_handle.pick_recipient(&reward_set_vrf_hash, next_pox_info)
2388+
sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info)
23792389
}
23802390

23812391
pub fn is_stacks_block_in_sortition_set(
@@ -3229,9 +3239,18 @@ impl<'a> SortitionHandleTx<'a> {
32293239
if reward_set.len() > 0 {
32303240
// if we have a reward set, then we must also have produced a recipient
32313241
// info for this block
3232-
let (addr, ix) = recipient_info.unwrap().recipient.clone();
3233-
assert_eq!(&reward_set.remove(ix as usize), &addr,
3234-
"BUG: Attempted to remove used address from reward set, but failed to do so safely");
3242+
let mut recipients_to_remove: Vec<_> = recipient_info
3243+
.unwrap()
3244+
.recipients
3245+
.iter()
3246+
.map(|(addr, ix)| (addr.clone(), *ix))
3247+
.collect();
3248+
recipients_to_remove.sort_unstable_by(|(_, a), (_, b)| b.cmp(a));
3249+
// remove from the reward set any consumed addresses in this first reward block
3250+
for (addr, ix) in recipients_to_remove.iter() {
3251+
assert_eq!(&reward_set.remove(*ix as usize), addr,
3252+
"BUG: Attempted to remove used address from reward set, but failed to do so safely");
3253+
}
32353254
}
32363255

32373256
keys.push(db_keys::pox_reward_set_size().to_string());
@@ -3254,22 +3273,43 @@ impl<'a> SortitionHandleTx<'a> {
32543273
// update the reward set
32553274
if let Some(reward_info) = recipient_info {
32563275
let mut current_len = self.get_reward_set_size()?;
3257-
let (_, recipient_index) = reward_info.recipient;
3258-
3259-
if recipient_index >= current_len {
3260-
unreachable!(
3261-
"Supplied index should never be greater than recipient set size"
3262-
);
3276+
let mut recipient_indexes: Vec<_> =
3277+
reward_info.recipients.iter().map(|(_, x)| *x).collect();
3278+
let mut remapped_entries = HashMap::new();
3279+
// sort in decrementing order
3280+
recipient_indexes.sort_unstable_by(|a, b| b.cmp(a));
3281+
for index in recipient_indexes.into_iter() {
3282+
// sanity check
3283+
if index >= current_len {
3284+
unreachable!(
3285+
"Supplied index should never be greater than recipient set size"
3286+
);
3287+
} else if index + 1 == current_len {
3288+
// selected index is the last element: no need to swap, just decrement len
3289+
current_len -= 1;
3290+
} else {
3291+
let replacement = current_len - 1; // if current_len were 0, we would already have panicked.
3292+
let replace_with = if let Some((_prior_ix, replace_with)) =
3293+
remapped_entries.remove_entry(&replacement)
3294+
{
3295+
// the entry to swap in was itself swapped, so let's use the new value instead
3296+
replace_with
3297+
} else {
3298+
self.get_reward_set_entry(replacement)?
3299+
};
3300+
3301+
// swap and decrement to remove from set
3302+
remapped_entries.insert(index, replace_with);
3303+
current_len -= 1;
3304+
}
32633305
}
3264-
3265-
current_len -= 1;
3266-
let recipient = self.get_reward_set_entry(current_len)?;
3267-
32683306
// store the changes in the new trie
32693307
keys.push(db_keys::pox_reward_set_size().to_string());
32703308
values.push(db_keys::reward_set_size_to_string(current_len as usize));
3271-
keys.push(db_keys::pox_reward_set_entry(recipient_index));
3272-
values.push(recipient.to_string())
3309+
for (recipient_index, replace_with) in remapped_entries.into_iter() {
3310+
keys.push(db_keys::pox_reward_set_entry(recipient_index));
3311+
values.push(replace_with.to_string())
3312+
}
32733313
}
32743314
}
32753315
} else {

src/chainstate/burn/mod.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use burnchains::Txid;
3434
use util::hash::{to_hex, Hash160};
3535
use util::vrf::VRFProof;
3636

37+
use rand::seq::index::sample;
3738
use rand::Rng;
3839
use rand::SeedableRng;
3940
use rand_chacha::ChaCha20Rng;
@@ -179,12 +180,22 @@ impl SortitionHash {
179180
SortitionHash(ret)
180181
}
181182

182-
/// Choose 1 index from the range [0, max).
183-
pub fn choose(&self, max: u32) -> u32 {
183+
/// Choose two indices (without replacement) from the range [0, max).
184+
pub fn choose_two(&self, max: u32) -> Vec<u32> {
184185
let mut rng = ChaCha20Rng::from_seed(self.0.clone());
185-
let index: u32 = rng.gen_range(0, max);
186-
assert!(index < max);
187-
index
186+
if max < 2 {
187+
return (0..max).collect();
188+
}
189+
let first = rng.gen_range(0, max);
190+
let try_second = rng.gen_range(0, max - 1);
191+
let second = if first == try_second {
192+
// "swap" try_second with max
193+
max - 1
194+
} else {
195+
try_second
196+
};
197+
198+
vec![first, second]
188199
}
189200

190201
/// Convert a SortitionHash into a (little-endian) uint256

0 commit comments

Comments
 (0)