mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-05-25 10:12:40 +08:00
1086 lines
40 KiB
Rust
1086 lines
40 KiB
Rust
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
|
|
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
use std::collections::HashMap;
|
|
use std::process::{Command, Stdio};
|
|
|
|
use stacks::burnchains::bitcoin::address::BitcoinAddress;
|
|
use stacks::burnchains::bitcoin::{BitcoinNetworkType, BitcoinTxOutput};
|
|
use stacks::burnchains::{Burnchain, BurnchainSigner, Error as BurnchainError, Txid};
|
|
use stacks::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandle};
|
|
use stacks::chainstate::burn::distribution::BurnSamplePoint;
|
|
use stacks::chainstate::burn::operations::leader_block_commit::{
|
|
MissedBlockCommit, BURN_BLOCK_MINED_AT_MODULUS,
|
|
};
|
|
use stacks::chainstate::burn::operations::LeaderBlockCommitOp;
|
|
use stacks::chainstate::stacks::address::PoxAddress;
|
|
use stacks::core::MINING_COMMITMENT_WINDOW;
|
|
use stacks::util_lib::db::Error as DBError;
|
|
use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, VRFSeed};
|
|
use stacks_common::util::hash::hex_bytes;
|
|
|
|
pub struct MinerStats {
|
|
pub unconfirmed_commits_helper: String,
|
|
}
|
|
|
|
/// Unconfirmed block-commit transaction as emitted by our helper
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
struct UnconfirmedBlockCommit {
|
|
/// burnchain signer
|
|
address: String,
|
|
/// PoX payouts
|
|
pox_addrs: Vec<String>,
|
|
/// UTXO spent to create this block-commit
|
|
input_index: u32,
|
|
input_txid: String,
|
|
/// transaction ID
|
|
txid: String,
|
|
/// amount spent
|
|
burn: u64,
|
|
}
|
|
|
|
const DEADBEEF: [u8; 32] = [
|
|
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
|
|
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
|
|
];
|
|
|
|
impl MinerStats {
|
|
/// Find the burn distribution for a single sortition's block-commits and missed-commits
|
|
fn get_burn_distribution<SH: SortitionHandle>(
|
|
sort_handle: &mut SH,
|
|
burnchain: &Burnchain,
|
|
burn_block_height: u64,
|
|
block_commits: Vec<LeaderBlockCommitOp>,
|
|
missed_commits: Vec<MissedBlockCommit>,
|
|
) -> Result<Vec<BurnSamplePoint>, BurnchainError> {
|
|
// assemble the commit windows
|
|
let mut windowed_block_commits = vec![block_commits];
|
|
let mut windowed_missed_commits = vec![];
|
|
|
|
if !burnchain.is_in_prepare_phase(burn_block_height) {
|
|
// PoX reward-phase is active!
|
|
// build a map of intended sortition -> missed commit for the missed commits
|
|
// discovered in this block.
|
|
let mut missed_commits_map: HashMap<_, Vec<_>> = HashMap::new();
|
|
for missed in missed_commits.iter() {
|
|
if let Some(commits_at_sortition) =
|
|
missed_commits_map.get_mut(&missed.intended_sortition)
|
|
{
|
|
commits_at_sortition.push(missed);
|
|
} else {
|
|
missed_commits_map.insert(missed.intended_sortition.clone(), vec![missed]);
|
|
}
|
|
}
|
|
|
|
for blocks_back in 0..(MINING_COMMITMENT_WINDOW - 1) {
|
|
if burn_block_height.saturating_sub(1) < (blocks_back as u64) {
|
|
debug!("Mining commitment window shortened because block height is less than window size";
|
|
"block_height" => %burn_block_height.saturating_sub(1),
|
|
"window_size" => %MINING_COMMITMENT_WINDOW);
|
|
break;
|
|
}
|
|
let block_height = (burn_block_height.saturating_sub(1)) - (blocks_back as u64);
|
|
let sortition_id = match sort_handle.get_block_snapshot_by_height(block_height)? {
|
|
Some(sn) => sn.sortition_id,
|
|
None => break,
|
|
};
|
|
windowed_block_commits.push(SortitionDB::get_block_commits_by_block(
|
|
sort_handle.sqlite(),
|
|
&sortition_id,
|
|
)?);
|
|
let mut missed_commits_at_height = SortitionDB::get_missed_commits_by_intended(
|
|
sort_handle.sqlite(),
|
|
&sortition_id,
|
|
)?;
|
|
if let Some(missed_commit_in_block) = missed_commits_map.remove(&sortition_id) {
|
|
missed_commits_at_height
|
|
.extend(missed_commit_in_block.into_iter().map(|x| x.clone()));
|
|
}
|
|
|
|
windowed_missed_commits.push(missed_commits_at_height);
|
|
}
|
|
} else {
|
|
// PoX reward-phase is not active
|
|
debug!(
|
|
"Block {} is in a prepare phase or post-PoX sunset, so no windowing will take place",
|
|
burn_block_height;
|
|
);
|
|
|
|
assert_eq!(windowed_block_commits.len(), 1);
|
|
assert_eq!(windowed_missed_commits.len(), 0);
|
|
}
|
|
|
|
// reverse vecs so that windows are in ascending block height order
|
|
windowed_block_commits.reverse();
|
|
windowed_missed_commits.reverse();
|
|
|
|
// figure out if the PoX sunset finished during the window,
|
|
// and/or which sortitions must be PoB due to them falling in a prepare phase.
|
|
let window_end_height = burn_block_height;
|
|
let window_start_height = window_end_height + 1 - (windowed_block_commits.len() as u64);
|
|
let mut burn_blocks = vec![false; windowed_block_commits.len()];
|
|
|
|
// set burn_blocks flags to accomodate prepare phases and PoX sunset
|
|
for (i, b) in burn_blocks.iter_mut().enumerate() {
|
|
if burnchain.is_in_prepare_phase(window_start_height + (i as u64)) {
|
|
// must burn
|
|
*b = true;
|
|
} else {
|
|
// must not burn
|
|
*b = false;
|
|
}
|
|
}
|
|
|
|
// not all commits in windowed_block_commits have been confirmed, so make sure that they
|
|
// are in the right order
|
|
let mut block_height_at_index = None;
|
|
for (index, commits) in windowed_block_commits.iter_mut().enumerate() {
|
|
let index = index as u64;
|
|
for commit in commits.iter_mut() {
|
|
if let Some((first_block_height, first_index)) = block_height_at_index {
|
|
if commit.block_height != first_block_height + (index - first_index) {
|
|
commit.block_height = first_block_height + (index - first_index);
|
|
}
|
|
} else {
|
|
block_height_at_index = Some((commit.block_height, index));
|
|
}
|
|
}
|
|
}
|
|
|
|
// calculate the burn distribution from these operations.
|
|
// The resulting distribution will contain the user burns that match block commits
|
|
let burn_dist = BurnSamplePoint::make_min_median_distribution(
|
|
windowed_block_commits,
|
|
windowed_missed_commits,
|
|
burn_blocks,
|
|
);
|
|
|
|
Ok(burn_dist)
|
|
}
|
|
|
|
fn fmt_bin_args(bin: &str, args: &[&str]) -> String {
|
|
let mut all = Vec::with_capacity(1 + args.len());
|
|
all.push(bin);
|
|
for arg in args {
|
|
all.push(arg);
|
|
}
|
|
all.join(" ")
|
|
}
|
|
|
|
/// Returns (exit code, stdout, stderr)
|
|
fn run_subprocess(
|
|
bin_fullpath: &str,
|
|
args: &[&str],
|
|
) -> Result<(i32, Vec<u8>, Vec<u8>), String> {
|
|
let full_args = Self::fmt_bin_args(bin_fullpath, args);
|
|
let mut cmd = Command::new(bin_fullpath);
|
|
cmd.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.args(args);
|
|
|
|
debug!("Run: `{:?}`", &cmd);
|
|
|
|
let output = cmd
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to run `{}`: {:?}", &full_args, &e))?
|
|
.wait_with_output()
|
|
.map_err(|ioe| format!("Failed to run `{}`: {:?}", &full_args, &ioe))?;
|
|
|
|
let exit_code = match output.status.code() {
|
|
Some(code) => code,
|
|
None => {
|
|
// failed due to signal
|
|
return Err(format!("Failed to run `{}`: killed by signal", &full_args));
|
|
}
|
|
};
|
|
|
|
Ok((exit_code, output.stdout, output.stderr))
|
|
}
|
|
|
|
/// Get the list of all unconfirmed block-commits.
|
|
pub fn get_unconfirmed_commits(
|
|
&self,
|
|
next_block_height: u64,
|
|
all_miners: &[&str],
|
|
) -> Result<Vec<LeaderBlockCommitOp>, String> {
|
|
let (exit_code, stdout, _stderr) =
|
|
Self::run_subprocess(&self.unconfirmed_commits_helper, &all_miners)?;
|
|
if exit_code != 0 {
|
|
return Err(format!(
|
|
"Failed to run `{}`: exit code {}",
|
|
&self.unconfirmed_commits_helper, exit_code
|
|
));
|
|
}
|
|
|
|
// decode stdout to JSON
|
|
let unconfirmed_commits: Vec<UnconfirmedBlockCommit> = serde_json::from_slice(&stdout)
|
|
.map_err(|e| {
|
|
format!(
|
|
"Failed to decode output from `{}`: {:?}. Output was `{}`",
|
|
&self.unconfirmed_commits_helper,
|
|
&e,
|
|
String::from_utf8_lossy(&stdout)
|
|
)
|
|
})?;
|
|
|
|
let mut unconfirmed_spends = vec![];
|
|
for unconfirmed_commit in unconfirmed_commits.into_iter() {
|
|
let Ok(txid) = Txid::from_hex(&unconfirmed_commit.txid) else {
|
|
return Err(format!("Not a valid txid: `{}`", &unconfirmed_commit.txid));
|
|
};
|
|
let Ok(input_txid) = Txid::from_hex(&unconfirmed_commit.input_txid) else {
|
|
return Err(format!(
|
|
"Not a valid txid: `{}`",
|
|
&unconfirmed_commit.input_txid
|
|
));
|
|
};
|
|
let mut decoded_pox_addrs = vec![];
|
|
for pox_addr_hex in unconfirmed_commit.pox_addrs.iter() {
|
|
let Ok(pox_addr_bytes) = hex_bytes(&pox_addr_hex) else {
|
|
return Err(format!("Not a hex string: `{}`", &pox_addr_hex));
|
|
};
|
|
let Some(bitcoin_addr) =
|
|
BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Mainnet, &pox_addr_bytes)
|
|
else {
|
|
return Err(format!(
|
|
"Not a recognized Bitcoin scriptpubkey: {}",
|
|
&pox_addr_hex
|
|
));
|
|
};
|
|
let Some(pox_addr) = PoxAddress::try_from_bitcoin_output(&BitcoinTxOutput {
|
|
address: bitcoin_addr.clone(),
|
|
units: 1,
|
|
}) else {
|
|
return Err(format!("Not a recognized PoX address: {}", &bitcoin_addr));
|
|
};
|
|
decoded_pox_addrs.push(pox_addr);
|
|
}
|
|
|
|
// mocked commit
|
|
let mocked_commit = LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash(DEADBEEF.clone()),
|
|
new_seed: VRFSeed(DEADBEEF.clone()),
|
|
parent_block_ptr: 1,
|
|
parent_vtxindex: 1,
|
|
key_block_ptr: 1,
|
|
key_vtxindex: 1,
|
|
memo: vec![],
|
|
commit_outs: decoded_pox_addrs,
|
|
burn_fee: unconfirmed_commit.burn,
|
|
input: (input_txid, unconfirmed_commit.input_index),
|
|
apparent_sender: BurnchainSigner(unconfirmed_commit.address),
|
|
txid,
|
|
vtxindex: 1,
|
|
block_height: next_block_height,
|
|
burn_parent_modulus: ((next_block_height.saturating_sub(1))
|
|
% BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash(DEADBEEF.clone()),
|
|
};
|
|
|
|
unconfirmed_spends.push(mocked_commit);
|
|
}
|
|
Ok(unconfirmed_spends)
|
|
}
|
|
|
|
/// Convert a list of burn sample points into a probability distribution by candidate's
|
|
/// apparent sender (e.g. miner address).
|
|
pub fn burn_dist_to_prob_dist(burn_dist: &[BurnSamplePoint]) -> HashMap<String, f64> {
|
|
if burn_dist.len() == 0 {
|
|
return HashMap::new();
|
|
}
|
|
if burn_dist.len() == 1 {
|
|
let mut ret = HashMap::new();
|
|
ret.insert(burn_dist[0].candidate.apparent_sender.to_string(), 1.0);
|
|
return ret;
|
|
}
|
|
|
|
let mut ret = HashMap::new();
|
|
for pt in burn_dist.iter() {
|
|
// take the upper 32 bits
|
|
let range_lower_64 = (pt.range_end - pt.range_start) >> 192;
|
|
let int_prob = (range_lower_64.low_u64() >> 32) as u32;
|
|
|
|
ret.insert(
|
|
pt.candidate.apparent_sender.to_string(),
|
|
(int_prob as f64) / (u32::MAX as f64),
|
|
);
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
/// Get the spend distribution and total spend.
|
|
/// If the miner has both a confirmed and unconfirmed spend, then take the latter.
|
|
pub fn get_spend_distribution(
|
|
active_miners_and_commits: &[(String, LeaderBlockCommitOp)],
|
|
unconfirmed_block_commits: &[LeaderBlockCommitOp],
|
|
expected_pox_addrs: &[PoxAddress],
|
|
) -> (HashMap<String, u64>, u64) {
|
|
let unconfirmed_block_commits: Vec<_> = unconfirmed_block_commits
|
|
.iter()
|
|
.filter(|commit| {
|
|
if commit.commit_outs.len() != expected_pox_addrs.len() {
|
|
return false;
|
|
}
|
|
for i in 0..commit.commit_outs.len() {
|
|
if commit.commit_outs[i].to_burnchain_repr()
|
|
!= expected_pox_addrs[i].to_burnchain_repr()
|
|
{
|
|
info!(
|
|
"Skipping invalid unconfirmed block-commit: {:?} != {:?}",
|
|
&commit.commit_outs[i].to_burnchain_repr(),
|
|
expected_pox_addrs[i].to_burnchain_repr()
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
})
|
|
.collect();
|
|
|
|
let mut total_spend = 0;
|
|
let mut dist = HashMap::new();
|
|
for commit in unconfirmed_block_commits {
|
|
let addr = commit.apparent_sender.to_string();
|
|
dist.insert(addr, commit.burn_fee);
|
|
}
|
|
|
|
for (_, commit) in active_miners_and_commits.iter() {
|
|
let addr = commit.apparent_sender.to_string();
|
|
if dist.contains_key(&addr) {
|
|
continue;
|
|
}
|
|
dist.insert(addr, commit.burn_fee);
|
|
}
|
|
|
|
for (_, spend) in dist.iter() {
|
|
total_spend += *spend;
|
|
}
|
|
|
|
(dist, total_spend)
|
|
}
|
|
|
|
/// Get the probability distribution for the Bitcoin block 6+ blocks in the future, assuming
|
|
/// all block-commit spends remain the same.
|
|
pub fn get_future_win_distribution(
|
|
active_miners_and_commits: &[(String, LeaderBlockCommitOp)],
|
|
unconfirmed_block_commits: &[LeaderBlockCommitOp],
|
|
expected_pox_addrs: &[PoxAddress],
|
|
) -> HashMap<String, f64> {
|
|
let (dist, total_spend) = Self::get_spend_distribution(
|
|
active_miners_and_commits,
|
|
unconfirmed_block_commits,
|
|
&expected_pox_addrs,
|
|
);
|
|
|
|
let mut probs = HashMap::new();
|
|
for (addr, spend) in dist.into_iter() {
|
|
if total_spend == 0 {
|
|
probs.insert(addr, 0.0);
|
|
} else {
|
|
probs.insert(addr, (spend as f64) / (total_spend as f64));
|
|
}
|
|
}
|
|
probs
|
|
}
|
|
|
|
/// Get the burn distribution for the _next_ Bitcoin block, assuming that the given list of
|
|
/// block-commit data will get mined. For miners that are known to the system but who do not
|
|
/// have unconfirmed block-commits, infer that they'll just mine the same block-commit value
|
|
/// again.
|
|
pub fn get_unconfirmed_burn_distribution(
|
|
&self,
|
|
burnchain: &Burnchain,
|
|
sortdb: &SortitionDB,
|
|
active_miners_and_commits: &[(String, LeaderBlockCommitOp)],
|
|
unconfirmed_block_commits: Vec<LeaderBlockCommitOp>,
|
|
expected_pox_addrs: &[PoxAddress],
|
|
at_block: Option<u64>,
|
|
) -> Result<Vec<BurnSamplePoint>, BurnchainError> {
|
|
let mut commit_table = HashMap::new();
|
|
for commit in unconfirmed_block_commits.iter() {
|
|
commit_table.insert(commit.apparent_sender.to_string(), commit.clone());
|
|
}
|
|
|
|
let tip = if let Some(at_block) = at_block {
|
|
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?;
|
|
let ih = sortdb.index_handle(&tip.sortition_id);
|
|
ih.get_block_snapshot_by_height(at_block)?
|
|
.ok_or(BurnchainError::MissingParentBlock)?
|
|
} else {
|
|
SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?
|
|
};
|
|
|
|
let next_block_height = tip.block_height + 1;
|
|
let expected_input_index = if burnchain.is_in_prepare_phase(tip.block_height) {
|
|
LeaderBlockCommitOp::expected_chained_utxo(true)
|
|
} else {
|
|
LeaderBlockCommitOp::expected_chained_utxo(false)
|
|
};
|
|
|
|
for (miner, last_commit) in active_miners_and_commits.iter() {
|
|
if !commit_table.contains_key(miner) {
|
|
let mocked_commit = LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash(DEADBEEF.clone()),
|
|
new_seed: VRFSeed(DEADBEEF.clone()),
|
|
parent_block_ptr: 2,
|
|
parent_vtxindex: 2,
|
|
key_block_ptr: 2,
|
|
key_vtxindex: 2,
|
|
memo: vec![],
|
|
commit_outs: expected_pox_addrs.to_vec(),
|
|
burn_fee: last_commit.burn_fee,
|
|
input: (last_commit.txid, expected_input_index),
|
|
apparent_sender: last_commit.apparent_sender.clone(),
|
|
txid: Txid(DEADBEEF.clone()),
|
|
vtxindex: 1,
|
|
block_height: next_block_height,
|
|
burn_parent_modulus: ((next_block_height.saturating_sub(1))
|
|
% BURN_BLOCK_MINED_AT_MODULUS)
|
|
as u8,
|
|
burn_header_hash: BurnchainHeaderHash(DEADBEEF.clone()),
|
|
};
|
|
commit_table.insert(miner.to_string(), mocked_commit);
|
|
}
|
|
}
|
|
|
|
let unconfirmed_block_commits: Vec<_> = commit_table
|
|
.into_values()
|
|
.filter(|commit| {
|
|
if commit.commit_outs.len() != expected_pox_addrs.len() {
|
|
return false;
|
|
}
|
|
for i in 0..commit.commit_outs.len() {
|
|
if commit.commit_outs[i].to_burnchain_repr()
|
|
!= expected_pox_addrs[i].to_burnchain_repr()
|
|
{
|
|
info!(
|
|
"Skipping invalid unconfirmed block-commit: {:?} != {:?}",
|
|
&commit.commit_outs[i].to_burnchain_repr(),
|
|
expected_pox_addrs[i].to_burnchain_repr()
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
})
|
|
.collect();
|
|
|
|
let mut handle = sortdb.index_handle(&tip.sortition_id);
|
|
Self::get_burn_distribution(
|
|
&mut handle,
|
|
burnchain,
|
|
tip.block_height + 1,
|
|
unconfirmed_block_commits,
|
|
vec![],
|
|
)
|
|
}
|
|
|
|
/// Given the sortition DB, get the list of all miners in the past MINING_COMMITMENT_WINDOW
|
|
/// blocks, as well as their last block-commits
|
|
pub fn get_active_miners(
|
|
sortdb: &SortitionDB,
|
|
at_burn_block: Option<u64>,
|
|
) -> Result<Vec<(String, LeaderBlockCommitOp)>, DBError> {
|
|
let mut tip = if let Some(at_burn_block) = at_burn_block {
|
|
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?;
|
|
let ih = sortdb.index_handle(&tip.sortition_id);
|
|
ih.get_block_snapshot_by_height(at_burn_block)?
|
|
.ok_or(DBError::NotFoundError)?
|
|
} else {
|
|
SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?
|
|
};
|
|
|
|
let mut miners = HashMap::new();
|
|
for _i in 0..MINING_COMMITMENT_WINDOW {
|
|
let commits =
|
|
SortitionDB::get_block_commits_by_block(sortdb.conn(), &tip.sortition_id)?;
|
|
for commit in commits.into_iter() {
|
|
let miner = commit.apparent_sender.to_string();
|
|
if miners.get(&miner).is_none() {
|
|
miners.insert(miner, commit);
|
|
}
|
|
}
|
|
tip = SortitionDB::get_block_snapshot(sortdb.conn(), &tip.parent_sortition_id)?
|
|
.ok_or(DBError::NotFoundError)?;
|
|
}
|
|
Ok(miners.into_iter().collect())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use std::fs;
|
|
use std::io::Write;
|
|
|
|
use stacks::burnchains::{BurnchainSigner, Txid};
|
|
use stacks::chainstate::burn::distribution::BurnSamplePoint;
|
|
use stacks::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS;
|
|
use stacks::chainstate::burn::operations::LeaderBlockCommitOp;
|
|
use stacks::chainstate::stacks::address::{PoxAddress, PoxAddressType20};
|
|
use stacks_common::types::chainstate::{
|
|
BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksPublicKey, VRFSeed,
|
|
};
|
|
use stacks_common::util::hash::{hex_bytes, Hash160};
|
|
use stacks_common::util::uint::{BitArray, Uint256};
|
|
|
|
use super::MinerStats;
|
|
|
|
#[test]
|
|
fn test_burn_dist_to_prob_dist() {
|
|
let block_commit_1 = LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 111,
|
|
parent_vtxindex: 456,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 12345,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner::new_p2pkh(
|
|
&StacksPublicKey::from_hex(
|
|
"02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 443,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x00; 32]),
|
|
};
|
|
|
|
let block_commit_2 = LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 112,
|
|
parent_vtxindex: 111,
|
|
key_block_ptr: 122,
|
|
key_vtxindex: 457,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 12345,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner::new_p2pkh(
|
|
&StacksPublicKey::from_hex(
|
|
"023616a344700c9455bf0b55cc65e404c7b8f82e815da885398a44f6dc70e64045",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d0")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 444,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash::from_hex(
|
|
"0000000000000000000000000000000000000000000000000000000000000004",
|
|
)
|
|
.unwrap(),
|
|
};
|
|
|
|
let block_commit_3 = LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 113,
|
|
parent_vtxindex: 111,
|
|
key_block_ptr: 121,
|
|
key_vtxindex: 10,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 23456,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner::new_p2pkh(
|
|
&StacksPublicKey::from_hex(
|
|
"020a9b0a938a2226694fe4f867193cf0b78cd6264e4277fd686468a00a9afdc36d",
|
|
)
|
|
.unwrap(),
|
|
),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("301dc687a9f06a1ae87a013f27133e9cec0843c2983567be73e185827c7c13de")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 445,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash::from_hex(
|
|
"0000000000000000000000000000000000000000000000000000000000000004",
|
|
)
|
|
.unwrap(),
|
|
};
|
|
let burn_dist = vec![
|
|
BurnSamplePoint {
|
|
burns: block_commit_1.burn_fee.into(),
|
|
median_burn: block_commit_2.burn_fee.into(),
|
|
range_start: Uint256::zero(),
|
|
range_end: Uint256([
|
|
0x3ed94d3cb0a84709,
|
|
0x0963dded799a7c1a,
|
|
0x70989faf596c8b65,
|
|
0x41a3ed94d3cb0a84,
|
|
]),
|
|
candidate: block_commit_1.clone(),
|
|
},
|
|
BurnSamplePoint {
|
|
burns: block_commit_2.burn_fee.into(),
|
|
median_burn: block_commit_2.burn_fee.into(),
|
|
range_start: Uint256([
|
|
0x3ed94d3cb0a84709,
|
|
0x0963dded799a7c1a,
|
|
0x70989faf596c8b65,
|
|
0x41a3ed94d3cb0a84,
|
|
]),
|
|
range_end: Uint256([
|
|
0x7db29a7961508e12,
|
|
0x12c7bbdaf334f834,
|
|
0xe1313f5eb2d916ca,
|
|
0x8347db29a7961508,
|
|
]),
|
|
candidate: block_commit_2.clone(),
|
|
},
|
|
BurnSamplePoint {
|
|
burns: (block_commit_3.burn_fee).into(),
|
|
median_burn: block_commit_3.burn_fee.into(),
|
|
range_start: Uint256([
|
|
0x7db29a7961508e12,
|
|
0x12c7bbdaf334f834,
|
|
0xe1313f5eb2d916ca,
|
|
0x8347db29a7961508,
|
|
]),
|
|
range_end: Uint256::max(),
|
|
candidate: block_commit_3.clone(),
|
|
},
|
|
];
|
|
|
|
let prob_dist = MinerStats::burn_dist_to_prob_dist(&burn_dist);
|
|
assert_eq!(prob_dist.len(), 3);
|
|
assert!(
|
|
(prob_dist
|
|
.get(&format!("{}", &block_commit_1.apparent_sender))
|
|
.unwrap()
|
|
- 0.25641)
|
|
.abs()
|
|
< 0.001
|
|
);
|
|
assert!(
|
|
(prob_dist
|
|
.get(&format!("{}", &block_commit_2.apparent_sender))
|
|
.unwrap()
|
|
- 0.25641)
|
|
.abs()
|
|
< 0.001
|
|
);
|
|
assert!(
|
|
(prob_dist
|
|
.get(&format!("{}", &block_commit_3.apparent_sender))
|
|
.unwrap()
|
|
- 0.48718)
|
|
.abs()
|
|
< 0.001
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_unconfirmed_commits() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let shell_code = r#"#!/bin/bash
|
|
echo <<EOF '[
|
|
{
|
|
"txid": "73c318be8cd272a73200b9630089d77a44342d84b2c0d81c937da714152cf402",
|
|
"burn": 555000,
|
|
"address": "1FCcoFSKWvNyhjazNvVdLLw8mGkGdcRMux",
|
|
"input_txid": "ef0dbf0fc4755de5e94843a4da7c1d943571299afb15f32b76bac5d18d8668ce",
|
|
"input_index": 3,
|
|
"pox_addrs": [
|
|
"0014db14133a9dbb1d0e16b60513453e48b6ff2847a9",
|
|
"a91418c42080a1e87fd02dd3fca94c4513f9ecfe741487"
|
|
]
|
|
}
|
|
]'
|
|
EOF
|
|
"#;
|
|
let path = "/tmp/test-get-unconfirmed-commits.sh";
|
|
if fs::metadata(&path).is_ok() {
|
|
fs::remove_file(&path).unwrap();
|
|
}
|
|
{
|
|
let mut f = fs::File::create(&path).unwrap();
|
|
f.write_all(shell_code.as_bytes()).unwrap();
|
|
|
|
let md = f.metadata().unwrap();
|
|
let mut permissions = md.permissions();
|
|
permissions.set_mode(0o744);
|
|
|
|
fs::set_permissions(path, permissions).unwrap();
|
|
f.sync_all().unwrap();
|
|
}
|
|
|
|
let ms = MinerStats {
|
|
unconfirmed_commits_helper: path.to_string(),
|
|
};
|
|
|
|
let mut commits = ms.get_unconfirmed_commits(123, &[]).unwrap();
|
|
assert_eq!(commits.len(), 1);
|
|
let commit = commits.pop().unwrap();
|
|
|
|
assert_eq!(
|
|
commit.txid,
|
|
Txid::from_hex("73c318be8cd272a73200b9630089d77a44342d84b2c0d81c937da714152cf402")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(commit.burn_fee, 555000);
|
|
assert_eq!(
|
|
commit.apparent_sender.0,
|
|
"1FCcoFSKWvNyhjazNvVdLLw8mGkGdcRMux".to_string()
|
|
);
|
|
assert_eq!(
|
|
commit.input.0,
|
|
Txid::from_hex("ef0dbf0fc4755de5e94843a4da7c1d943571299afb15f32b76bac5d18d8668ce")
|
|
.unwrap()
|
|
);
|
|
assert_eq!(commit.input.1, 3);
|
|
assert_eq!(commit.block_height, 123);
|
|
|
|
assert_eq!(
|
|
commit.commit_outs,
|
|
vec![
|
|
PoxAddress::Addr20(
|
|
true,
|
|
PoxAddressType20::P2WPKH,
|
|
[
|
|
219, 20, 19, 58, 157, 187, 29, 14, 22, 182, 5, 19, 69, 62, 72, 182, 255,
|
|
40, 71, 169
|
|
]
|
|
),
|
|
PoxAddress::Standard(
|
|
StacksAddress {
|
|
version: 20,
|
|
bytes: Hash160([
|
|
0x18, 0xc4, 0x20, 0x80, 0xa1, 0xe8, 0x7f, 0xd0, 0x2d, 0xd3, 0xfc, 0xa9,
|
|
0x4c, 0x45, 0x13, 0xf9, 0xec, 0xfe, 0x74, 0x14
|
|
])
|
|
},
|
|
None
|
|
)
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_spend_and_win_distribution() {
|
|
let active_miners_and_commits = vec![
|
|
(
|
|
"miner-1".to_string(),
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 111,
|
|
parent_vtxindex: 456,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 2,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-1".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes(
|
|
"3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf",
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 443,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x00; 32]),
|
|
},
|
|
),
|
|
(
|
|
"miner-2".to_string(),
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 112,
|
|
parent_vtxindex: 111,
|
|
key_block_ptr: 122,
|
|
key_vtxindex: 457,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 3,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-2".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes(
|
|
"3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d0",
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 444,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash::from_hex(
|
|
"0000000000000000000000000000000000000000000000000000000000000004",
|
|
)
|
|
.unwrap(),
|
|
},
|
|
),
|
|
(
|
|
"miner-3".to_string(),
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 113,
|
|
parent_vtxindex: 111,
|
|
key_block_ptr: 121,
|
|
key_vtxindex: 10,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 5,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-3".into()),
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes(
|
|
"301dc687a9f06a1ae87a013f27133e9cec0843c2983567be73e185827c7c13de",
|
|
)
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 445,
|
|
block_height: 124,
|
|
burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash::from_hex(
|
|
"0000000000000000000000000000000000000000000000000000000000000004",
|
|
)
|
|
.unwrap(),
|
|
},
|
|
),
|
|
];
|
|
|
|
let unconfirmed_block_commits = vec![
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 124,
|
|
parent_vtxindex: 456,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 2,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-1".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 443,
|
|
block_height: 125,
|
|
burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x01; 32]),
|
|
},
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 124,
|
|
parent_vtxindex: 444,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 3,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-2".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 444,
|
|
block_height: 125,
|
|
burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x01; 32]),
|
|
},
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 124,
|
|
parent_vtxindex: 445,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 10,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-3".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 445,
|
|
block_height: 125,
|
|
burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x01; 32]),
|
|
},
|
|
LeaderBlockCommitOp {
|
|
sunset_burn: 0,
|
|
block_header_hash: BlockHeaderHash([0x22; 32]),
|
|
new_seed: VRFSeed([0x33; 32]),
|
|
parent_block_ptr: 124,
|
|
parent_vtxindex: 445,
|
|
key_block_ptr: 123,
|
|
key_vtxindex: 456,
|
|
memo: vec![0x80],
|
|
|
|
burn_fee: 10,
|
|
input: (Txid([0; 32]), 0),
|
|
apparent_sender: BurnchainSigner("miner-4".into()),
|
|
|
|
commit_outs: vec![],
|
|
|
|
txid: Txid::from_bytes_be(
|
|
&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf")
|
|
.unwrap(),
|
|
)
|
|
.unwrap(),
|
|
vtxindex: 446,
|
|
block_height: 125,
|
|
burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8,
|
|
burn_header_hash: BurnchainHeaderHash([0x01; 32]),
|
|
},
|
|
];
|
|
|
|
let (spend_dist, total_spend) = MinerStats::get_spend_distribution(
|
|
&active_miners_and_commits,
|
|
&unconfirmed_block_commits,
|
|
&[],
|
|
);
|
|
assert_eq!(total_spend, 2 + 3 + 10 + 10);
|
|
|
|
assert_eq!(spend_dist.len(), 4);
|
|
for miner in &[
|
|
"miner-1".to_string(),
|
|
"miner-2".to_string(),
|
|
"miner-3".to_string(),
|
|
"miner-4".to_string(),
|
|
] {
|
|
let spend = *spend_dist
|
|
.get(miner)
|
|
.unwrap_or_else(|| panic!("no spend for {}", &miner));
|
|
match miner.as_str() {
|
|
"miner-1" => {
|
|
assert_eq!(spend, 2);
|
|
}
|
|
"miner-2" => {
|
|
assert_eq!(spend, 3);
|
|
}
|
|
"miner-3" => {
|
|
assert_eq!(spend, 10);
|
|
}
|
|
"miner-4" => {
|
|
assert_eq!(spend, 10);
|
|
}
|
|
_ => {
|
|
panic!("unknown miner {}", &miner);
|
|
}
|
|
}
|
|
}
|
|
|
|
let win_probs = MinerStats::get_future_win_distribution(
|
|
&active_miners_and_commits,
|
|
&unconfirmed_block_commits,
|
|
&[],
|
|
);
|
|
for miner in &[
|
|
"miner-1".to_string(),
|
|
"miner-2".to_string(),
|
|
"miner-3".to_string(),
|
|
"miner-4".to_string(),
|
|
] {
|
|
let prob = *win_probs
|
|
.get(miner)
|
|
.unwrap_or_else(|| panic!("no probability for {}", &miner));
|
|
match miner.as_str() {
|
|
"miner-1" => {
|
|
assert!((prob - (2.0 / 25.0)).abs() < 0.00001);
|
|
}
|
|
"miner-2" => {
|
|
assert!((prob - (3.0 / 25.0)).abs() < 0.00001);
|
|
}
|
|
"miner-3" => {
|
|
assert!((prob - (10.0 / 25.0)).abs() < 0.00001);
|
|
}
|
|
"miner-4" => {
|
|
assert!((prob - (10.0 / 25.0)).abs() < 0.00001);
|
|
}
|
|
_ => {
|
|
panic!("unknown miner {}", &miner);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|