diff --git a/sip/sip-007-stacking-consensus.md b/sip/sip-007-stacking-consensus.md index cbc08bef450..c914368183e 100644 --- a/sip/sip-007-stacking-consensus.md +++ b/sip/sip-007-stacking-consensus.md @@ -191,10 +191,10 @@ Address validity is determined according to two different rules: descendant of the anchor block*, all of the miner's commitment funds must be burnt. 2. If a miner is building off a descendant of the anchor block, the - miner must send commitment funds to 5 addresses from the reward + miner must send commitment funds to 2 addresses from the reward set, chosen as follows: * Use the verifiable random function (also used by sortition) to - choose 5 addresses from the reward set. These 5 addresses are + choose 2 addresses from the reward set. These 2 addresses are the reward addresses for this block. * Once addresses have been chosen for a block, these addresses are 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 ## Adjusting Reward Threshold Based on Participation -Each reward cycle may transfer miner funds to up to 5000 Bitcoin -addresses. To ensure that this number of addresses is sufficient to -cover the pool of participants (given 100% participation of liquid -STX), the threshold for participation must be 0.02% (1/5000th) of the -liquid supply of STX. However, if participation is _lower_ than 100%, -the reward pool could admit lower STX holders. The Stacking protocol -specifies **2 operating levels**: +Each reward cycle may transfer miner funds to up to 4000 Bitcoin +addresses (2 addresses in a 2000 burn block cycle). To ensure that +this number of addresses is sufficient to cover the pool of +participants (given 100% participation of liquid STX), the threshold +for participation must be 0.025% (1/4000th) of the liquid supply of +STX. However, if participation is _lower_ than 100%, the reward pool +could admit lower STX holders. The Stacking protocol specifies **2 +operating levels**: * **25%** If fewer than `0.25 * STX_LIQUID_SUPPLY` STX participate in a reward cycle, participant wallets controlling `x` STX may include - `floor(x / (0.00005*STX_LIQUID_SUPPLY))` addresses in the reward set. - That is, the minimum participation threshold is 1/20,000th of the liquid + `floor(x / (0.0000625*STX_LIQUID_SUPPLY))` addresses in the reward set. + That is, the minimum participation threshold is 1/16,000th of the liquid supply. * **25%-100%** If between `0.25 * STX_LIQUID_SUPPLY` and `1.0 * STX_LIQUID_SUPPLY` STX participate in a reward cycle, the reward threshold is optimized in order to maximize the number of slots that are filled. That is, the minimum threshold `T` for participation will be - roughly 1/5,000th of the participating STX (adjusted in increments + roughly 1/4,000th of the participating STX (adjusted in increments of 10,000 STX). Participant wallets controlling `x` STX may include `floor(x / T)` addresses in the reward set. @@ -517,10 +518,6 @@ the second through nth outputs: The order of these addresses does not matter. Each of these outputs must receive the same amount of BTC. c. If the number of remaining addresses in the reward set N is less than M, then the leader - block commit transaction must burn BTC: - i. If N > 0, then the (N+2)nd output must be a burn output, and it must burn - (M-N) * (the amount of BTC transfered to each of the first N outputs) - ii. If N == 0, then the 2nd output must be a burn output, and the amount burned - by this output will be counted as the amount committed to by the block commit. -2. Otherwise, the 2nd output must be a burn output, and the amount burned by this output will be - counted as the amount committed to by the block commit. + block commit transaction must burn BTC by including (M-N) burn outputs. +2. Otherwise, the second through (M+1)th output must be burn addresses, and the amount burned by + these outputs will be counted as the amount committed to by the block commit. diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index 07daad27949..bc4d52081e3 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -51,6 +51,7 @@ use chainstate::stacks::StacksPublicKey; use chainstate::burn::db::sortdb::PoxId; use chainstate::burn::distribution::BurnSamplePoint; +use chainstate::burn::operations::leader_block_commit::OUTPUTS_PER_COMMIT; use chainstate::burn::operations::BlockstackOperationType; use chainstate::burn::operations::Error as op_error; use chainstate::burn::operations::LeaderKeyRegisterOp; @@ -311,7 +312,7 @@ impl PoxConstants { } pub fn reward_slots(&self) -> u32 { - self.reward_cycle_length + self.reward_cycle_length * (OUTPUTS_PER_COMMIT as u32) } /// is participating_ustx enough to engage in PoX in the next reward cycle? diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index f366fc1c6a1..c2678df941f 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -973,7 +973,7 @@ impl<'a> SortitionHandleTx<'a> { /// * The reward cycle had an anchor block, but it isn't known by this node. /// * The reward cycle did not have anchor block /// * The Stacking recipient set is empty (either because this reward cycle has already exhausted the set of addresses or because no one ever Stacked). - fn pick_recipient( + fn pick_recipients( &mut self, reward_set_vrf_seed: &SortitionHash, next_pox_info: Option<&RewardCycleInfo>, @@ -986,20 +986,26 @@ impl<'a> SortitionHandleTx<'a> { return Ok(None); } - let chosen_recipient = reward_set_vrf_seed.choose( + if OUTPUTS_PER_COMMIT != 2 { + unreachable!("BUG: PoX reward address selection only implemented for OUTPUTS_PER_COMMIT = 2"); + } + + let chosen_recipients = reward_set_vrf_seed.choose_two( reward_set .len() .try_into() .expect("BUG: u32 overflow in PoX outputs per commit"), ); - let recipient = ( - reward_set[chosen_recipient as usize], - u16::try_from(chosen_recipient).unwrap(), - ); Ok(Some(RewardSetInfo { anchor_block: anchor_block.clone(), - recipient, + recipients: chosen_recipients + .into_iter() + .map(|ix| { + let recipient = reward_set[ix as usize].clone(); + (recipient, u16::try_from(ix).unwrap()) + }) + .collect(), })) } else { Ok(None) @@ -1013,12 +1019,16 @@ impl<'a> SortitionHandleTx<'a> { if reward_set_size == 0 { Ok(None) } else { - let chosen_recipient = reward_set_vrf_seed.choose(reward_set_size as u32); - let ix = u16::try_from(chosen_recipient).unwrap(); - let recipient = (self.get_reward_set_entry(ix)?, ix); + let chosen_recipients = reward_set_vrf_seed.choose_two(reward_set_size as u32); + let mut recipients = vec![]; + for ix in chosen_recipients.into_iter() { + let ix = u16::try_from(ix).unwrap(); + let recipient = self.get_reward_set_entry(ix)?; + recipients.push((recipient, ix)); + } Ok(Some(RewardSetInfo { anchor_block, - recipient, + recipients, })) } } else { @@ -2336,7 +2346,7 @@ impl SortitionDB { .mix_burn_header(&parent_snapshot.burn_header_hash); let reward_set_info = - sortition_db_handle.pick_recipient(&reward_set_vrf_hash, next_pox_info.as_ref())?; + sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info.as_ref())?; let new_snapshot = sortition_db_handle.process_block_txs( &parent_snapshot, @@ -2375,7 +2385,7 @@ impl SortitionDB { let mut sortition_db_handle = SortitionHandleTx::begin(self, &parent_snapshot.sortition_id)?; - sortition_db_handle.pick_recipient(&reward_set_vrf_hash, next_pox_info) + sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info) } pub fn is_stacks_block_in_sortition_set( @@ -3229,9 +3239,18 @@ impl<'a> SortitionHandleTx<'a> { if reward_set.len() > 0 { // if we have a reward set, then we must also have produced a recipient // info for this block - let (addr, ix) = recipient_info.unwrap().recipient.clone(); - assert_eq!(&reward_set.remove(ix as usize), &addr, - "BUG: Attempted to remove used address from reward set, but failed to do so safely"); + let mut recipients_to_remove: Vec<_> = recipient_info + .unwrap() + .recipients + .iter() + .map(|(addr, ix)| (addr.clone(), *ix)) + .collect(); + recipients_to_remove.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)); + // remove from the reward set any consumed addresses in this first reward block + for (addr, ix) in recipients_to_remove.iter() { + assert_eq!(&reward_set.remove(*ix as usize), addr, + "BUG: Attempted to remove used address from reward set, but failed to do so safely"); + } } keys.push(db_keys::pox_reward_set_size().to_string()); @@ -3254,22 +3273,43 @@ impl<'a> SortitionHandleTx<'a> { // update the reward set if let Some(reward_info) = recipient_info { let mut current_len = self.get_reward_set_size()?; - let (_, recipient_index) = reward_info.recipient; - - if recipient_index >= current_len { - unreachable!( - "Supplied index should never be greater than recipient set size" - ); + let mut recipient_indexes: Vec<_> = + reward_info.recipients.iter().map(|(_, x)| *x).collect(); + let mut remapped_entries = HashMap::new(); + // sort in decrementing order + recipient_indexes.sort_unstable_by(|a, b| b.cmp(a)); + for index in recipient_indexes.into_iter() { + // sanity check + if index >= current_len { + unreachable!( + "Supplied index should never be greater than recipient set size" + ); + } else if index + 1 == current_len { + // selected index is the last element: no need to swap, just decrement len + current_len -= 1; + } else { + let replacement = current_len - 1; // if current_len were 0, we would already have panicked. + let replace_with = if let Some((_prior_ix, replace_with)) = + remapped_entries.remove_entry(&replacement) + { + // the entry to swap in was itself swapped, so let's use the new value instead + replace_with + } else { + self.get_reward_set_entry(replacement)? + }; + + // swap and decrement to remove from set + remapped_entries.insert(index, replace_with); + current_len -= 1; + } } - - current_len -= 1; - let recipient = self.get_reward_set_entry(current_len)?; - // store the changes in the new trie keys.push(db_keys::pox_reward_set_size().to_string()); values.push(db_keys::reward_set_size_to_string(current_len as usize)); - keys.push(db_keys::pox_reward_set_entry(recipient_index)); - values.push(recipient.to_string()) + for (recipient_index, replace_with) in remapped_entries.into_iter() { + keys.push(db_keys::pox_reward_set_entry(recipient_index)); + values.push(replace_with.to_string()) + } } } } else { diff --git a/src/chainstate/burn/mod.rs b/src/chainstate/burn/mod.rs index 6dd56e838e0..25c6158b252 100644 --- a/src/chainstate/burn/mod.rs +++ b/src/chainstate/burn/mod.rs @@ -34,6 +34,7 @@ use burnchains::Txid; use util::hash::{to_hex, Hash160}; use util::vrf::VRFProof; +use rand::seq::index::sample; use rand::Rng; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -179,12 +180,22 @@ impl SortitionHash { SortitionHash(ret) } - /// Choose 1 index from the range [0, max). - pub fn choose(&self, max: u32) -> u32 { + /// Choose two indices (without replacement) from the range [0, max). + pub fn choose_two(&self, max: u32) -> Vec { let mut rng = ChaCha20Rng::from_seed(self.0.clone()); - let index: u32 = rng.gen_range(0, max); - assert!(index < max); - index + if max < 2 { + return (0..max).collect(); + } + let first = rng.gen_range(0, max); + let try_second = rng.gen_range(0, max - 1); + let second = if first == try_second { + // "swap" try_second with max + max - 1 + } else { + try_second + }; + + vec![first, second] } /// Convert a SortitionHash into a (little-endian) uint256 diff --git a/src/chainstate/burn/operations/leader_block_commit.rs b/src/chainstate/burn/operations/leader_block_commit.rs index f299b6d801e..0d149a04268 100644 --- a/src/chainstate/burn/operations/leader_block_commit.rs +++ b/src/chainstate/burn/operations/leader_block_commit.rs @@ -60,7 +60,7 @@ struct ParsedData { memo: Vec, } -pub static OUTPUTS_PER_COMMIT: usize = 1; +pub static OUTPUTS_PER_COMMIT: usize = 2; impl LeaderBlockCommitOp { #[cfg(test)] @@ -193,10 +193,10 @@ impl LeaderBlockCommitOp { return Err(op_error::InvalidInput); } - if tx.opcode() != (Opcodes::LeaderBlockCommit as u8) { + if tx.opcode() != Opcodes::LeaderBlockCommit as u8 { warn!("Invalid tx: invalid opcode {}", tx.opcode()); return Err(op_error::InvalidInput); - } + }; let data = LeaderBlockCommitOp::parse_data(&tx.data()).ok_or_else(|| { warn!("Invalid tx data"); @@ -236,62 +236,34 @@ impl LeaderBlockCommitOp { let mut commit_outs = vec![]; let mut pox_fee = None; - let mut burn_fee = None; - for (ix, output) in outputs.into_iter().enumerate() { // only look at the first OUTPUTS_PER_COMMIT outputs - // or until first _burn_ output if ix >= OUTPUTS_PER_COMMIT { break; } - if output.address.is_burn() { - burn_fee.replace(output.amount); - break; - } else { - // all pox outputs must have the same fee - if let Some(pox_fee) = pox_fee { - if output.amount != pox_fee { - warn!("Invalid commit tx: different output amounts for different PoX reward addresses"); - return Err(op_error::ParseError); - } - } else { - pox_fee.replace(output.amount); + // all pox outputs must have the same fee + if let Some(pox_fee) = pox_fee { + if output.amount != pox_fee { + warn!("Invalid commit tx: different output amounts for different PoX reward addresses"); + return Err(op_error::ParseError); } - commit_outs.push(output.address); + } else { + pox_fee.replace(output.amount); } + commit_outs.push(output.address); } - // EITHER there was an amount burned _or_ there were OUTPUTS_PER_COMMIT pox outputs - if burn_fee.is_none() && commit_outs.len() != OUTPUTS_PER_COMMIT { - warn!("Invalid commit tx: if fewer than {} PoX addresses are committed to, remainder must be burnt", OUTPUTS_PER_COMMIT); - return Err(op_error::ParseError); + if commit_outs.len() != OUTPUTS_PER_COMMIT { + warn!("Invalid commit tx: {} commit addresses, but {} PoX addresses should be committed to", commit_outs.len(), OUTPUTS_PER_COMMIT); + return Err(op_error::InvalidInput); } // compute the total amount transfered/burned, and check that the burn amount // is expected given the amount transfered. - let burn_fee = match (burn_fee, pox_fee) { - (Some(burned_amount), Some(pox_amount)) => { - // burned amount must be equal to the "missing" - // PoX slots - let expected_burn_amount = pox_amount - .checked_mul((OUTPUTS_PER_COMMIT - commit_outs.len()) as u64) - .ok_or_else(|| op_error::ParseError)?; - if expected_burn_amount != burned_amount { - warn!("Invalid commit tx: burned output different from PoX reward output"); - return Err(op_error::ParseError); - } - pox_amount - .checked_mul(OUTPUTS_PER_COMMIT as u64) - .ok_or_else(|| op_error::ParseError)? - } - (Some(burned_amount), None) => burned_amount, - (None, Some(pox_amount)) => pox_amount - .checked_mul(OUTPUTS_PER_COMMIT as u64) - .ok_or_else(|| op_error::ParseError)?, - (None, None) => { - unreachable!("A 0-len output should have already errored"); - } - }; + let burn_fee = pox_fee + .expect("A 0-len output should have already errored") + .checked_mul(OUTPUTS_PER_COMMIT as u64) // total commitment is the pox_amount * outputs + .ok_or_else(|| op_error::ParseError)?; if burn_fee == 0 { warn!("Invalid commit tx: burn/transfer amount is 0"); @@ -317,6 +289,15 @@ impl LeaderBlockCommitOp { burn_header_hash: block_hash.clone(), }) } + + /// are all the outputs for this block commit burns? + pub fn all_outputs_burn(&self) -> bool { + self.commit_outs + .iter() + .fold(true, |previous_is_burn, output_addr| { + previous_is_burn && output_addr.is_burn() + }) + } } impl StacksMessageCodec for LeaderBlockCommitOp { @@ -362,7 +343,30 @@ impl BlockstackOperation for LeaderBlockCommitOp { pub struct RewardSetInfo { pub anchor_block: BlockHeaderHash, - pub recipient: (StacksAddress, u16), + pub recipients: Vec<(StacksAddress, u16)>, +} + +impl RewardSetInfo { + /// Takes an Option and produces the commit_outs + /// for a corresponding LeaderBlockCommitOp. If RewardSetInfo is none, + /// the LeaderBlockCommitOp will use burn addresses. + pub fn into_commit_outs(from: Option, mainnet: bool) -> Vec { + if let Some(recipient_set) = from { + let mut outs: Vec<_> = recipient_set + .recipients + .into_iter() + .map(|(recipient, _)| recipient) + .collect(); + while outs.len() < OUTPUTS_PER_COMMIT { + outs.push(StacksAddress::burn_address(mainnet)); + } + outs + } else { + (0..OUTPUTS_PER_COMMIT) + .map(|_| StacksAddress::burn_address(mainnet)) + .collect() + } + } } impl LeaderBlockCommitOp { @@ -399,44 +403,76 @@ impl LeaderBlockCommitOp { // we do this because the descended_from check isn't particularly cheap, so // we want to make sure that any TX that forces us to perform the check // has either burned BTC or sent BTC to the PoX recipients - let expect_pox_descendant = if self.commit_outs.len() == 0 { - false - } else { - if self.commit_outs.len() != 1 { - warn!( - "Invalid block commit: expected {} PoX transfers, but commit has {}", - 1, - self.commit_outs.len() - ); - return Err(op_error::BlockCommitBadOutputs); - } - let (expected_commit, _) = reward_set_info.recipient; - if !self.commit_outs.contains(&expected_commit) { - warn!("Invalid block commit: expected to send funds to {}, but that address is not in the committed output set", - expected_commit); + + // first, handle a corner case: + // all of the commitment outputs are _burns_ + // _and_ the reward set chose two burn addresses as reward addresses. + // then, don't need to do a pox descendant check. + let recipient_set_all_burns = reward_set_info + .recipients + .iter() + .fold(true, |prior_is_burn, (addr, _)| { + prior_is_burn && addr.is_burn() + }); + + if recipient_set_all_burns { + if !self.all_outputs_burn() { + warn!("Invalid block commit: recipient set should be all burns"); return Err(op_error::BlockCommitBadOutputs); } - true - }; - - let descended_from_anchor = tx.descended_from(parent_block_height, &reward_set_info.anchor_block) - .map_err(|e| { - error!("Failed to check whether parent (height={}) is descendent of anchor block={}: {}", - parent_block_height, &reward_set_info.anchor_block, e); - op_error::BlockCommitAnchorCheck})?; - if descended_from_anchor != expect_pox_descendant { - if descended_from_anchor { - warn!("Invalid block commit: descended from PoX anchor, but used burn outputs"); + } else { + let expect_pox_descendant = if self.all_outputs_burn() { + false } else { - warn!( - "Invalid block commit: not descended from PoX anchor, but used PoX outputs" - ); + if self.commit_outs.len() != reward_set_info.recipients.len() { + warn!( + "Invalid block commit: expected {} PoX transfers, but commit has {}", + reward_set_info.recipients.len(), + self.commit_outs.len() + ); + return Err(op_error::BlockCommitBadOutputs); + } + + // sort check_recipients and commit_outs so that we can perform an + // iterative equality check + let mut check_recipients: Vec<_> = reward_set_info + .recipients + .iter() + .map(|(addr, _)| addr.clone()) + .collect(); + check_recipients.sort(); + let mut commit_outs = self.commit_outs.clone(); + commit_outs.sort(); + for (expected_commit, found_commit) in commit_outs.iter().zip(check_recipients) + { + if expected_commit != &found_commit { + warn!("Invalid block commit: committed output {} does not match expected {}", + found_commit, expected_commit); + return Err(op_error::BlockCommitBadOutputs); + } + } + true + }; + + let descended_from_anchor = tx.descended_from(parent_block_height, &reward_set_info.anchor_block) + .map_err(|e| { + error!("Failed to check whether parent (height={}) is descendent of anchor block={}: {}", + parent_block_height, &reward_set_info.anchor_block, e); + op_error::BlockCommitAnchorCheck})?; + if descended_from_anchor != expect_pox_descendant { + if descended_from_anchor { + warn!("Invalid block commit: descended from PoX anchor, but used burn outputs"); + } else { + warn!( + "Invalid block commit: not descended from PoX anchor, but used PoX outputs" + ); + } + return Err(op_error::BlockCommitBadOutputs); } - return Err(op_error::BlockCommitBadOutputs); } } else { // no recipient info for this sortition, so expect all burns - if self.commit_outs.len() != 0 { + if !self.all_outputs_burn() { warn!("Invalid block commit: this transaction should only have burn outputs."); return Err(op_error::BlockCommitBadOutputs); } @@ -551,7 +587,7 @@ mod tests { use address::AddressHashMode; use deps::bitcoin::blockdata::transaction::Transaction; - use deps::bitcoin::network::serialize::deserialize; + use deps::bitcoin::network::serialize::{deserialize, serialize_hex}; use chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; @@ -597,22 +633,139 @@ mod tests { num_required: 0, in_type: BitcoinInputType::Standard, }], - outputs: vec![BitcoinTxOutput { - units: 10, - address: BitcoinAddress { - addrtype: BitcoinAddressType::PublicKeyHash, - network_id: BitcoinNetworkType::Mainnet, - bytes: Hash160([1; 20]), + outputs: vec![ + BitcoinTxOutput { + units: 10, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([1; 20]), + }, + }, + BitcoinTxOutput { + units: 10, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 30, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([0; 20]), + }, }, + ], + }); + + let op = LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) + .unwrap(); + + // should have 2 commit outputs, summing to 20 burned units + assert_eq!(op.commit_outs.len(), 2); + assert_eq!(op.burn_fee, 20); + + let tx = BurnchainTransaction::Bitcoin(BitcoinTransaction { + txid: Txid([0; 32]), + vtxindex: 0, + opcode: Opcodes::LeaderBlockCommit as u8, + data: vec![1; 80], + inputs: vec![BitcoinTxInput { + keys: vec![], + num_required: 0, + in_type: BitcoinInputType::Standard, }], + outputs: vec![ + BitcoinTxOutput { + units: 10, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([1; 20]), + }, + }, + BitcoinTxOutput { + units: 9, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([0; 20]), + }, + }, + ], + }); + + // burn amount should have been 10, not 9 + match LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) + .unwrap_err() + { + op_error::ParseError => {} + _ => unreachable!(), + }; + + let tx = BurnchainTransaction::Bitcoin(BitcoinTransaction { + txid: Txid([0; 32]), + vtxindex: 0, + opcode: Opcodes::LeaderBlockCommit as u8, + data: vec![1; 80], + inputs: vec![BitcoinTxInput { + keys: vec![], + num_required: 0, + in_type: BitcoinInputType::Standard, + }], + outputs: vec![ + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([1; 20]), + }, + }, + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + ], }); let op = LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) .unwrap(); - // should have 1 commit outputs, and a burn - assert_eq!(op.commit_outs.len(), 1); - assert_eq!(op.burn_fee, 10); + // should have 2 commit outputs + assert_eq!(op.commit_outs.len(), 2); + assert_eq!(op.burn_fee, 26); let tx = BurnchainTransaction::Bitcoin(BitcoinTransaction { txid: Txid([0; 32]), @@ -634,12 +787,13 @@ mod tests { }], }); - let op = LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) - .unwrap(); - - // should have 1 commit outputs - assert_eq!(op.commit_outs.len(), 1); - assert_eq!(op.burn_fee, 13); + // not enough PoX outputs + match LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) + .unwrap_err() + { + op_error::InvalidInput => {} + _ => unreachable!(), + }; let tx = BurnchainTransaction::Bitcoin(BitcoinTransaction { txid: Txid([0; 32]), @@ -651,14 +805,31 @@ mod tests { num_required: 0, in_type: BitcoinInputType::Standard, }], - outputs: vec![], + outputs: vec![ + BitcoinTxOutput { + units: 13, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([1; 20]), + }, + }, + BitcoinTxOutput { + units: 10, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + ], }); - // not enough PoX outputs + // unequal PoX outputs match LeaderBlockCommitOp::parse_from_tx(16843019, &BurnchainHeaderHash([0; 32]), &tx) .unwrap_err() { - op_error::InvalidInput => {} + op_error::ParseError => {} _ => unreachable!(), }; @@ -672,14 +843,48 @@ mod tests { num_required: 0, in_type: BitcoinInputType::Standard, }], - outputs: vec![BitcoinTxOutput { - units: 0, - address: BitcoinAddress { - addrtype: BitcoinAddressType::PublicKeyHash, - network_id: BitcoinNetworkType::Mainnet, - bytes: Hash160([1; 20]), + outputs: vec![ + BitcoinTxOutput { + units: 0, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([1; 20]), + }, }, - }], + BitcoinTxOutput { + units: 0, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 0, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 0, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + BitcoinTxOutput { + units: 0, + address: BitcoinAddress { + addrtype: BitcoinAddressType::PublicKeyHash, + network_id: BitcoinNetworkType::Mainnet, + bytes: Hash160([2; 20]), + }, + }, + ], }); // 0 total burn @@ -703,7 +908,7 @@ mod tests { let tx_fixtures = vec![ OpFixture { // valid - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006b483045022100eba8c0a57c1eb71cdfba0874de63cf37b3aace1e56dcbd61701548194a79af34022041dd191256f3f8a45562e5d60956bb871421ba69db605716250554b23b08277b012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000030000000000000000536a4c5069645b222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333334041424350516061626370718039300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".to_string(), + txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006b483045022100eba8c0a57c1eb71cdfba0874de63cf37b3aace1e56dcbd61701548194a79af34022041dd191256f3f8a45562e5d60956bb871421ba69db605716250554b23b08277b012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000040000000000000000536a4c5069645b222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333334041424350516061626370718039300000000000001976a914000000000000000000000000000000000000000088ac39300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".into(), opstr: "69645b2222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333340414243505160616263707180".to_string(), result: Some(LeaderBlockCommitOp { block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), @@ -714,9 +919,12 @@ mod tests { key_vtxindex: 0x7071, memo: vec![0x80], - commit_outs: vec![], + commit_outs: vec![ + StacksAddress { version: 26, bytes: Hash160::empty() }, + StacksAddress { version: 26, bytes: Hash160::empty() } + ], - burn_fee: 12345, + burn_fee: 24690, input: BurnchainSigner { public_keys: vec![ StacksPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), @@ -725,7 +933,7 @@ mod tests { hash_mode: AddressHashMode::SerializeP2PKH }, - txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + txid: Txid::from_hex("b08d5d1bc81049a3957e9ff9a5882463811735fd5de985e6d894e9b3d5c49501").unwrap(), vtxindex: vtxindex, block_height: block_height, burn_header_hash: burn_header_hash, @@ -753,8 +961,20 @@ mod tests { let parser = BitcoinBlockParser::new(BitcoinNetworkType::Testnet, BLOCKSTACK_MAGIC_MAINNET); + let mut is_first = false; for tx_fixture in tx_fixtures { - let tx = make_tx(&tx_fixture.txstr).unwrap(); + let mut tx = make_tx(&tx_fixture.txstr).unwrap(); + if is_first { + eprintln!("TX outputs: {}", tx.output.len()); + tx.output.insert( + 2, + StacksAddress::burn_address(false).to_bitcoin_tx_out(12345), + ); + is_first = false; + eprintln!("Updated txstr = {}", serialize_hex(&tx).unwrap()); + assert!(false); + } + let header = match tx_fixture.result { Some(ref op) => BurnchainBlockHeader { block_height: op.block_height, @@ -792,11 +1012,11 @@ mod tests { } (Err(_e), None) => {} (Ok(_parsed_tx), None) => { - test_debug!("Parsed a tx when we should not have"); + eprintln!("Parsed a tx when we should not have"); assert!(false); } (Err(_e), Some(_result)) => { - test_debug!("Did not parse a tx when we should have"); + eprintln!("Did not parse a tx when we should have"); assert!(false); } }; diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs index 7d719cbd128..8694a41afda 100644 --- a/src/chainstate/coordinator/tests.rs +++ b/src/chainstate/coordinator/tests.rs @@ -367,8 +367,11 @@ fn make_genesis_block_with_recipients( builder.epoch_finish(epoch_tx); let commit_outs = if let Some(recipients) = recipients { - let (addr, _) = recipients.recipient; - vec![addr] + recipients + .recipients + .iter() + .map(|(a, _)| a.clone()) + .collect() } else { vec![] }; @@ -491,8 +494,11 @@ fn make_stacks_block_with_recipients( builder.epoch_finish(epoch_tx); let commit_outs = if let Some(recipients) = recipients { - let (addr, _) = recipients.recipient; - vec![addr] + recipients + .recipients + .iter() + .map(|(a, _)| a.clone()) + .collect() } else { vec![] }; @@ -725,7 +731,7 @@ fn test_sortition_with_reward_set() { let mut vrf_keys: Vec<_> = (0..150).map(|_| VRFPrivateKey::new()).collect(); let mut committers: Vec<_> = (0..150).map(|_| StacksPrivateKey::new()).collect(); - let reward_set_size = 5; + let reward_set_size = 10; let reward_set: Vec<_> = (0..reward_set_size) .map(|_| p2pkh_from(&StacksPrivateKey::new())) .collect(); @@ -814,14 +820,15 @@ fn test_sortition_with_reward_set() { .test_get_next_block_recipients(reward_cycle_info.as_ref()) .unwrap(); if let Some(ref next_block_recipients) = next_block_recipients { - let (addr, _) = next_block_recipients.recipient; - assert!( - !reward_recipients.contains(&addr), - "Reward set should not already contain address {}", - addr - ); - eprintln!("At iteration: {}, inserting address ... {}", ix, addr); - reward_recipients.insert(addr.clone()); + for (addr, _) in next_block_recipients.recipients.iter() { + assert!( + !reward_recipients.contains(addr), + "Reward set should not already contain address {}", + addr + ); + eprintln!("At iteration: {}, inserting address ... {}", ix, addr); + reward_recipients.insert(addr.clone()); + } } let (good_op, mut block) = if ix == 0 { @@ -877,14 +884,16 @@ fn test_sortition_with_reward_set() { // sometime have the wrong _number_ of recipients, // other times just have the wrong set of recipients - let recipient = if ix % 2 == 0 { - (p2pkh_from(miner_wrong_out), 0) + let recipients = if ix % 2 == 0 { + vec![(p2pkh_from(miner_wrong_out), 0)] } else { - (p2pkh_from(&StacksPrivateKey::new()), 0) + (0..OUTPUTS_PER_COMMIT) + .map(|ix| (p2pkh_from(&StacksPrivateKey::new()), ix as u16)) + .collect() }; let bad_block_recipipients = Some(RewardSetInfo { anchor_block: BlockHeaderHash([0; 32]), - recipient, + recipients, }); let (bad_outs_op, _) = make_stacks_block_with_recipients( &sort_db, @@ -961,6 +970,229 @@ fn test_sortition_with_reward_set() { } } +#[test] +fn test_sortition_with_burner_reward_set() { + let path = "/tmp/stacks-blockchain-burner-reward-set"; + let _r = std::fs::remove_dir_all(path); + + let mut vrf_keys: Vec<_> = (0..150).map(|_| VRFPrivateKey::new()).collect(); + let mut committers: Vec<_> = (0..150).map(|_| StacksPrivateKey::new()).collect(); + + let reward_set_size = 9; + let mut reward_set: Vec<_> = (0..reward_set_size - 1) + .map(|_| StacksAddress::burn_address(false)) + .collect(); + reward_set.push(p2pkh_from(&StacksPrivateKey::new())); + + setup_states(&[path], &vrf_keys, &committers); + + let mut coord = make_reward_set_coordinator(path, reward_set); + + coord.handle_new_burnchain_block().unwrap(); + + let sort_db = get_sortition_db(path); + + let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); + assert_eq!(tip.block_height, 1); + assert_eq!(tip.sortition, false); + let (_, ops) = sort_db + .get_sortition_result(&tip.sortition_id) + .unwrap() + .unwrap(); + + // we should have all the VRF registrations accepted + assert_eq!(ops.accepted_ops.len(), vrf_keys.len()); + assert_eq!(ops.consumed_leader_keys.len(), 0); + + let mut started_first_reward_cycle = false; + // process sequential blocks, and their sortitions... + let mut stacks_blocks: Vec<(SortitionId, StacksBlock)> = vec![]; + let mut anchor_blocks = vec![]; + + // split up the vrf keys and committers so that we have some that will be mining "correctly" + // and some that will be producing bad outputs + + let BURNER_OFFSET = 50; + let mut vrf_key_burners = vrf_keys.split_off(50); + let mut miner_burners = committers.split_off(50); + + let WRONG_OUTS_OFFSET = 100; + let vrf_key_wrong_outs = vrf_key_burners.split_off(50); + let miner_wrong_outs = miner_burners.split_off(50); + + // track the reward set consumption + let mut reward_recipients = HashSet::new(); + for ix in 0..vrf_keys.len() { + let vrf_key = &vrf_keys[ix]; + let miner = &committers[ix]; + + let vrf_burner = &vrf_key_burners[ix]; + let miner_burner = &miner_burners[ix]; + + let vrf_wrong_out = &vrf_key_wrong_outs[ix]; + let miner_wrong_out = &miner_wrong_outs[ix]; + + let mut burnchain = get_burnchain_db(path); + let mut chainstate = get_chainstate(path); + + let parent = if ix == 0 { + BlockHeaderHash([0; 32]) + } else { + stacks_blocks[ix - 1].1.header.block_hash() + }; + + let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); + let next_mock_header = BurnchainBlockHeader { + block_height: burnchain_tip.block_height + 1, + block_hash: BurnchainHeaderHash([0; 32]), + parent_block_hash: burnchain_tip.block_hash, + num_txs: 0, + timestamp: 1, + }; + + let reward_cycle_info = coord.get_reward_cycle_info(&next_mock_header).unwrap(); + if reward_cycle_info.is_some() { + // did we process a reward set last cycle? check if the + // recipient set size matches our expectation + if started_first_reward_cycle { + assert_eq!(reward_recipients.len(), 2); + } + // clear the reward recipients tracker, since those + // recipients are now eligible again in the new reward cycle + reward_recipients.clear(); + } + let next_block_recipients = get_rw_sortdb(path) + .test_get_next_block_recipients(reward_cycle_info.as_ref()) + .unwrap(); + if let Some(ref next_block_recipients) = next_block_recipients { + for (addr, _) in next_block_recipients.recipients.iter() { + if !addr.is_burn() { + assert!( + !reward_recipients.contains(addr), + "Reward set should not already contain address {}", + addr + ); + } + eprintln!("At iteration: {}, inserting address ... {}", ix, addr); + reward_recipients.insert(addr.clone()); + } + } + + let (good_op, block) = if ix == 0 { + make_genesis_block_with_recipients( + &sort_db, + &mut chainstate, + &parent, + miner, + 10000, + vrf_key, + ix as u32, + next_block_recipients.as_ref(), + ) + } else { + make_stacks_block_with_recipients( + &sort_db, + &mut chainstate, + &parent, + miner, + 10000, + vrf_key, + ix as u32, + next_block_recipients.as_ref(), + ) + }; + + let expected_winner = good_op.txid(); + let mut ops = vec![good_op]; + + if started_first_reward_cycle { + // sometime have the wrong _number_ of recipients, + // other times just have the wrong set of recipients + let recipients = if ix % 2 == 0 { + vec![(p2pkh_from(miner_wrong_out), 0)] + } else { + (0..OUTPUTS_PER_COMMIT) + .map(|ix| (p2pkh_from(&StacksPrivateKey::new()), ix as u16)) + .collect() + }; + let bad_block_recipipients = Some(RewardSetInfo { + anchor_block: BlockHeaderHash([0; 32]), + recipients, + }); + let (bad_outs_op, _) = make_stacks_block_with_recipients( + &sort_db, + &mut chainstate, + &parent, + miner_wrong_out, + 10000, + vrf_burner, + (ix + WRONG_OUTS_OFFSET) as u32, + bad_block_recipipients.as_ref(), + ); + ops.push(bad_outs_op); + } + + let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); + produce_burn_block( + &mut burnchain, + &burnchain_tip.block_hash, + ops, + vec![].iter_mut(), + ); + // handle the sortition + coord.handle_new_burnchain_block().unwrap(); + + let b = get_burnchain(path); + let new_burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); + if b.is_reward_cycle_start(new_burnchain_tip.block_height) { + started_first_reward_cycle = true; + // store the anchor block for this sortition for later checking + let ic = sort_db.index_handle_at_tip(); + let bhh = ic.get_last_anchor_block_hash().unwrap().unwrap(); + anchor_blocks.push(bhh); + } + + let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); + assert_eq!(&tip.winning_block_txid, &expected_winner); + + // load the block into staging + let block_hash = block.header.block_hash(); + + assert_eq!(&tip.winning_stacks_block_hash, &block_hash); + stacks_blocks.push((tip.sortition_id.clone(), block.clone())); + + preprocess_block(&mut chainstate, &sort_db, &tip, block); + + // handle the stacks block + coord.handle_new_stacks_block().unwrap(); + } + + let stacks_tip = SortitionDB::get_canonical_stacks_chain_tip_hash(sort_db.conn()).unwrap(); + let mut chainstate = get_chainstate(path); + assert_eq!( + chainstate.with_read_only_clarity_tx( + &sort_db.index_conn(), + &StacksBlockId::new(&stacks_tip.0, &stacks_tip.1), + |conn| conn + .with_readonly_clarity_env( + PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), + LimitedCostTracker::new_max_limit(), + |env| env.eval_raw("block-height") + ) + .unwrap() + ), + Value::UInt(50) + ); + + { + let ic = sort_db.index_handle_at_tip(); + let pox_id = ic.get_pox_id().unwrap(); + assert_eq!(&pox_id.to_string(), + "11111111111", + "PoX ID should reflect the 10 reward cycles _with_ a known anchor block, plus the 'initial' known reward cycle at genesis"); + } +} + #[test] // This test should panic until the MARF stability issue // https://github.com/blockstack/stacks-blockchain/issues/1805 diff --git a/src/chainstate/stacks/address.rs b/src/chainstate/stacks/address.rs index 56da69f3c39..35453c34a72 100644 --- a/src/chainstate/stacks/address.rs +++ b/src/chainstate/stacks/address.rs @@ -38,10 +38,10 @@ use burnchains::PublicKey; use address::c32::c32_address_decode; -use deps::bitcoin::blockdata::script::Builder as BtcScriptBuilder; - use deps::bitcoin::blockdata::opcodes::All as BtcOp; +use deps::bitcoin::blockdata::script::Builder as BtcScriptBuilder; use deps::bitcoin::blockdata::transaction::TxOut; +use std::cmp::{Ord, Ordering}; use burnchains::bitcoin::address::{ address_type_to_version_byte, to_b52_version_byte, to_c32_version_byte, @@ -79,6 +79,21 @@ impl From for StacksAddress { } } +impl PartialOrd for StacksAddress { + fn partial_cmp(&self, other: &StacksAddress) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StacksAddress { + fn cmp(&self, other: &StacksAddress) -> Ordering { + match self.version.cmp(&other.version) { + Ordering::Equal => self.bytes.cmp(&other.bytes), + inequality => inequality, + } + } +} + impl StacksAddress { pub fn new(version: u8, hash: Hash160) -> StacksAddress { StacksAddress { diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 2732840cf71..0749dbef034 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -95,12 +95,12 @@ fn tuple_to_pox_addr(tuple_data: TupleData) -> (AddressHashMode, Hash160) { .expect("FATAL: no 'hashbytes' field in pox-addr") .to_owned(); - let version_u8 = version_value.expect_buff(1)[0]; + let version_u8 = version_value.expect_buff_padded(1, 0)[0]; let version: AddressHashMode = version_u8 .try_into() .expect("FATAL: PoX version is not a supported version byte"); - let hashbytes_vec = hashbytes_value.expect_buff(20); + let hashbytes_vec = hashbytes_value.expect_buff_padded(20, 0); let mut hashbytes_20 = [0u8; 20]; hashbytes_20.copy_from_slice(&hashbytes_vec[0..20]); @@ -388,7 +388,7 @@ pub mod test { #[test] fn get_reward_threshold_units() { - let test_pox_constants = PoxConstants::new(1000, 1, 1, 1, 5); + let test_pox_constants = PoxConstants::new(500, 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; diff --git a/src/net/mod.rs b/src/net/mod.rs index 6f4847b45a5..ad28bf393b3 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -2931,7 +2931,7 @@ pub mod test { ) { Ok(recipients) => { block_commit_op.commit_outs = match recipients { - Some(info) => vec![info.recipient.0], + Some(info) => info.recipients.into_iter().map(|x| x.0).collect(), None => vec![], }; } diff --git a/src/vm/types/mod.rs b/src/vm/types/mod.rs index f448e793585..b74c9ae92d8 100644 --- a/src/vm/types/mod.rs +++ b/src/vm/types/mod.rs @@ -726,7 +726,8 @@ impl Value { if let Value::UInt(inner) = self { inner } else { - panic!(format!("Value '{:?}' is not a u128", &self)); + error!("Value '{:?}' is not a u128", &self); + panic!(); } } @@ -734,31 +735,45 @@ impl Value { if let Value::Int(inner) = self { inner } else { - panic!(format!("Value '{:?}' is not an i128", &self)); + error!("Value '{:?}' is not an i128", &self); + panic!(); } } pub fn expect_buff(self, sz: usize) -> Vec { if let Value::Sequence(SequenceData::Buffer(buffdata)) = self { - if buffdata.data.len() == sz { + if buffdata.data.len() <= sz { buffdata.data } else { - panic!(format!( + error!( "Value buffer has len {}, expected {}", buffdata.data.len(), sz - )); + ); + panic!(); } } else { - panic!(format!("Value '{:?}' is not a buff", &self)); + error!("Value '{:?}' is not a buff", &self); + panic!(); } } + pub fn expect_buff_padded(self, sz: usize, pad: u8) -> Vec { + let mut data = self.expect_buff(sz); + if sz > data.len() { + for _ in data.len()..sz { + data.push(pad) + } + } + data + } + pub fn expect_bool(self) -> bool { if let Value::Bool(b) = self { b } else { - panic!(format!("Value '{:?}' is not a bool", &self)); + error!("Value '{:?}' is not a bool", &self); + panic!(); } } @@ -766,7 +781,8 @@ impl Value { if let Value::Tuple(data) = self { data } else { - panic!(format!("Value '{:?}' is not a tuple", &self)); + error!("Value '{:?}' is not a tuple", &self); + panic!(); } } @@ -777,7 +793,8 @@ impl Value { None => None, } } else { - panic!(format!("Value '{:?}' is not an optional", &self)); + error!("Value '{:?}' is not an optional", &self); + panic!(); } } @@ -785,7 +802,8 @@ impl Value { if let Value::Principal(p) = self { p } else { - panic!(format!("Value '{:?}' is not a principal", &self)); + error!("Value '{:?}' is not a principal", &self); + panic!(); } } @@ -797,7 +815,8 @@ impl Value { Err(*res_data.data) } } else { - panic!("FATAL: not a response"); + error!("Value '{:?}' is not a response", &self); + panic!(); } } @@ -806,10 +825,12 @@ impl Value { if res_data.committed { *res_data.data } else { - panic!("FATAL: not a (ok ..)"); + error!("Value is not a (ok ..)"); + panic!(); } } else { - panic!("FATAL: not a response"); + error!("Value '{:?}' is not a response", &self); + panic!(); } } @@ -818,10 +839,12 @@ impl Value { if !res_data.committed { *res_data.data } else { - panic!("FATAL: not a (err ..)"); + error!("Value is not a (err ..)"); + panic!(); } } else { - panic!("FATAL: not a response"); + error!("Value '{:?}' is not a response", &self); + panic!(); } } } @@ -1269,4 +1292,31 @@ mod test { "(tuple (a 2))" ); } + + #[test] + fn expect_buff() { + let buff = Value::Sequence(SequenceData::Buffer(BuffData { + data: vec![1, 2, 3, 4, 5], + })); + assert_eq!(buff.clone().expect_buff(5), vec![1, 2, 3, 4, 5]); + assert_eq!(buff.clone().expect_buff(6), vec![1, 2, 3, 4, 5]); + assert_eq!( + buff.clone().expect_buff_padded(6, 0), + vec![1, 2, 3, 4, 5, 0] + ); + assert_eq!(buff.clone().expect_buff(10), vec![1, 2, 3, 4, 5]); + assert_eq!( + buff.clone().expect_buff_padded(10, 1), + vec![1, 2, 3, 4, 5, 1, 1, 1, 1, 1] + ); + } + + #[test] + #[should_panic] + fn expect_buff_too_small() { + let buff = Value::Sequence(SequenceData::Buffer(BuffData { + data: vec![1, 2, 3, 4, 5], + })); + let _ = buff.expect_buff(4); + } } diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index a7b28b70f4a..76e308a80a2 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -587,27 +587,18 @@ impl BitcoinRegtestController { return None; } - let burned = if payload.commit_outs.len() > 0 { - let pox_transfers = payload.commit_outs.len() as u64; - let burn_remainder = (OUTPUTS_PER_COMMIT as u64) - pox_transfers; - let value_per_transfer = payload.burn_fee / (OUTPUTS_PER_COMMIT as u64); - if value_per_transfer < 5500 { - error!("Total burn fee not enough for number of outputs"); - return None; - } - for commit_to in payload.commit_outs.iter() { - tx.output - .push(commit_to.to_bitcoin_tx_out(value_per_transfer)); - } - value_per_transfer * burn_remainder - } else { - payload.burn_fee - }; - - if burned > 0 { - let burn_address_hash = Hash160([0u8; 20]); - let burn_output = BitcoinAddress::to_p2pkh_tx_out(&burn_address_hash, burned); - tx.output.push(burn_output); + if OUTPUTS_PER_COMMIT != payload.commit_outs.len() { + error!("Generated block commit with wrong OUTPUTS_PER_COMMIT"); + return None; + } + let value_per_transfer = payload.burn_fee / (OUTPUTS_PER_COMMIT as u64); + if value_per_transfer < 5500 { + error!("Total burn fee not enough for number of outputs"); + return None; + } + for commit_to in payload.commit_outs.iter() { + tx.output + .push(commit_to.to_bitcoin_tx_out(value_per_transfer)); } self.finalize_tx(&mut tx, payload.burn_fee, utxos, signer, attempt)?; diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 48af8ab8c42..469524c553a 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -211,12 +211,7 @@ fn inner_generate_block_commit_op( ) -> BlockstackOperationType { let (parent_block_ptr, parent_vtxindex) = (parent_burnchain_height, parent_winning_vtx); - let commit_outs = if let Some(recipient_set) = recipients { - let (addr, _) = recipient_set.recipient; - vec![addr] - } else { - vec![] - }; + let commit_outs = RewardSetInfo::into_commit_outs(recipients, false); BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { block_header_hash, diff --git a/testnet/stacks-node/src/node.rs b/testnet/stacks-node/src/node.rs index 6760f5fc6a9..5f1679e8cb9 100644 --- a/testnet/stacks-node/src/node.rs +++ b/testnet/stacks-node/src/node.rs @@ -9,7 +9,8 @@ use std::{thread, thread::JoinHandle, time}; use stacks::burnchains::{Burnchain, BurnchainHeaderHash, Txid}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, + leader_block_commit::RewardSetInfo, BlockstackOperationType, LeaderBlockCommitOp, + LeaderKeyRegisterOp, }; use stacks::chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; use stacks::chainstate::stacks::db::{ClarityTx, StacksChainState, StacksHeaderInfo}; @@ -692,6 +693,8 @@ impl Node { ), }; + let commit_outs = RewardSetInfo::into_commit_outs(None, false); + BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { block_header_hash, burn_fee, @@ -704,7 +707,7 @@ impl Node { parent_vtxindex, vtxindex: 0, txid: Txid([0u8; 32]), - commit_outs: vec![], + commit_outs, block_height: 0, burn_header_hash: BurnchainHeaderHash([0u8; 32]), }) diff --git a/testnet/stacks-node/src/tests/bitcoin_regtest.rs b/testnet/stacks-node/src/tests/bitcoin_regtest.rs index 9050fae70e2..11f59dda66e 100644 --- a/testnet/stacks-node/src/tests/bitcoin_regtest.rs +++ b/testnet/stacks-node/src/tests/bitcoin_regtest.rs @@ -103,6 +103,8 @@ impl Drop for BitcoinCoreController { } } +const BITCOIND_INT_TEST_COMMITS: u64 = 11000; + #[test] #[ignore] fn bitcoind_integration_test() { @@ -112,7 +114,7 @@ fn bitcoind_integration_test() { let mut conf = super::new_test_conf(); conf.burnchain.commit_anchor_block_within = 2000; - conf.burnchain.burn_fee_cap = 5000; + conf.burnchain.burn_fee_cap = BITCOIND_INT_TEST_COMMITS; conf.burnchain.mode = "helium".to_string(); conf.burnchain.peer_host = "127.0.0.1".to_string(); conf.burnchain.rpc_port = 18443; @@ -136,14 +138,14 @@ fn bitcoind_integration_test() { // In this serie of tests, the callback is fired post-burnchain-sync, pre-stacks-sync run_loop.callbacks.on_new_burn_chain_state(|round, burnchain_tip, chain_tip| { + let block = &burnchain_tip.block_snapshot; + let expected_total_burn = BITCOIND_INT_TEST_COMMITS * (round as u64 + 1); + assert_eq!(block.total_burn, expected_total_burn); + assert_eq!(block.sortition, true); + assert_eq!(block.num_sortitions, round as u64 + 1); + assert_eq!(block.block_height, round as u64 + 203); match round { 0 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 203); - assert!(block.total_burn == 5000); - assert!(block.num_sortitions == 1); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -157,19 +159,13 @@ fn bitcoind_integration_test() { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 0); assert!(op.parent_vtxindex == 0); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } } }, 1 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 204); - assert!(block.total_burn == 10000); - assert!(block.num_sortitions == 2); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -182,7 +178,7 @@ fn bitcoind_integration_test() { LeaderBlockCommit(op) => { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 203); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } @@ -191,12 +187,6 @@ fn bitcoind_integration_test() { assert!(burnchain_tip.block_snapshot.parent_burn_header_hash == chain_tip.metadata.burn_header_hash); }, 2 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 205); - assert!(block.total_burn == 15000); - assert!(block.num_sortitions == 3); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -209,7 +199,7 @@ fn bitcoind_integration_test() { LeaderBlockCommit(op) => { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 204); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } @@ -218,12 +208,6 @@ fn bitcoind_integration_test() { assert!(burnchain_tip.block_snapshot.parent_burn_header_hash == chain_tip.metadata.burn_header_hash); }, 3 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 206); - assert!(block.total_burn == 20000); - assert!(block.num_sortitions == 4); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -236,7 +220,7 @@ fn bitcoind_integration_test() { LeaderBlockCommit(op) => { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 205); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } @@ -245,12 +229,6 @@ fn bitcoind_integration_test() { assert!(burnchain_tip.block_snapshot.parent_burn_header_hash == chain_tip.metadata.burn_header_hash); }, 4 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 207); - assert!(block.total_burn == 25000); - assert!(block.num_sortitions == 5); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -263,7 +241,7 @@ fn bitcoind_integration_test() { LeaderBlockCommit(op) => { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 206); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } @@ -272,12 +250,6 @@ fn bitcoind_integration_test() { assert!(burnchain_tip.block_snapshot.parent_burn_header_hash == chain_tip.metadata.burn_header_hash); }, 5 => { - let block = &burnchain_tip.block_snapshot; - assert!(block.block_height == 208); - assert!(block.total_burn == 30000); - assert!(block.num_sortitions == 6); - assert!(block.sortition == true); - let state_transition = &burnchain_tip.state_transition; assert!(state_transition.accepted_ops.len() == 1); assert!(state_transition.consumed_leader_keys.len() == 1); @@ -290,7 +262,7 @@ fn bitcoind_integration_test() { LeaderBlockCommit(op) => { assert!(burnchain_tip.state_transition.consumed_leader_keys[0].public_key.to_hex() == "ff80684f3a5912662adbae013fb6521f10fb6ba7e4e60ccba8671b765cef8a34"); assert!(op.parent_block_ptr == 207); - assert!(op.burn_fee == 5000); + assert_eq!(op.burn_fee, BITCOIND_INT_TEST_COMMITS); } _ => assert!(false) } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 4c7875366c5..19b97332160 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -36,6 +36,7 @@ fn neon_integration_test_conf() -> (Config, StacksAddress) { conf.node.miner = true; conf.node.wait_time_for_microblocks = 500; + conf.burnchain.burn_fee_cap = 20000; conf.burnchain.mode = "neon".into(); conf.burnchain.username = Some("neon-tester".into()); @@ -770,7 +771,7 @@ fn pox_integration_test() { let http_origin = format!("http://{}", &conf.node.rpc_bind); let mut burnchain_config = btc_regtest_controller.get_burnchain(); - let mut pox_constants = PoxConstants::new(10, 5, 4, 5, 15); + let pox_constants = PoxConstants::new(10, 5, 4, 5, 15); burnchain_config.pox_constants = pox_constants; btc_regtest_controller.bootstrap_chain(201); @@ -1004,7 +1005,7 @@ fn pox_integration_test() { eprintln!("Got UTXOs: {}", utxos.len()); assert_eq!( utxos.len(), - 3, + 7, "Should have received three outputs during PoX reward cycle" ); @@ -1016,7 +1017,7 @@ fn pox_integration_test() { eprintln!("Got UTXOs: {}", utxos.len()); assert_eq!( utxos.len(), - 3, + 7, "Should have received three outputs during PoX reward cycle" );