From ddeac381ba6b7320a9384b4dcd184fda7b364005 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 09:59:05 -0400 Subject: [PATCH 01/56] feat: ATC-C advantage function and lookup table --- stackslib/src/chainstate/burn/atc.rs | 1396 ++++++++++++++++++++++++++ 1 file changed, 1396 insertions(+) create mode 100644 stackslib/src/chainstate/burn/atc.rs diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs new file mode 100644 index 000000000..68716acf0 --- /dev/null +++ b/stackslib/src/chainstate/burn/atc.rs @@ -0,0 +1,1396 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020 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 . + +use crate::stacks_common::util::uint::BitArray; +use stacks_common::util::uint::Uint256; + +/// A fixed-point numerical representation for ATC. THe integer and fractional parts are both 64 +/// bits. +/// +/// The reasons we use this instead of f64 for ATC calculations are as follows: +/// * This avoids unrepresentable states, like NaN or +/- INF +/// * This avoids ambituous states, like +0.0 and -0.0. +/// * This integrates better into the sortition-sampling system, which uses a u256 to represent a +/// probability range (which is what this is going to be used for) +#[derive(Debug, Clone, PartialEq, Copy, Eq, Hash)] +pub(crate) struct AtcRational(pub(crate) Uint256); +impl AtcRational { + /// Construct from a fraction (numerator and denominator) + pub fn frac(num: u64, den: u64) -> Self { + Self((Uint256::from_u64(num) << 64) / Uint256::from_u64(den)) + } + + /// 0 value + pub fn zero() -> Self { + Self(Uint256::zero()) + } + + /// 1 value + pub fn one() -> Self { + Self(Uint256::one() << 64) + } + + /// largest value less than 1 + pub fn one_sup() -> Self { + Self((Uint256::one() << 64) - Uint256::from_u64(1)) + } + + /// Largest possible value (corresponds to u64::MAX.u64::MAX) + pub fn max() -> Self { + Self((Uint256::from_u64(u64::MAX) << 64) | Uint256::from_u64(u64::MAX)) + } + + /// Get integer part + pub fn ipart(&self) -> u64 { + (self.0 >> 64).low_u64() + } + + /// Checked addition + pub fn add(&self, other: &AtcRational) -> Option { + // NOTE: this is always safe since u128::MAX + u128::MAX < Uint256::max() + let sum = AtcRational(self.0 + other.0); + if sum.0 > Self::max().0 { + return None; + } + Some(sum) + } + + /// Checked subtraction + pub fn sub(&self, other: &AtcRational) -> Option { + if self.0 < other.0 { + return None; + } + Some(AtcRational(self.0 - other.0)) + } + + /// Checked multiplication + pub fn mul(&self, other: &AtcRational) -> Option { + // NOTE: this is always safe since u128::MAX * u128::MAX < Uint256::max() + let prod = AtcRational((self.0 * other.0) >> 64); + if prod.0 > Self::max().0 { + return None; + } + Some(prod) + } + + /// Minimum of self and other + pub fn min(&self, other: &AtcRational) -> Self { + if self.0 < other.0 { + Self(self.0.clone()) + } else { + Self(other.0.clone()) + } + } + + /// Hex representation of the inner bits + pub fn to_hex(&self) -> String { + self.0.to_hex_be() + } + + /// Inner u256, for conversion to something a BurnSamplePoint can use + pub fn into_inner(self) -> Uint256 { + self.0 + } +} + +#[cfg(test)] +mod test { + use crate::chainstate::burn::atc::AtcRational; + use crate::chainstate::burn::BlockSnapshot; + use stacks_common::util::hash::to_hex; + use stacks_common::util::uint::Uint256; + + impl AtcRational { + /// Convert to f64, and panic on conversion failure + pub fn to_f64(&self) -> f64 { + let ipart = self.ipart() as f64; + let fpart = self.0.low_u64() as f64; + ipart + (fpart / (u64::MAX as f64)) + } + + /// Convert from f64 between 0 and 1, panicking on conversion failure. Scales up the f64 so that its + /// fractional parts reside in the lower 64 bits of the AtcRational. + pub fn from_f64_unit(value: f64) -> Self { + if value < 0.0 || value >= 1.0 { + panic!("only usable for values in [0.0, 1.0) range"); + } + + // NOTE: this only changes the exponent, not the mantissa. + // Moreover, u128::from(u64::MAX) + 1 has f64 representation 0x43f0000000000000, so these conversions are safe. + let scaled_value = value * ((u128::from(u64::MAX) + 1) as f64); + + // this is safe, because 0.0 <= value < 1.0, so scaled_value <= u64::MAX + let value_u64 = scaled_value as u64; + Self(Uint256::from_u64(value_u64)) + } + } + + #[test] + fn test_atc_rational() { + assert_eq!(AtcRational::frac(1, 1), AtcRational::one()); + assert_eq!( + AtcRational::frac(1, 2).0, + Uint256::from_u64(u64::MAX / 2) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 4).0, + Uint256::from_u64(u64::MAX / 4) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 8).0, + Uint256::from_u64(u64::MAX / 8) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 16).0, + Uint256::from_u64(u64::MAX / 16) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 32).0, + Uint256::from_u64(u64::MAX / 32) + Uint256::from_u64(1) + ); + + assert_eq!( + AtcRational::frac(1, 2) + .add(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::one() + ); + assert_eq!( + AtcRational::frac(1, 4) + .add(&AtcRational::frac(1, 4)) + .unwrap(), + AtcRational::frac(1, 2) + ); + assert_eq!( + AtcRational::frac(1, 8) + .add(&AtcRational::frac(1, 8)) + .unwrap(), + AtcRational::frac(1, 4) + ); + assert_eq!( + AtcRational::frac(3, 8) + .add(&AtcRational::frac(3, 8)) + .unwrap(), + AtcRational::frac(3, 4) + ); + assert_eq!( + AtcRational::max().add(&AtcRational(Uint256::from_u64(1))), + None + ); + + assert_eq!( + AtcRational::frac(1, 2) + .sub(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::zero() + ); + + assert_eq!( + AtcRational::one().sub(&AtcRational::frac(1, 2)).unwrap(), + AtcRational::frac(1, 2) + ); + assert_eq!( + AtcRational::one().sub(&AtcRational::frac(1, 32)).unwrap(), + AtcRational::frac(31, 32) + ); + + assert_eq!( + AtcRational::frac(1, 2) + .mul(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::frac(1, 4) + ); + assert_eq!( + AtcRational::frac(5, 6) + .mul(&AtcRational::frac(7, 8)) + .unwrap(), + AtcRational::frac(35, 48) + ); + assert_eq!( + AtcRational::frac(100, 2) + .mul(&AtcRational::frac(200, 4)) + .unwrap(), + AtcRational::frac(20000, 8) + ); + assert_eq!( + AtcRational::frac(1, 2) + .mul(&AtcRational::frac(1024, 1)) + .unwrap(), + AtcRational::frac(512, 1) + ); + + assert_eq!( + AtcRational::frac(1, 2).min(&AtcRational::frac(15, 32)), + AtcRational::frac(15, 32) + ); + } + + #[test] + #[ignore] + fn print_functions() { + let mut grid: Vec> = vec![vec![' '; 100]; 102]; + for i in 0..100 { + let f_atc = (i as f64) / 100.0; + let atc = AtcRational::frac(i as u64, 100); + let l_atc = BlockSnapshot::null_miner_logistic(atc).to_f64(); + let p_atc = BlockSnapshot::null_miner_probability(atc).to_f64(); + + // NOTE: columns increase downwards, so flip this + let l_atc_100 = 100 - ((l_atc * 100.0) as usize); + let p_atc_100 = 100 - ((p_atc * 100.0) as usize); + let a_atc_100 = 100 - (((1.0 - f_atc) * 100.0) as usize); + grid[a_atc_100][i] = '$'; + grid[l_atc_100][i] = '#'; + grid[p_atc_100][i] = '^'; + } + for j in 0..100 { + grid[101][j] = '_'; + } + + println!(""); + for row in grid.iter() { + let grid_str: String = row.clone().into_iter().collect(); + println!("|{}", &grid_str); + } + } + + /// Calculate the logic advantage curve for the null miner. + /// This function's parameters are chosen such that: + /// * if the ATC carryover has diminished by less than 20%, the null miner has negligeable + /// chances of winning. This is to avoid punishing honest miners when there are flash blocks. + /// * If the ATC carryover has diminished by between 20% and 80%, the null miner has a + /// better-than-linear probability of winning. That is, if the burnchain MEV miner pays less + /// than X% of the expected carryover (20% <= X < 80%), then their probability of winning is + /// (1) strictly less than X%, and (2) strictly less than any Pr[X% - c] for 0 < c < X. + /// * If the ATC carryover is less than 20%, the null miner has an overwhelmingly likely chance + /// of winning (>95%). + /// + /// The logistic curve fits the points (atc=0.2, null_prob=0.75) and (atc=0.8, null_prob=0.01). + fn null_miner_logistic(atc: f64) -> f64 { + // recall the inverted logistic function: + // + // L + // f(x) = --------------------- + // -k * (x0 - x) + // 1 + e + // + // It is shaped like a *backwards* "S" -- it approaches L as `x` tends towards negative + // infinity, and it approaches 0 as `x` tends towards positive infinity. This function is + // the null miner advantage function, where `x` is the ATC carryover value. + // + // We need to drive x0 and k from our two points: + // + // (x1, y1) = (0.2, 0.75) + // (x2, y2) = (0.8, 0.01) + // + // to derive L, x0, and k: + // L = 0.8 + // z = ln(L/y1 - 1) / ln(L/y2 - 1) + // x0 = (x1 - z * x2) / (1 - z) + // k = ln(L/y1 - 1) / (x1 - x0) + // + // The values for x0 and k were generated with the following GNU bc script: + // ``` + // $ cat /tmp/variables.bc + // scale=32 + // supremum=0.8 /* this is L */ + // x1=0.2 + // y1=0.75 + // x2=0.8 + // y2=0.01 + // z=l(supremum/y1 - 1)/l(supremum/y2 -1) + // x0=(x1 - z * x2)/(1 - z) + // k=l(supremum/y1 - 1)/(x1 - x0) + // print "x0 = "; x0 + // print "k = "; k + // ``` + // + // This script evaluates to: + // ``` + // $ bc -l < /tmp/variables.bc + // x0 = .42957690816204645842320195118064 + // k = 11.79583008928205260028158351938437 + // ``` + + let L: f64 = 0.8; + + // truncated f64 + let x0: f64 = 0.42957690816204647; + let k: f64 = 11.795830089282052; + + // natural logarithm constant + let e: f64 = 2.718281828459045; + + let adv = L / (1.0 + e.powf(-k * (x0 - atc))); + adv + } + + #[test] + #[ignore] + fn make_null_miner_lookup_table() { + let mut lookup_table = Vec::with_capacity(1024); + for atc in 0..1024 { + let fatc = (atc as f64) / 1024.0; + let lgst_fatc = null_miner_logistic(fatc); + let lgst_rational = AtcRational::from_f64_unit(lgst_fatc); + lookup_table.push(lgst_rational); + } + println!("["); + for lt in lookup_table.into_iter() { + let inner = lt.into_inner(); + println!(" AtcRational(Uint256({:?})),", &inner.0); + } + println!("]"); + } +} + +/// Pre-calculated 1024-member lookup table for the null miner advantage function, as AtcRational +/// fixed point integers. The first item corresponds to the value of the function at 0.0, and the +/// last item corresponds to the function at 1.0 - (1.0 / 1024.0). The input to a function is the +/// assumed total commit carryover -- the ratio between what the winning miner paid in this +/// block-commit to the median of what they historically paid (for an epoch-defined search window +/// size). A value greater than 1.0 means that the miner paid all of the assumed commit +/// carry-over, and the null miner has negligeable chances of winning. A value less than 1.0 means +/// that the miner underpaid relative to their past performance, and the closer to 0.0 this ratio +/// is, the more likely the null miner wins and this miner loses. +/// +/// This table is generated with `make_null_miner_lookup_table()` above. +pub(crate) const ATC_LOOKUP: [AtcRational; 1024] = [ + AtcRational(Uint256([14665006693661589504, 0, 0, 0])), + AtcRational(Uint256([14663943061084833792, 0, 0, 0])), + AtcRational(Uint256([14662867262262108160, 0, 0, 0])), + AtcRational(Uint256([14661779159858638848, 0, 0, 0])), + AtcRational(Uint256([14660678615031697408, 0, 0, 0])), + AtcRational(Uint256([14659565487415023616, 0, 0, 0])), + AtcRational(Uint256([14658439635103131648, 0, 0, 0])), + AtcRational(Uint256([14657300914635431936, 0, 0, 0])), + AtcRational(Uint256([14656149180980262912, 0, 0, 0])), + AtcRational(Uint256([14654984287518758912, 0, 0, 0])), + AtcRational(Uint256([14653806086028572672, 0, 0, 0])), + AtcRational(Uint256([14652614426667460608, 0, 0, 0])), + AtcRational(Uint256([14651409157956749312, 0, 0, 0])), + AtcRational(Uint256([14650190126764625920, 0, 0, 0])), + AtcRational(Uint256([14648957178289305600, 0, 0, 0])), + AtcRational(Uint256([14647710156042049536, 0, 0, 0])), + AtcRational(Uint256([14646448901830051840, 0, 0, 0])), + AtcRational(Uint256([14645173255739158528, 0, 0, 0])), + AtcRational(Uint256([14643883056116467712, 0, 0, 0])), + AtcRational(Uint256([14642578139552755712, 0, 0, 0])), + AtcRational(Uint256([14641258340864796672, 0, 0, 0])), + AtcRational(Uint256([14639923493077501952, 0, 0, 0])), + AtcRational(Uint256([14638573427405920256, 0, 0, 0])), + AtcRational(Uint256([14637207973237102592, 0, 0, 0])), + AtcRational(Uint256([14635826958111819776, 0, 0, 0])), + AtcRational(Uint256([14634430207706118144, 0, 0, 0])), + AtcRational(Uint256([14633017545812742144, 0, 0, 0])), + AtcRational(Uint256([14631588794322399232, 0, 0, 0])), + AtcRational(Uint256([14630143773204873216, 0, 0, 0])), + AtcRational(Uint256([14628682300490010624, 0, 0, 0])), + AtcRational(Uint256([14627204192248543232, 0, 0, 0])), + AtcRational(Uint256([14625709262572754944, 0, 0, 0])), + AtcRational(Uint256([14624197323557009408, 0, 0, 0])), + AtcRational(Uint256([14622668185278134272, 0, 0, 0])), + AtcRational(Uint256([14621121655775633408, 0, 0, 0])), + AtcRational(Uint256([14619557541031794688, 0, 0, 0])), + AtcRational(Uint256([14617975644951588864, 0, 0, 0])), + AtcRational(Uint256([14616375769342470144, 0, 0, 0])), + AtcRational(Uint256([14614757713894002688, 0, 0, 0])), + AtcRational(Uint256([14613121276157339648, 0, 0, 0])), + AtcRational(Uint256([14611466251524579328, 0, 0, 0])), + AtcRational(Uint256([14609792433207912448, 0, 0, 0])), + AtcRational(Uint256([14608099612218703872, 0, 0, 0])), + AtcRational(Uint256([14606387577346342912, 0, 0, 0])), + AtcRational(Uint256([14604656115137021952, 0, 0, 0])), + AtcRational(Uint256([14602905009872304128, 0, 0, 0])), + AtcRational(Uint256([14601134043547590656, 0, 0, 0])), + AtcRational(Uint256([14599342995850407936, 0, 0, 0])), + AtcRational(Uint256([14597531644138579968, 0, 0, 0])), + AtcRational(Uint256([14595699763418222592, 0, 0, 0])), + AtcRational(Uint256([14593847126321623040, 0, 0, 0])), + AtcRational(Uint256([14591973503084957696, 0, 0, 0])), + AtcRational(Uint256([14590078661525866496, 0, 0, 0])), + AtcRational(Uint256([14588162367020904448, 0, 0, 0])), + AtcRational(Uint256([14586224382482810880, 0, 0, 0])), + AtcRational(Uint256([14584264468337694720, 0, 0, 0])), + AtcRational(Uint256([14582282382502025216, 0, 0, 0])), + AtcRational(Uint256([14580277880359520256, 0, 0, 0])), + AtcRational(Uint256([14578250714737874944, 0, 0, 0])), + AtcRational(Uint256([14576200635885367296, 0, 0, 0])), + AtcRational(Uint256([14574127391447336960, 0, 0, 0])), + AtcRational(Uint256([14572030726442487808, 0, 0, 0])), + AtcRational(Uint256([14569910383239120896, 0, 0, 0])), + AtcRational(Uint256([14567766101531174912, 0, 0, 0])), + AtcRational(Uint256([14565597618314184704, 0, 0, 0])), + AtcRational(Uint256([14563404667861078016, 0, 0, 0])), + AtcRational(Uint256([14561186981697867776, 0, 0, 0])), + AtcRational(Uint256([14558944288579205120, 0, 0, 0])), + AtcRational(Uint256([14556676314463823872, 0, 0, 0])), + AtcRational(Uint256([14554382782489843712, 0, 0, 0])), + AtcRational(Uint256([14552063412949977088, 0, 0, 0])), + AtcRational(Uint256([14549717923266603008, 0, 0, 0])), + AtcRational(Uint256([14547346027966732288, 0, 0, 0])), + AtcRational(Uint256([14544947438656860160, 0, 0, 0])), + AtcRational(Uint256([14542521863997716480, 0, 0, 0])), + AtcRational(Uint256([14540069009678876672, 0, 0, 0])), + AtcRational(Uint256([14537588578393323520, 0, 0, 0])), + AtcRational(Uint256([14535080269811841024, 0, 0, 0])), + AtcRational(Uint256([14532543780557377536, 0, 0, 0])), + AtcRational(Uint256([14529978804179232768, 0, 0, 0])), + AtcRational(Uint256([14527385031127242752, 0, 0, 0])), + AtcRational(Uint256([14524762148725782528, 0, 0, 0])), + AtcRational(Uint256([14522109841147760640, 0, 0, 0])), + AtcRational(Uint256([14519427789388460032, 0, 0, 0])), + AtcRational(Uint256([14516715671239366656, 0, 0, 0])), + AtcRational(Uint256([14513973161261858816, 0, 0, 0])), + AtcRational(Uint256([14511199930760869888, 0, 0, 0])), + AtcRational(Uint256([14508395647758436352, 0, 0, 0])), + AtcRational(Uint256([14505559976967245824, 0, 0, 0])), + AtcRational(Uint256([14502692579764047872, 0, 0, 0])), + AtcRational(Uint256([14499793114163054592, 0, 0, 0])), + AtcRational(Uint256([14496861234789287936, 0, 0, 0])), + AtcRational(Uint256([14493896592851855360, 0, 0, 0])), + AtcRational(Uint256([14490898836117196800, 0, 0, 0])), + AtcRational(Uint256([14487867608882292736, 0, 0, 0])), + AtcRational(Uint256([14484802551947833344, 0, 0, 0])), + AtcRational(Uint256([14481703302591363072, 0, 0, 0])), + AtcRational(Uint256([14478569494540392448, 0, 0, 0])), + AtcRational(Uint256([14475400757945503744, 0, 0, 0])), + AtcRational(Uint256([14472196719353440256, 0, 0, 0])), + AtcRational(Uint256([14468957001680179200, 0, 0, 0])), + AtcRational(Uint256([14465681224184016896, 0, 0, 0])), + AtcRational(Uint256([14462369002438653952, 0, 0, 0])), + AtcRational(Uint256([14459019948306282496, 0, 0, 0])), + AtcRational(Uint256([14455633669910710272, 0, 0, 0])), + AtcRational(Uint256([14452209771610484736, 0, 0, 0])), + AtcRational(Uint256([14448747853972076544, 0, 0, 0])), + AtcRational(Uint256([14445247513743073280, 0, 0, 0])), + AtcRational(Uint256([14441708343825438720, 0, 0, 0])), + AtcRational(Uint256([14438129933248808960, 0, 0, 0])), + AtcRational(Uint256([14434511867143868416, 0, 0, 0])), + AtcRational(Uint256([14430853726715774976, 0, 0, 0])), + AtcRational(Uint256([14427155089217667072, 0, 0, 0])), + AtcRational(Uint256([14423415527924258816, 0, 0, 0])), + AtcRational(Uint256([14419634612105521152, 0, 0, 0])), + AtcRational(Uint256([14415811907000477696, 0, 0, 0])), + AtcRational(Uint256([14411946973791092736, 0, 0, 0])), + AtcRational(Uint256([14408039369576282112, 0, 0, 0])), + AtcRational(Uint256([14404088647346073600, 0, 0, 0])), + AtcRational(Uint256([14400094355955869696, 0, 0, 0])), + AtcRational(Uint256([14396056040100884480, 0, 0, 0])), + AtcRational(Uint256([14391973240290742272, 0, 0, 0])), + AtcRational(Uint256([14387845492824211456, 0, 0, 0])), + AtcRational(Uint256([14383672329764151296, 0, 0, 0])), + AtcRational(Uint256([14379453278912624640, 0, 0, 0])), + AtcRational(Uint256([14375187863786246144, 0, 0, 0])), + AtcRational(Uint256([14370875603591677952, 0, 0, 0])), + AtcRational(Uint256([14366516013201418240, 0, 0, 0])), + AtcRational(Uint256([14362108603129778176, 0, 0, 0])), + AtcRational(Uint256([14357652879509125120, 0, 0, 0])), + AtcRational(Uint256([14353148344066387968, 0, 0, 0])), + AtcRational(Uint256([14348594494099806208, 0, 0, 0])), + AtcRational(Uint256([14343990822456012800, 0, 0, 0])), + AtcRational(Uint256([14339336817507360768, 0, 0, 0])), + AtcRational(Uint256([14334631963129606144, 0, 0, 0])), + AtcRational(Uint256([14329875738679891968, 0, 0, 0])), + AtcRational(Uint256([14325067618975068160, 0, 0, 0])), + AtcRational(Uint256([14320207074270386176, 0, 0, 0])), + AtcRational(Uint256([14315293570238543872, 0, 0, 0])), + AtcRational(Uint256([14310326567949113344, 0, 0, 0])), + AtcRational(Uint256([14305305523848388608, 0, 0, 0])), + AtcRational(Uint256([14300229889739610112, 0, 0, 0])), + AtcRational(Uint256([14295099112763666432, 0, 0, 0])), + AtcRational(Uint256([14289912635380201472, 0, 0, 0])), + AtcRational(Uint256([14284669895349196800, 0, 0, 0])), + AtcRational(Uint256([14279370325713045504, 0, 0, 0])), + AtcRational(Uint256([14274013354779123712, 0, 0, 0])), + AtcRational(Uint256([14268598406102849536, 0, 0, 0])), + AtcRational(Uint256([14263124898471307264, 0, 0, 0])), + AtcRational(Uint256([14257592245887395840, 0, 0, 0])), + AtcRational(Uint256([14251999857554575360, 0, 0, 0])), + AtcRational(Uint256([14246347137862176768, 0, 0, 0])), + AtcRational(Uint256([14240633486371330048, 0, 0, 0])), + AtcRational(Uint256([14234858297801515008, 0, 0, 0])), + AtcRational(Uint256([14229020962017785856, 0, 0, 0])), + AtcRational(Uint256([14223120864018599936, 0, 0, 0])), + AtcRational(Uint256([14217157383924420608, 0, 0, 0])), + AtcRational(Uint256([14211129896966959104, 0, 0, 0])), + AtcRational(Uint256([14205037773479176192, 0, 0, 0])), + AtcRational(Uint256([14198880378886055936, 0, 0, 0])), + AtcRational(Uint256([14192657073696112640, 0, 0, 0])), + AtcRational(Uint256([14186367213493727232, 0, 0, 0])), + AtcRational(Uint256([14180010148932296704, 0, 0, 0])), + AtcRational(Uint256([14173585225728227328, 0, 0, 0])), + AtcRational(Uint256([14167091784655794176, 0, 0, 0])), + AtcRational(Uint256([14160529161542889472, 0, 0, 0])), + AtcRational(Uint256([14153896687267710976, 0, 0, 0])), + AtcRational(Uint256([14147193687756355584, 0, 0, 0])), + AtcRational(Uint256([14140419483981410304, 0, 0, 0])), + AtcRational(Uint256([14133573391961522176, 0, 0, 0])), + AtcRational(Uint256([14126654722761990144, 0, 0, 0])), + AtcRational(Uint256([14119662782496409600, 0, 0, 0])), + AtcRational(Uint256([14112596872329363456, 0, 0, 0])), + AtcRational(Uint256([14105456288480262144, 0, 0, 0])), + AtcRational(Uint256([14098240322228244480, 0, 0, 0])), + AtcRational(Uint256([14090948259918305280, 0, 0, 0])), + AtcRational(Uint256([14083579382968543232, 0, 0, 0])), + AtcRational(Uint256([14076132967878658048, 0, 0, 0])), + AtcRational(Uint256([14068608286239690752, 0, 0, 0])), + AtcRational(Uint256([14061004604745011200, 0, 0, 0])), + AtcRational(Uint256([14053321185202620416, 0, 0, 0])), + AtcRational(Uint256([14045557284548792320, 0, 0, 0])), + AtcRational(Uint256([14037712154863056896, 0, 0, 0])), + AtcRational(Uint256([14029785043384606720, 0, 0, 0])), + AtcRational(Uint256([14021775192530079744, 0, 0, 0])), + AtcRational(Uint256([14013681839912861696, 0, 0, 0])), + AtcRational(Uint256([14005504218363817984, 0, 0, 0])), + AtcRational(Uint256([13997241555953580032, 0, 0, 0])), + AtcRational(Uint256([13988893076016375808, 0, 0, 0])), + AtcRational(Uint256([13980457997175449600, 0, 0, 0])), + AtcRational(Uint256([13971935533370089472, 0, 0, 0])), + AtcRational(Uint256([13963324893884334080, 0, 0, 0])), + AtcRational(Uint256([13954625283377340416, 0, 0, 0])), + AtcRational(Uint256([13945835901915490304, 0, 0, 0])), + AtcRational(Uint256([13936955945006243840, 0, 0, 0])), + AtcRational(Uint256([13927984603633807360, 0, 0, 0])), + AtcRational(Uint256([13918921064296585216, 0, 0, 0])), + AtcRational(Uint256([13909764509046546432, 0, 0, 0])), + AtcRational(Uint256([13900514115530459136, 0, 0, 0])), + AtcRational(Uint256([13891169057033058304, 0, 0, 0])), + AtcRational(Uint256([13881728502522195968, 0, 0, 0])), + AtcRational(Uint256([13872191616696016896, 0, 0, 0])), + AtcRational(Uint256([13862557560032120832, 0, 0, 0])), + AtcRational(Uint256([13852825488838891520, 0, 0, 0])), + AtcRational(Uint256([13842994555308853248, 0, 0, 0])), + AtcRational(Uint256([13833063907574269952, 0, 0, 0])), + AtcRational(Uint256([13823032689764870144, 0, 0, 0])), + AtcRational(Uint256([13812900042067845120, 0, 0, 0])), + AtcRational(Uint256([13802665100790099968, 0, 0, 0])), + AtcRational(Uint256([13792326998422816768, 0, 0, 0])), + AtcRational(Uint256([13781884863708366848, 0, 0, 0])), + AtcRational(Uint256([13771337821709592576, 0, 0, 0])), + AtcRational(Uint256([13760684993881540608, 0, 0, 0])), + AtcRational(Uint256([13749925498145615872, 0, 0, 0])), + AtcRational(Uint256([13739058448966279168, 0, 0, 0])), + AtcRational(Uint256([13728082957430233088, 0, 0, 0])), + AtcRational(Uint256([13716998131328233472, 0, 0, 0])), + AtcRational(Uint256([13705803075239473152, 0, 0, 0])), + AtcRational(Uint256([13694496890618648576, 0, 0, 0])), + AtcRational(Uint256([13683078675885682688, 0, 0, 0])), + AtcRational(Uint256([13671547526518214656, 0, 0, 0])), + AtcRational(Uint256([13659902535146829824, 0, 0, 0])), + AtcRational(Uint256([13648142791653093376, 0, 0, 0])), + AtcRational(Uint256([13636267383270436864, 0, 0, 0])), + AtcRational(Uint256([13624275394687913984, 0, 0, 0])), + AtcRational(Uint256([13612165908156874752, 0, 0, 0])), + AtcRational(Uint256([13599938003600584704, 0, 0, 0])), + AtcRational(Uint256([13587590758726844416, 0, 0, 0])), + AtcRational(Uint256([13575123249143625728, 0, 0, 0])), + AtcRational(Uint256([13562534548477763584, 0, 0, 0])), + AtcRational(Uint256([13549823728496742400, 0, 0, 0])), + AtcRational(Uint256([13536989859233630208, 0, 0, 0])), + AtcRational(Uint256([13524032009115150336, 0, 0, 0])), + AtcRational(Uint256([13510949245092962304, 0, 0, 0])), + AtcRational(Uint256([13497740632778186752, 0, 0, 0])), + AtcRational(Uint256([13484405236579164160, 0, 0, 0])), + AtcRational(Uint256([13470942119842529280, 0, 0, 0])), + AtcRational(Uint256([13457350344997619712, 0, 0, 0])), + AtcRational(Uint256([13443628973704206336, 0, 0, 0])), + AtcRational(Uint256([13429777067003654144, 0, 0, 0])), + AtcRational(Uint256([13415793685473462272, 0, 0, 0])), + AtcRational(Uint256([13401677889385263104, 0, 0, 0])), + AtcRational(Uint256([13387428738866302976, 0, 0, 0])), + AtcRational(Uint256([13373045294064392192, 0, 0, 0])), + AtcRational(Uint256([13358526615316400128, 0, 0, 0])), + AtcRational(Uint256([13343871763320287232, 0, 0, 0])), + AtcRational(Uint256([13329079799310704640, 0, 0, 0])), + AtcRational(Uint256([13314149785238190080, 0, 0, 0])), + AtcRational(Uint256([13299080783951974400, 0, 0, 0])), + AtcRational(Uint256([13283871859386413056, 0, 0, 0])), + AtcRational(Uint256([13268522076751075328, 0, 0, 0])), + AtcRational(Uint256([13253030502724497408, 0, 0, 0])), + AtcRational(Uint256([13237396205651617792, 0, 0, 0])), + AtcRational(Uint256([13221618255744899072, 0, 0, 0])), + AtcRational(Uint256([13205695725289166848, 0, 0, 0])), + AtcRational(Uint256([13189627688850184192, 0, 0, 0])), + AtcRational(Uint256([13173413223486908416, 0, 0, 0])), + AtcRational(Uint256([13157051408967542784, 0, 0, 0])), + AtcRational(Uint256([13140541327989270528, 0, 0, 0])), + AtcRational(Uint256([13123882066401785856, 0, 0, 0])), + AtcRational(Uint256([13107072713434537984, 0, 0, 0])), + AtcRational(Uint256([13090112361927747584, 0, 0, 0])), + AtcRational(Uint256([13073000108567144448, 0, 0, 0])), + AtcRational(Uint256([13055735054122481664, 0, 0, 0])), + AtcRational(Uint256([13038316303689742336, 0, 0, 0])), + AtcRational(Uint256([13020742966937124864, 0, 0, 0])), + AtcRational(Uint256([13003014158354718720, 0, 0, 0])), + AtcRational(Uint256([12985128997507874816, 0, 0, 0])), + AtcRational(Uint256([12967086609294301184, 0, 0, 0])), + AtcRational(Uint256([12948886124204806144, 0, 0, 0])), + AtcRational(Uint256([12930526678587715584, 0, 0, 0])), + AtcRational(Uint256([12912007414916904960, 0, 0, 0])), + AtcRational(Uint256([12893327482063446016, 0, 0, 0])), + AtcRational(Uint256([12874486035570843648, 0, 0, 0])), + AtcRational(Uint256([12855482237933809664, 0, 0, 0])), + AtcRational(Uint256([12836315258880561152, 0, 0, 0])), + AtcRational(Uint256([12816984275658594304, 0, 0, 0])), + AtcRational(Uint256([12797488473323913216, 0, 0, 0])), + AtcRational(Uint256([12777827045033641984, 0, 0, 0])), + AtcRational(Uint256([12757999192342022144, 0, 0, 0])), + AtcRational(Uint256([12738004125499680768, 0, 0, 0])), + AtcRational(Uint256([12717841063756201984, 0, 0, 0])), + AtcRational(Uint256([12697509235665854464, 0, 0, 0])), + AtcRational(Uint256([12677007879396530176, 0, 0, 0])), + AtcRational(Uint256([12656336243041691648, 0, 0, 0])), + AtcRational(Uint256([12635493584935419904, 0, 0, 0])), + AtcRational(Uint256([12614479173970364416, 0, 0, 0])), + AtcRational(Uint256([12593292289918617600, 0, 0, 0])), + AtcRational(Uint256([12571932223755370496, 0, 0, 0])), + AtcRational(Uint256([12550398277985329152, 0, 0, 0])), + AtcRational(Uint256([12528689766971766784, 0, 0, 0])), + AtcRational(Uint256([12506806017268160512, 0, 0, 0])), + AtcRational(Uint256([12484746367952306176, 0, 0, 0])), + AtcRational(Uint256([12462510170962810880, 0, 0, 0])), + AtcRational(Uint256([12440096791437899776, 0, 0, 0])), + AtcRational(Uint256([12417505608056395776, 0, 0, 0])), + AtcRational(Uint256([12394736013380814848, 0, 0, 0])), + AtcRational(Uint256([12371787414202433536, 0, 0, 0])), + AtcRational(Uint256([12348659231888226304, 0, 0, 0])), + AtcRational(Uint256([12325350902729566208, 0, 0, 0])), + AtcRational(Uint256([12301861878292580352, 0, 0, 0])), + AtcRational(Uint256([12278191625770014720, 0, 0, 0])), + AtcRational(Uint256([12254339628334479360, 0, 0, 0])), + AtcRational(Uint256([12230305385492973568, 0, 0, 0])), + AtcRational(Uint256([12206088413442545664, 0, 0, 0])), + AtcRational(Uint256([12181688245426927616, 0, 0, 0])), + AtcRational(Uint256([12157104432094023680, 0, 0, 0])), + AtcRational(Uint256([12132336541854107648, 0, 0, 0])), + AtcRational(Uint256([12107384161238581248, 0, 0, 0])), + AtcRational(Uint256([12082246895259109376, 0, 0, 0])), + AtcRational(Uint256([12056924367767033856, 0, 0, 0])), + AtcRational(Uint256([12031416221812840448, 0, 0, 0])), + AtcRational(Uint256([12005722120005560320, 0, 0, 0])), + AtcRational(Uint256([11979841744871907328, 0, 0, 0])), + AtcRational(Uint256([11953774799215020032, 0, 0, 0])), + AtcRational(Uint256([11927521006472566784, 0, 0, 0])), + AtcRational(Uint256([11901080111074107392, 0, 0, 0])), + AtcRational(Uint256([11874451878797459456, 0, 0, 0])), + AtcRational(Uint256([11847636097123960832, 0, 0, 0])), + AtcRational(Uint256([11820632575592335360, 0, 0, 0])), + AtcRational(Uint256([11793441146151079936, 0, 0, 0])), + AtcRational(Uint256([11766061663509092352, 0, 0, 0])), + AtcRational(Uint256([11738494005484369920, 0, 0, 0])), + AtcRational(Uint256([11710738073350592512, 0, 0, 0])), + AtcRational(Uint256([11682793792181340160, 0, 0, 0])), + AtcRational(Uint256([11654661111191783424, 0, 0, 0])), + AtcRational(Uint256([11626340004077604864, 0, 0, 0])), + AtcRational(Uint256([11597830469350934528, 0, 0, 0])), + AtcRational(Uint256([11569132530673096704, 0, 0, 0])), + AtcRational(Uint256([11540246237183952896, 0, 0, 0])), + AtcRational(Uint256([11511171663827582976, 0, 0, 0])), + AtcRational(Uint256([11481908911674114048, 0, 0, 0])), + AtcRational(Uint256([11452458108237473792, 0, 0, 0])), + AtcRational(Uint256([11422819407788793856, 0, 0, 0])), + AtcRational(Uint256([11392992991665272832, 0, 0, 0])), + AtcRational(Uint256([11362979068574269440, 0, 0, 0])), + AtcRational(Uint256([11332777874892353536, 0, 0, 0])), + AtcRational(Uint256([11302389674959124480, 0, 0, 0])), + AtcRational(Uint256([11271814761365499904, 0, 0, 0])), + AtcRational(Uint256([11241053455236325376, 0, 0, 0])), + AtcRational(Uint256([11210106106506956800, 0, 0, 0])), + AtcRational(Uint256([11178973094193678336, 0, 0, 0])), + AtcRational(Uint256([11147654826657650688, 0, 0, 0])), + AtcRational(Uint256([11116151741862152192, 0, 0, 0])), + AtcRational(Uint256([11084464307622914048, 0, 0, 0])), + AtcRational(Uint256([11052593021851269120, 0, 0, 0])), + AtcRational(Uint256([11020538412789880832, 0, 0, 0])), + AtcRational(Uint256([10988301039240828928, 0, 0, 0])), + AtcRational(Uint256([10955881490785785856, 0, 0, 0])), + AtcRational(Uint256([10923280387998085120, 0, 0, 0])), + AtcRational(Uint256([10890498382646384640, 0, 0, 0])), + AtcRational(Uint256([10857536157889769472, 0, 0, 0])), + AtcRational(Uint256([10824394428463968256, 0, 0, 0])), + AtcRational(Uint256([10791073940858529792, 0, 0, 0])), + AtcRational(Uint256([10757575473484689408, 0, 0, 0])), + AtcRational(Uint256([10723899836833691648, 0, 0, 0])), + AtcRational(Uint256([10690047873625384960, 0, 0, 0])), + AtcRational(Uint256([10656020458946807808, 0, 0, 0])), + AtcRational(Uint256([10621818500380600320, 0, 0, 0])), + AtcRational(Uint256([10587442938122995712, 0, 0, 0])), + AtcRational(Uint256([10552894745091184640, 0, 0, 0])), + AtcRational(Uint256([10518174927019845632, 0, 0, 0])), + AtcRational(Uint256([10483284522546655232, 0, 0, 0])), + AtcRational(Uint256([10448224603286523904, 0, 0, 0])), + AtcRational(Uint256([10412996273894438912, 0, 0, 0])), + AtcRational(Uint256([10377600672116664320, 0, 0, 0])), + AtcRational(Uint256([10342038968830132224, 0, 0, 0])), + AtcRational(Uint256([10306312368069857280, 0, 0, 0])), + AtcRational(Uint256([10270422107044188160, 0, 0, 0])), + AtcRational(Uint256([10234369456137705472, 0, 0, 0])), + AtcRational(Uint256([10198155718901680128, 0, 0, 0])), + AtcRational(Uint256([10161782232031832064, 0, 0, 0])), + AtcRational(Uint256([10125250365333327872, 0, 0, 0])), + AtcRational(Uint256([10088561521672830976, 0, 0, 0])), + AtcRational(Uint256([10051717136917477376, 0, 0, 0])), + AtcRational(Uint256([10014718679860666368, 0, 0, 0])), + AtcRational(Uint256([9977567652134516736, 0, 0, 0])), + AtcRational(Uint256([9940265588108912640, 0, 0, 0])), + AtcRational(Uint256([9902814054777008128, 0, 0, 0])), + AtcRational(Uint256([9865214651627091968, 0, 0, 0])), + AtcRational(Uint256([9827469010500773888, 0, 0, 0])), + AtcRational(Uint256([9789578795437342720, 0, 0, 0])), + AtcRational(Uint256([9751545702504284160, 0, 0, 0])), + AtcRational(Uint256([9713371459613874176, 0, 0, 0])), + AtcRational(Uint256([9675057826325798912, 0, 0, 0])), + AtcRational(Uint256([9636606593635780608, 0, 0, 0])), + AtcRational(Uint256([9598019583750131712, 0, 0, 0])), + AtcRational(Uint256([9559298649846272000, 0, 0, 0])), + AtcRational(Uint256([9520445675819153408, 0, 0, 0])), + AtcRational(Uint256([9481462576013621248, 0, 0, 0])), + AtcRational(Uint256([9442351294942703616, 0, 0, 0])), + AtcRational(Uint256([9403113806991841280, 0, 0, 0])), + AtcRational(Uint256([9363752116109119488, 0, 0, 0])), + AtcRational(Uint256([9324268255481511936, 0, 0, 0])), + AtcRational(Uint256([9284664287197179904, 0, 0, 0])), + AtcRational(Uint256([9244942301893949440, 0, 0, 0])), + AtcRational(Uint256([9205104418393949184, 0, 0, 0])), + AtcRational(Uint256([9165152783324563456, 0, 0, 0])), + AtcRational(Uint256([9125089570725771264, 0, 0, 0])), + AtcRational(Uint256([9084916981643961344, 0, 0, 0])), + AtcRational(Uint256([9044637243712360448, 0, 0, 0])), + AtcRational(Uint256([9004252610718200832, 0, 0, 0])), + AtcRational(Uint256([8963765362156744704, 0, 0, 0])), + AtcRational(Uint256([8923177802772338688, 0, 0, 0])), + AtcRational(Uint256([8882492262086646784, 0, 0, 0])), + AtcRational(Uint256([8841711093914219520, 0, 0, 0])), + AtcRational(Uint256([8800836675865615360, 0, 0, 0])), + AtcRational(Uint256([8759871408838231040, 0, 0, 0])), + AtcRational(Uint256([8718817716495054848, 0, 0, 0])), + AtcRational(Uint256([8677678044731567104, 0, 0, 0])), + AtcRational(Uint256([8636454861130998784, 0, 0, 0])), + AtcRational(Uint256([8595150654408180736, 0, 0, 0])), + AtcRational(Uint256([8553767933842236416, 0, 0, 0])), + AtcRational(Uint256([8512309228698363904, 0, 0, 0])), + AtcRational(Uint256([8470777087638975488, 0, 0, 0])), + AtcRational(Uint256([8429174078124461056, 0, 0, 0])), + AtcRational(Uint256([8387502785803874304, 0, 0, 0])), + AtcRational(Uint256([8345765813895795712, 0, 0, 0])), + AtcRational(Uint256([8303965782559726592, 0, 0, 0])), + AtcRational(Uint256([8262105328258275328, 0, 0, 0])), + AtcRational(Uint256([8220187103110477824, 0, 0, 0])), + AtcRational(Uint256([8178213774236573696, 0, 0, 0])), + AtcRational(Uint256([8136188023094564864, 0, 0, 0])), + AtcRational(Uint256([8094112544808916992, 0, 0, 0])), + AtcRational(Uint256([8051990047491715072, 0, 0, 0])), + AtcRational(Uint256([8009823251556677632, 0, 0, 0])), + AtcRational(Uint256([7967614889026356224, 0, 0, 0])), + AtcRational(Uint256([7925367702832887808, 0, 0, 0])), + AtcRational(Uint256([7883084446112715776, 0, 0, 0])), + AtcRational(Uint256([7840767881495595008, 0, 0, 0])), + AtcRational(Uint256([7798420780388343808, 0, 0, 0])), + AtcRational(Uint256([7756045922253651968, 0, 0, 0])), + AtcRational(Uint256([7713646093884422144, 0, 0, 0])), + AtcRational(Uint256([7671224088673970176, 0, 0, 0])), + AtcRational(Uint256([7628782705882552320, 0, 0, 0])), + AtcRational(Uint256([7586324749900575744, 0, 0, 0])), + AtcRational(Uint256([7543853029508941824, 0, 0, 0])), + AtcRational(Uint256([7501370357136906240, 0, 0, 0])), + AtcRational(Uint256([7458879548117898240, 0, 0, 0])), + AtcRational(Uint256([7416383419943693312, 0, 0, 0])), + AtcRational(Uint256([7373884791517374464, 0, 0, 0])), + AtcRational(Uint256([7331386482405493760, 0, 0, 0])), + AtcRational(Uint256([7288891312089871360, 0, 0, 0])), + AtcRational(Uint256([7246402099219427328, 0, 0, 0])), + AtcRational(Uint256([7203921660862483456, 0, 0, 0])), + AtcRational(Uint256([7161452811759982592, 0, 0, 0])), + AtcRational(Uint256([7118998363579975680, 0, 0, 0])), + AtcRational(Uint256([7076561124173879296, 0, 0, 0])), + AtcRational(Uint256([7034143896834856960, 0, 0, 0])), + AtcRational(Uint256([6991749479558778880, 0, 0, 0])), + AtcRational(Uint256([6949380664308144128, 0, 0, 0])), + AtcRational(Uint256([6907040236279402496, 0, 0, 0])), + AtcRational(Uint256([6864730973174070272, 0, 0, 0])), + AtcRational(Uint256([6822455644474029056, 0, 0, 0])), + AtcRational(Uint256([6780217010721434624, 0, 0, 0])), + AtcRational(Uint256([6738017822803616768, 0, 0, 0])), + AtcRational(Uint256([6695860821243351040, 0, 0, 0])), + AtcRational(Uint256([6653748735494901760, 0, 0, 0])), + AtcRational(Uint256([6611684283246219264, 0, 0, 0])), + AtcRational(Uint256([6569670169727631360, 0, 0, 0])), + AtcRational(Uint256([6527709087027459072, 0, 0, 0])), + AtcRational(Uint256([6485803713414843392, 0, 0, 0])), + AtcRational(Uint256([6443956712670195712, 0, 0, 0])), + AtcRational(Uint256([6402170733423590400, 0, 0, 0])), + AtcRational(Uint256([6360448408501444608, 0, 0, 0])), + AtcRational(Uint256([6318792354281820160, 0, 0, 0])), + AtcRational(Uint256([6277205170058672128, 0, 0, 0])), + AtcRational(Uint256([6235689437415347200, 0, 0, 0])), + AtcRational(Uint256([6194247719607663616, 0, 0, 0])), + AtcRational(Uint256([6152882560956841984, 0, 0, 0])), + AtcRational(Uint256([6111596486252597248, 0, 0, 0])), + AtcRational(Uint256([6070392000166668288, 0, 0, 0])), + AtcRational(Uint256([6029271586677042176, 0, 0, 0])), + AtcRational(Uint256([5988237708503158784, 0, 0, 0])), + AtcRational(Uint256([5947292806552320000, 0, 0, 0])), + AtcRational(Uint256([5906439299377565696, 0, 0, 0])), + AtcRational(Uint256([5865679582647235584, 0, 0, 0])), + AtcRational(Uint256([5825016028626446336, 0, 0, 0])), + AtcRational(Uint256([5784450985670685696, 0, 0, 0])), + AtcRational(Uint256([5743986777731734528, 0, 0, 0])), + AtcRational(Uint256([5703625703876088832, 0, 0, 0])), + AtcRational(Uint256([5663370037816086528, 0, 0, 0])), + AtcRational(Uint256([5623222027453882368, 0, 0, 0])), + AtcRational(Uint256([5583183894438436864, 0, 0, 0])), + AtcRational(Uint256([5543257833735676928, 0, 0, 0])), + AtcRational(Uint256([5503446013211941888, 0, 0, 0])), + AtcRational(Uint256([5463750573230858240, 0, 0, 0])), + AtcRational(Uint256([5424173626263745536, 0, 0, 0])), + AtcRational(Uint256([5384717256513666048, 0, 0, 0])), + AtcRational(Uint256([5345383519553192960, 0, 0, 0])), + AtcRational(Uint256([5306174441976003584, 0, 0, 0])), + AtcRational(Uint256([5267092021062338560, 0, 0, 0])), + AtcRational(Uint256([5228138224458407936, 0, 0, 0])), + AtcRational(Uint256([5189314989869789184, 0, 0, 0])), + AtcRational(Uint256([5150624224768840704, 0, 0, 0])), + AtcRational(Uint256([5112067806116179968, 0, 0, 0])), + AtcRational(Uint256([5073647580096222208, 0, 0, 0])), + AtcRational(Uint256([5035365361866804224, 0, 0, 0])), + AtcRational(Uint256([4997222935322875904, 0, 0, 0])), + AtcRational(Uint256([4959222052874251264, 0, 0, 0])), + AtcRational(Uint256([4921364435237403648, 0, 0, 0])), + AtcRational(Uint256([4883651771241251840, 0, 0, 0])), + AtcRational(Uint256([4846085717646911488, 0, 0, 0])), + AtcRational(Uint256([4808667898981359616, 0, 0, 0])), + AtcRational(Uint256([4771399907384928256, 0, 0, 0])), + AtcRational(Uint256([4734283302472590336, 0, 0, 0])), + AtcRational(Uint256([4697319611208928256, 0, 0, 0])), + AtcRational(Uint256([4660510327796715520, 0, 0, 0])), + AtcRational(Uint256([4623856913578997760, 0, 0, 0])), + AtcRational(Uint256([4587360796954596352, 0, 0, 0])), + AtcRational(Uint256([4551023373306879488, 0, 0, 0])), + AtcRational(Uint256([4514846004945721344, 0, 0, 0])), + AtcRational(Uint256([4478830021062493696, 0, 0, 0])), + AtcRational(Uint256([4442976717697962496, 0, 0, 0])), + AtcRational(Uint256([4407287357722949632, 0, 0, 0])), + AtcRational(Uint256([4371763170831599616, 0, 0, 0])), + AtcRational(Uint256([4336405353547112960, 0, 0, 0])), + AtcRational(Uint256([4301215069239754752, 0, 0, 0])), + AtcRational(Uint256([4266193448156999680, 0, 0, 0])), + AtcRational(Uint256([4231341587465614848, 0, 0, 0])), + AtcRational(Uint256([4196660551305514496, 0, 0, 0])), + AtcRational(Uint256([4162151370855192064, 0, 0, 0])), + AtcRational(Uint256([4127815044408539136, 0, 0, 0])), + AtcRational(Uint256([4093652537462862336, 0, 0, 0])), + AtcRational(Uint256([4059664782817884160, 0, 0, 0])), + AtcRational(Uint256([4025852680685536768, 0, 0, 0])), + AtcRational(Uint256([3992217098810330624, 0, 0, 0])), + AtcRational(Uint256([3958758872600086528, 0, 0, 0])), + AtcRational(Uint256([3925478805266815488, 0, 0, 0])), + AtcRational(Uint256([3892377667977526784, 0, 0, 0])), + AtcRational(Uint256([3859456200014740992, 0, 0, 0])), + AtcRational(Uint256([3826715108946479616, 0, 0, 0])), + AtcRational(Uint256([3794155070805506048, 0, 0, 0])), + AtcRational(Uint256([3761776730277590016, 0, 0, 0])), + AtcRational(Uint256([3729580700898548736, 0, 0, 0])), + AtcRational(Uint256([3697567565259854336, 0, 0, 0])), + AtcRational(Uint256([3665737875222543872, 0, 0, 0])), + AtcRational(Uint256([3634092152139219456, 0, 0, 0])), + AtcRational(Uint256([3602630887083875840, 0, 0, 0])), + AtcRational(Uint256([3571354541089344000, 0, 0, 0])), + AtcRational(Uint256([3540263545392078336, 0, 0, 0])), + AtcRational(Uint256([3509358301684075008, 0, 0, 0])), + AtcRational(Uint256([3478639182371662336, 0, 0, 0])), + AtcRational(Uint256([3448106530840935936, 0, 0, 0])), + AtcRational(Uint256([3417760661729580032, 0, 0, 0])), + AtcRational(Uint256([3387601861204853760, 0, 0, 0])), + AtcRational(Uint256([3357630387247493120, 0, 0, 0])), + AtcRational(Uint256([3327846469941282304, 0, 0, 0])), + AtcRational(Uint256([3298250311768075776, 0, 0, 0])), + AtcRational(Uint256([3268842087908014080, 0, 0, 0])), + AtcRational(Uint256([3239621946544709632, 0, 0, 0])), + AtcRational(Uint256([3210590009175161344, 0, 0, 0])), + AtcRational(Uint256([3181746370924176384, 0, 0, 0])), + AtcRational(Uint256([3153091100863047680, 0, 0, 0])), + AtcRational(Uint256([3124624242332286464, 0, 0, 0])), + AtcRational(Uint256([3096345813268148736, 0, 0, 0])), + AtcRational(Uint256([3068255806532773376, 0, 0, 0])), + AtcRational(Uint256([3040354190247658496, 0, 0, 0])), + AtcRational(Uint256([3012640908130307584, 0, 0, 0])), + AtcRational(Uint256([2985115879833786880, 0, 0, 0])), + AtcRational(Uint256([2957779001289008640, 0, 0, 0])), + AtcRational(Uint256([2930630145049504256, 0, 0, 0])), + AtcRational(Uint256([2903669160638502400, 0, 0, 0])), + AtcRational(Uint256([2876895874898083840, 0, 0, 0])), + AtcRational(Uint256([2850310092340229632, 0, 0, 0])), + AtcRational(Uint256([2823911595499543552, 0, 0, 0])), + AtcRational(Uint256([2797700145287476736, 0, 0, 0])), + AtcRational(Uint256([2771675481347832320, 0, 0, 0])), + AtcRational(Uint256([2745837322413390848, 0, 0, 0])), + AtcRational(Uint256([2720185366663441408, 0, 0, 0])), + AtcRational(Uint256([2694719292082065408, 0, 0, 0])), + AtcRational(Uint256([2669438756816964096, 0, 0, 0])), + AtcRational(Uint256([2644343399538680832, 0, 0, 0])), + AtcRational(Uint256([2619432839800029696, 0, 0, 0])), + AtcRational(Uint256([2594706678395571200, 0, 0, 0])), + AtcRational(Uint256([2570164497720961536, 0, 0, 0])), + AtcRational(Uint256([2545805862132034048, 0, 0, 0])), + AtcRational(Uint256([2521630318303431168, 0, 0, 0])), + AtcRational(Uint256([2497637395586657792, 0, 0, 0])), + AtcRational(Uint256([2473826606367389696, 0, 0, 0])), + AtcRational(Uint256([2450197446421903360, 0, 0, 0])), + AtcRational(Uint256([2426749395272486912, 0, 0, 0])), + AtcRational(Uint256([2403481916541677568, 0, 0, 0])), + AtcRational(Uint256([2380394458305224704, 0, 0, 0])), + AtcRational(Uint256([2357486453443613696, 0, 0, 0])), + AtcRational(Uint256([2334757319992054272, 0, 0, 0])), + AtcRational(Uint256([2312206461488791552, 0, 0, 0])), + AtcRational(Uint256([2289833267321639936, 0, 0, 0])), + AtcRational(Uint256([2267637113072605440, 0, 0, 0])), + AtcRational(Uint256([2245617360860510720, 0, 0, 0])), + AtcRational(Uint256([2223773359681494528, 0, 0, 0])), + AtcRational(Uint256([2202104445747299072, 0, 0, 0])), + AtcRational(Uint256([2180609942821237760, 0, 0, 0])), + AtcRational(Uint256([2159289162551755520, 0, 0, 0])), + AtcRational(Uint256([2138141404803482112, 0, 0, 0])), + AtcRational(Uint256([2117165957985701120, 0, 0, 0])), + AtcRational(Uint256([2096362099378140928, 0, 0, 0])), + AtcRational(Uint256([2075729095454018048, 0, 0, 0])), + AtcRational(Uint256([2055266202200243968, 0, 0, 0])), + AtcRational(Uint256([2034972665434736128, 0, 0, 0])), + AtcRational(Uint256([2014847721120749056, 0, 0, 0])), + AtcRational(Uint256([1994890595678173952, 0, 0, 0])), + AtcRational(Uint256([1975100506291729152, 0, 0, 0])), + AtcRational(Uint256([1955476661215996672, 0, 0, 0])), + AtcRational(Uint256([1936018260077233664, 0, 0, 0])), + AtcRational(Uint256([1916724494171921152, 0, 0, 0])), + AtcRational(Uint256([1897594546761984256, 0, 0, 0])), + AtcRational(Uint256([1878627593366651904, 0, 0, 0])), + AtcRational(Uint256([1859822802050898432, 0, 0, 0])), + AtcRational(Uint256([1841179333710439168, 0, 0, 0])), + AtcRational(Uint256([1822696342353232640, 0, 0, 0])), + AtcRational(Uint256([1804372975377456640, 0, 0, 0])), + AtcRational(Uint256([1786208373845930240, 0, 0, 0])), + AtcRational(Uint256([1768201672756947200, 0, 0, 0])), + AtcRational(Uint256([1750352001311492352, 0, 0, 0])), + AtcRational(Uint256([1732658483176829696, 0, 0, 0])), + AtcRational(Uint256([1715120236746417152, 0, 0, 0])), + AtcRational(Uint256([1697736375396155136, 0, 0, 0])), + AtcRational(Uint256([1680506007736934400, 0, 0, 0])), + AtcRational(Uint256([1663428237863474432, 0, 0, 0])), + AtcRational(Uint256([1646502165599439872, 0, 0, 0])), + AtcRational(Uint256([1629726886738828032, 0, 0, 0])), + AtcRational(Uint256([1613101493283616512, 0, 0, 0])), + AtcRational(Uint256([1596625073677668096, 0, 0, 0])), + AtcRational(Uint256([1580296713036883968, 0, 0, 0])), + AtcRational(Uint256([1564115493375614976, 0, 0, 0])), + AtcRational(Uint256([1548080493829320192, 0, 0, 0])), + AtcRational(Uint256([1532190790873484288, 0, 0, 0])), + AtcRational(Uint256([1516445458538788608, 0, 0, 0])), + AtcRational(Uint256([1500843568622554368, 0, 0, 0])), + AtcRational(Uint256([1485384190896454656, 0, 0, 0])), + AtcRational(Uint256([1470066393310513152, 0, 0, 0])), + AtcRational(Uint256([1454889242193393664, 0, 0, 0])), + AtcRational(Uint256([1439851802449000192, 0, 0, 0])), + AtcRational(Uint256([1424953137749395968, 0, 0, 0])), + AtcRational(Uint256([1410192310724064000, 0, 0, 0])), + AtcRational(Uint256([1395568383145516288, 0, 0, 0])), + AtcRational(Uint256([1381080416111280128, 0, 0, 0])), + AtcRational(Uint256([1366727470222276864, 0, 0, 0])), + AtcRational(Uint256([1352508605757614592, 0, 0, 0])), + AtcRational(Uint256([1338422882845812992, 0, 0, 0])), + AtcRational(Uint256([1324469361632493312, 0, 0, 0])), + AtcRational(Uint256([1310647102444549376, 0, 0, 0])), + AtcRational(Uint256([1296955165950824704, 0, 0, 0])), + AtcRational(Uint256([1283392613319330816, 0, 0, 0])), + AtcRational(Uint256([1269958506371023872, 0, 0, 0])), + AtcRational(Uint256([1256651907730177536, 0, 0, 0])), + AtcRational(Uint256([1243471880971367936, 0, 0, 0])), + AtcRational(Uint256([1230417490763117312, 0, 0, 0])), + AtcRational(Uint256([1217487803008212736, 0, 0, 0])), + AtcRational(Uint256([1204681884980741632, 0, 0, 0])), + AtcRational(Uint256([1191998805459865088, 0, 0, 0])), + AtcRational(Uint256([1179437634860375808, 0, 0, 0])), + AtcRational(Uint256([1166997445360058880, 0, 0, 0])), + AtcRational(Uint256([1154677311023903744, 0, 0, 0])), + AtcRational(Uint256([1142476307925186944, 0, 0, 0])), + AtcRational(Uint256([1130393514263474816, 0, 0, 0])), + AtcRational(Uint256([1118428010479570176, 0, 0, 0])), + AtcRational(Uint256([1106578879367446784, 0, 0, 0])), + AtcRational(Uint256([1094845206183200000, 0, 0, 0])), + AtcRational(Uint256([1083226078751057536, 0, 0, 0])), + AtcRational(Uint256([1071720587566481536, 0, 0, 0])), + AtcRational(Uint256([1060327825896404608, 0, 0, 0])), + AtcRational(Uint256([1049046889876627200, 0, 0, 0])), + AtcRational(Uint256([1037876878606426112, 0, 0, 0])), + AtcRational(Uint256([1026816894240403456, 0, 0, 0])), + AtcRational(Uint256([1015866042077617536, 0, 0, 0])), + AtcRational(Uint256([1005023430648028800, 0, 0, 0])), + AtcRational(Uint256([994288171796307968, 0, 0, 0])), + AtcRational(Uint256([983659380763034624, 0, 0, 0])), + AtcRational(Uint256([973136176263332992, 0, 0, 0])), + AtcRational(Uint256([962717680562974336, 0, 0, 0])), + AtcRational(Uint256([952403019551993984, 0, 0, 0])), + AtcRational(Uint256([942191322815853056, 0, 0, 0])), + AtcRational(Uint256([932081723704189696, 0, 0, 0])), + AtcRational(Uint256([922073359397190528, 0, 0, 0])), + AtcRational(Uint256([912165370969629056, 0, 0, 0])), + AtcRational(Uint256([902356903452603136, 0, 0, 0])), + AtcRational(Uint256([892647105893010176, 0, 0, 0])), + AtcRational(Uint256([883035131410800384, 0, 0, 0])), + AtcRational(Uint256([873520137254044800, 0, 0, 0])), + AtcRational(Uint256([864101284851852928, 0, 0, 0])), + AtcRational(Uint256([854777739865181312, 0, 0, 0])), + AtcRational(Uint256([845548672235568384, 0, 0, 0])), + AtcRational(Uint256([836413256231831552, 0, 0, 0])), + AtcRational(Uint256([827370670494766720, 0, 0, 0])), + AtcRational(Uint256([818420098079881728, 0, 0, 0])), + AtcRational(Uint256([809560726498204800, 0, 0, 0])), + AtcRational(Uint256([800791747755200896, 0, 0, 0])), + AtcRational(Uint256([792112358387835392, 0, 0, 0])), + AtcRational(Uint256([783521759499814016, 0, 0, 0])), + AtcRational(Uint256([775019156795042816, 0, 0, 0])), + AtcRational(Uint256([766603760609335424, 0, 0, 0])), + AtcRational(Uint256([758274785940408960, 0, 0, 0])), + AtcRational(Uint256([750031452476196608, 0, 0, 0])), + AtcRational(Uint256([741872984621515008, 0, 0, 0])), + AtcRational(Uint256([733798611523120256, 0, 0, 0])), + AtcRational(Uint256([725807567093184512, 0, 0, 0])), + AtcRational(Uint256([717899090031224448, 0, 0, 0])), + AtcRational(Uint256([710072423844518784, 0, 0, 0])), + AtcRational(Uint256([702326816867043968, 0, 0, 0])), + AtcRational(Uint256([694661522276962432, 0, 0, 0])), + AtcRational(Uint256([687075798112689920, 0, 0, 0])), + AtcRational(Uint256([679568907287580672, 0, 0, 0])), + AtcRational(Uint256([672140117603256192, 0, 0, 0])), + AtcRational(Uint256([664788701761609984, 0, 0, 0])), + AtcRational(Uint256([657513937375516800, 0, 0, 0])), + AtcRational(Uint256([650315106978278272, 0, 0, 0])), + AtcRational(Uint256([643191498031836288, 0, 0, 0])), + AtcRational(Uint256([636142402933774464, 0, 0, 0])), + AtcRational(Uint256([629167119023148800, 0, 0, 0])), + AtcRational(Uint256([622264948585165440, 0, 0, 0])), + AtcRational(Uint256([615435198854739840, 0, 0, 0])), + AtcRational(Uint256([608677182018960512, 0, 0, 0])), + AtcRational(Uint256([601990215218487424, 0, 0, 0])), + AtcRational(Uint256([595373620547912192, 0, 0, 0])), + AtcRational(Uint256([588826725055103488, 0, 0, 0])), + AtcRational(Uint256([582348860739565568, 0, 0, 0])), + AtcRational(Uint256([575939364549835840, 0, 0, 0])), + AtcRational(Uint256([569597578379946176, 0, 0, 0])), + AtcRational(Uint256([563322849064973184, 0, 0, 0])), + AtcRational(Uint256([557114528375699392, 0, 0, 0])), + AtcRational(Uint256([550971973012414144, 0, 0, 0])), + AtcRational(Uint256([544894544597873792, 0, 0, 0])), + AtcRational(Uint256([538881609669446912, 0, 0, 0])), + AtcRational(Uint256([532932539670464960, 0, 0, 0])), + AtcRational(Uint256([527046710940803776, 0, 0, 0])), + AtcRational(Uint256([521223504706716480, 0, 0, 0])), + AtcRational(Uint256([515462307069940352, 0, 0, 0])), + AtcRational(Uint256([509762508996097024, 0, 0, 0])), + AtcRational(Uint256([504123506302410304, 0, 0, 0])), + AtcRational(Uint256([498544699644759936, 0, 0, 0])), + AtcRational(Uint256([493025494504093248, 0, 0, 0])), + AtcRational(Uint256([487565301172211520, 0, 0, 0])), + AtcRational(Uint256([482163534736955520, 0, 0, 0])), + AtcRational(Uint256([476819615066805056, 0, 0, 0])), + AtcRational(Uint256([471532966794915008, 0, 0, 0])), + AtcRational(Uint256([466303019302601600, 0, 0, 0])), + AtcRational(Uint256([461129206702303360, 0, 0, 0])), + AtcRational(Uint256([456010967820029760, 0, 0, 0])), + AtcRational(Uint256([450947746177316224, 0, 0, 0])), + AtcRational(Uint256([445938989972704576, 0, 0, 0])), + AtcRational(Uint256([440984152062762688, 0, 0, 0])), + AtcRational(Uint256([436082689942662912, 0, 0, 0])), + AtcRational(Uint256([431234065726332992, 0, 0, 0])), + AtcRational(Uint256([426437746126196672, 0, 0, 0])), + AtcRational(Uint256([421693202432519040, 0, 0, 0])), + AtcRational(Uint256([416999910492373440, 0, 0, 0])), + AtcRational(Uint256([412357350688240704, 0, 0, 0])), + AtcRational(Uint256([407765007916260352, 0, 0, 0])), + AtcRational(Uint256([403222371564144896, 0, 0, 0])), + AtcRational(Uint256([398728935488772800, 0, 0, 0])), + AtcRational(Uint256([394284197993471488, 0, 0, 0])), + AtcRational(Uint256([389887661805007040, 0, 0, 0])), + AtcRational(Uint256([385538834050291776, 0, 0, 0])), + AtcRational(Uint256([381237226232822592, 0, 0, 0])), + AtcRational(Uint256([376982354208862784, 0, 0, 0])), + AtcRational(Uint256([372773738163379840, 0, 0, 0])), + AtcRational(Uint256([368610902585751744, 0, 0, 0])), + AtcRational(Uint256([364493376245252288, 0, 0, 0])), + AtcRational(Uint256([360420692166327168, 0, 0, 0])), + AtcRational(Uint256([356392387603673216, 0, 0, 0])), + AtcRational(Uint256([352408004017130240, 0, 0, 0])), + AtcRational(Uint256([348467087046397696, 0, 0, 0])), + AtcRational(Uint256([344569186485583936, 0, 0, 0])), + AtcRational(Uint256([340713856257602176, 0, 0, 0])), + AtcRational(Uint256([336900654388419392, 0, 0, 0])), + AtcRational(Uint256([333129142981170944, 0, 0, 0])), + AtcRational(Uint256([329398888190146944, 0, 0, 0])), + AtcRational(Uint256([325709460194663424, 0, 0, 0])), + AtcRational(Uint256([322060433172825088, 0, 0, 0])), + AtcRational(Uint256([318451385275187776, 0, 0, 0])), + AtcRational(Uint256([314881898598331776, 0, 0, 0])), + AtcRational(Uint256([311351559158351808, 0, 0, 0])), + AtcRational(Uint256([307859956864274048, 0, 0, 0])), + AtcRational(Uint256([304406685491404992, 0, 0, 0])), + AtcRational(Uint256([300991342654624192, 0, 0, 0])), + AtcRational(Uint256([297613529781624320, 0, 0, 0])), + AtcRational(Uint256([294272852086108864, 0, 0, 0])), + AtcRational(Uint256([290968918540952256, 0, 0, 0])), + AtcRational(Uint256([287701341851331328, 0, 0, 0])), + AtcRational(Uint256([284469738427833696, 0, 0, 0])), + AtcRational(Uint256([281273728359550304, 0, 0, 0])), + AtcRational(Uint256([278112935387157216, 0, 0, 0])), + AtcRational(Uint256([274986986875995200, 0, 0, 0])), + AtcRational(Uint256([271895513789150592, 0, 0, 0])), + AtcRational(Uint256([268838150660545664, 0, 0, 0])), + AtcRational(Uint256([265814535568041440, 0, 0, 0])), + AtcRational(Uint256([262824310106561728, 0, 0, 0])), + AtcRational(Uint256([259867119361241024, 0, 0, 0])), + AtcRational(Uint256([256942611880603296, 0, 0, 0])), + AtcRational(Uint256([254050439649774752, 0, 0, 0])), + AtcRational(Uint256([251190258063738688, 0, 0, 0])), + AtcRational(Uint256([248361725900633600, 0, 0, 0])), + AtcRational(Uint256([245564505295100640, 0, 0, 0])), + AtcRational(Uint256([242798261711686880, 0, 0, 0])), + AtcRational(Uint256([240062663918305152, 0, 0, 0])), + AtcRational(Uint256([237357383959756160, 0, 0, 0])), + AtcRational(Uint256([234682097131319296, 0, 0, 0])), + AtcRational(Uint256([232036481952410816, 0, 0, 0])), + AtcRational(Uint256([229420220140319360, 0, 0, 0])), + AtcRational(Uint256([226832996584017152, 0, 0, 0])), + AtcRational(Uint256([224274499318053024, 0, 0, 0])), + AtcRational(Uint256([221744419496531680, 0, 0, 0])), + AtcRational(Uint256([219242451367179744, 0, 0, 0])), + AtcRational(Uint256([216768292245502976, 0, 0, 0])), + AtcRational(Uint256([214321642489039520, 0, 0, 0])), + AtcRational(Uint256([211902205471709248, 0, 0, 0])), + AtcRational(Uint256([209509687558263072, 0, 0, 0])), + AtcRational(Uint256([207143798078836928, 0, 0, 0])), + AtcRational(Uint256([204804249303609280, 0, 0, 0])), + AtcRational(Uint256([202490756417568736, 0, 0, 0])), + AtcRational(Uint256([200203037495391232, 0, 0, 0])), + AtcRational(Uint256([197940813476429664, 0, 0, 0])), + AtcRational(Uint256([195703808139820608, 0, 0, 0])), + AtcRational(Uint256([193491748079706688, 0, 0, 0])), + AtcRational(Uint256([191304362680578688, 0, 0, 0])), + AtcRational(Uint256([189141384092740352, 0, 0, 0])), + AtcRational(Uint256([187002547207894304, 0, 0, 0])), + AtcRational(Uint256([184887589634855776, 0, 0, 0])), + AtcRational(Uint256([182796251675390752, 0, 0, 0])), + AtcRational(Uint256([180728276300183808, 0, 0, 0])), + AtcRational(Uint256([178683409124936320, 0, 0, 0])), + AtcRational(Uint256([176661398386595648, 0, 0, 0])), + AtcRational(Uint256([174661994919716768, 0, 0, 0])), + AtcRational(Uint256([172684952132960128, 0, 0, 0])), + AtcRational(Uint256([170730025985722752, 0, 0, 0])), + AtcRational(Uint256([168796974964908128, 0, 0, 0])), + AtcRational(Uint256([166885560061832896, 0, 0, 0])), + AtcRational(Uint256([164995544749272480, 0, 0, 0])), + AtcRational(Uint256([163126694958648032, 0, 0, 0])), + AtcRational(Uint256([161278779057352736, 0, 0, 0])), + AtcRational(Uint256([159451567826220640, 0, 0, 0])), + AtcRational(Uint256([157644834437138816, 0, 0, 0])), + AtcRational(Uint256([155858354430802016, 0, 0, 0])), + AtcRational(Uint256([154091905694611360, 0, 0, 0])), + AtcRational(Uint256([152345268440719328, 0, 0, 0])), + AtcRational(Uint256([150618225184218048, 0, 0, 0])), + AtcRational(Uint256([148910560721475488, 0, 0, 0])), + AtcRational(Uint256([147222062108617056, 0, 0, 0])), + AtcRational(Uint256([145552518640153856, 0, 0, 0])), + AtcRational(Uint256([143901721827759536, 0, 0, 0])), + AtcRational(Uint256([142269465379193696, 0, 0, 0])), + AtcRational(Uint256([140655545177373184, 0, 0, 0])), + AtcRational(Uint256([139059759259593184, 0, 0, 0])), + AtcRational(Uint256([137481907796894496, 0, 0, 0])), + AtcRational(Uint256([135921793073581792, 0, 0, 0])), + AtcRational(Uint256([134379219466889200, 0, 0, 0])), + AtcRational(Uint256([132853993426794880, 0, 0, 0])), + AtcRational(Uint256([131345923455985760, 0, 0, 0])), + AtcRational(Uint256([129854820089970032, 0, 0, 0])), + AtcRational(Uint256([128380495877339056, 0, 0, 0])), + AtcRational(Uint256([126922765360178944, 0, 0, 0])), + AtcRational(Uint256([125481445054629696, 0, 0, 0])), + AtcRational(Uint256([124056353431594704, 0, 0, 0])), + AtcRational(Uint256([122647310897597840, 0, 0, 0])), + AtcRational(Uint256([121254139775789056, 0, 0, 0])), + AtcRational(Uint256([119876664287099296, 0, 0, 0])), + AtcRational(Uint256([118514710531542512, 0, 0, 0])), + AtcRational(Uint256([117168106469665536, 0, 0, 0])), + AtcRational(Uint256([115836681904146544, 0, 0, 0])), + AtcRational(Uint256([114520268461539280, 0, 0, 0])), + AtcRational(Uint256([113218699574165632, 0, 0, 0])), + AtcRational(Uint256([111931810462153952, 0, 0, 0])), + AtcRational(Uint256([110659438115623328, 0, 0, 0])), + AtcRational(Uint256([109401421277014816, 0, 0, 0])), + AtcRational(Uint256([108157600423566912, 0, 0, 0])), + AtcRational(Uint256([106927817749936160, 0, 0, 0])), + AtcRational(Uint256([105711917150963008, 0, 0, 0])), + AtcRational(Uint256([104509744204580720, 0, 0, 0])), + AtcRational(Uint256([103321146154867984, 0, 0, 0])), + AtcRational(Uint256([102145971895245168, 0, 0, 0])), + AtcRational(Uint256([100984071951811872, 0, 0, 0])), + AtcRational(Uint256([99835298466827488, 0, 0, 0])), + AtcRational(Uint256([98699505182332368, 0, 0, 0])), + AtcRational(Uint256([97576547423909568, 0, 0, 0])), + AtcRational(Uint256([96466282084587616, 0, 0, 0])), + AtcRational(Uint256([95368567608881936, 0, 0, 0])), + AtcRational(Uint256([94283263976975168, 0, 0, 0])), + AtcRational(Uint256([93210232689036528, 0, 0, 0])), + AtcRational(Uint256([92149336749677664, 0, 0, 0])), + AtcRational(Uint256([91100440652546432, 0, 0, 0])), + AtcRational(Uint256([90063410365056304, 0, 0, 0])), + AtcRational(Uint256([89038113313251152, 0, 0, 0])), + AtcRational(Uint256([88024418366805744, 0, 0, 0])), + AtcRational(Uint256([87022195824159632, 0, 0, 0])), + AtcRational(Uint256([86031317397784352, 0, 0, 0])), + AtcRational(Uint256([85051656199584336, 0, 0, 0])), + AtcRational(Uint256([84083086726428336, 0, 0, 0])), + AtcRational(Uint256([83125484845813488, 0, 0, 0])), + AtcRational(Uint256([82178727781658848, 0, 0, 0])), + AtcRational(Uint256([81242694100228816, 0, 0, 0])), + AtcRational(Uint256([80317263696186016, 0, 0, 0])), + AtcRational(Uint256([79402317778771824, 0, 0, 0])), + AtcRational(Uint256([78497738858114176, 0, 0, 0])), + AtcRational(Uint256([77603410731662624, 0, 0, 0])), + AtcRational(Uint256([76719218470748448, 0, 0, 0])), + AtcRational(Uint256([75845048407270416, 0, 0, 0])), + AtcRational(Uint256([74980788120504400, 0, 0, 0])), + AtcRational(Uint256([74126326424036208, 0, 0, 0])), + AtcRational(Uint256([73281553352817728, 0, 0, 0])), + AtcRational(Uint256([72446360150344240, 0, 0, 0])), + AtcRational(Uint256([71620639255952600, 0, 0, 0])), + AtcRational(Uint256([70804284292240360, 0, 0, 0])), + AtcRational(Uint256([69997190052603488, 0, 0, 0])), + AtcRational(Uint256([69199252488892648, 0, 0, 0])), + AtcRational(Uint256([68410368699187752, 0, 0, 0])), + AtcRational(Uint256([67630436915688592, 0, 0, 0])), + AtcRational(Uint256([66859356492722160, 0, 0, 0])), + AtcRational(Uint256([66097027894864808, 0, 0, 0])), + AtcRational(Uint256([65343352685178616, 0, 0, 0])), + AtcRational(Uint256([64598233513561880, 0, 0, 0])), + AtcRational(Uint256([63861574105211760, 0, 0, 0])), + AtcRational(Uint256([63133279249198800, 0, 0, 0])), + AtcRational(Uint256([62413254787153008, 0, 0, 0])), + AtcRational(Uint256([61701407602059336, 0, 0, 0])), + AtcRational(Uint256([60997645607163304, 0, 0, 0])), + AtcRational(Uint256([60301877734984648, 0, 0, 0])), + AtcRational(Uint256([59614013926438576, 0, 0, 0])), + AtcRational(Uint256([58933965120064440, 0, 0, 0])), + AtcRational(Uint256([58261643241359936, 0, 0, 0])), + AtcRational(Uint256([57596961192220440, 0, 0, 0])), + AtcRational(Uint256([56939832840483304, 0, 0, 0])), + AtcRational(Uint256([56290173009574848, 0, 0, 0])), + AtcRational(Uint256([55647897468260864, 0, 0, 0])), + AtcRational(Uint256([55012922920498480, 0, 0, 0])), + AtcRational(Uint256([54385166995389032, 0, 0, 0])), + AtcRational(Uint256([53764548237231728, 0, 0, 0])), + AtcRational(Uint256([53150986095676152, 0, 0, 0])), + AtcRational(Uint256([52544400915973480, 0, 0, 0])), + AtcRational(Uint256([51944713929325792, 0, 0, 0])), + AtcRational(Uint256([51351847243332064, 0, 0, 0])), + AtcRational(Uint256([50765723832530176, 0, 0, 0])), + AtcRational(Uint256([50186267529034840, 0, 0, 0])), + AtcRational(Uint256([49613403013269352, 0, 0, 0])), + AtcRational(Uint256([49047055804791736, 0, 0, 0])), + AtcRational(Uint256([48487152253213424, 0, 0, 0])), + AtcRational(Uint256([47933619529210104, 0, 0, 0])), + AtcRational(Uint256([47386385615624248, 0, 0, 0])), + AtcRational(Uint256([46845379298657936, 0, 0, 0])), + AtcRational(Uint256([46310530159155312, 0, 0, 0])), + AtcRational(Uint256([45781768563974600, 0, 0, 0])), + AtcRational(Uint256([45259025657447672, 0, 0, 0])), + AtcRational(Uint256([44742233352927632, 0, 0, 0])), + AtcRational(Uint256([44231324324422752, 0, 0, 0])), + AtcRational(Uint256([43726231998316280, 0, 0, 0])), + AtcRational(Uint256([43226890545171720, 0, 0, 0])), + AtcRational(Uint256([42733234871622224, 0, 0, 0])), + AtcRational(Uint256([42245200612343560, 0, 0, 0])), + AtcRational(Uint256([41762724122110312, 0, 0, 0])), + AtcRational(Uint256([41285742467933752, 0, 0, 0])), + AtcRational(Uint256([40814193421281544, 0, 0, 0])), + AtcRational(Uint256([40348015450377768, 0, 0, 0])), + AtcRational(Uint256([39887147712583024, 0, 0, 0])), + AtcRational(Uint256([39431530046853688, 0, 0, 0])), + AtcRational(Uint256([38981102966279480, 0, 0, 0])), + AtcRational(Uint256([38535807650699128, 0, 0, 0])), + AtcRational(Uint256([38095585939392688, 0, 0, 0])), + AtcRational(Uint256([37660380323850216, 0, 0, 0])), + AtcRational(Uint256([37230133940616360, 0, 0, 0])), + AtcRational(Uint256([36804790564209328, 0, 0, 0])), + AtcRational(Uint256([36384294600114552, 0, 0, 0])), + AtcRational(Uint256([35968591077851516, 0, 0, 0])), + AtcRational(Uint256([35557625644113388, 0, 0, 0])), + AtcRational(Uint256([35151344555979076, 0, 0, 0])), + AtcRational(Uint256([34749694674196404, 0, 0, 0])), + AtcRational(Uint256([34352623456536068, 0, 0, 0])), + AtcRational(Uint256([33960078951215948, 0, 0, 0])), + AtcRational(Uint256([33572009790394584, 0, 0, 0])), + AtcRational(Uint256([33188365183733360, 0, 0, 0])), + AtcRational(Uint256([32809094912027156, 0, 0, 0])), + AtcRational(Uint256([32434149320901908, 0, 0, 0])), + AtcRational(Uint256([32063479314579508, 0, 0, 0])), + AtcRational(Uint256([31697036349708460, 0, 0, 0])), + AtcRational(Uint256([31334772429260116, 0, 0, 0])), + AtcRational(Uint256([30976640096490016, 0, 0, 0])), + AtcRational(Uint256([30622592428963244, 0, 0, 0])), + AtcRational(Uint256([30272583032643336, 0, 0, 0])), + AtcRational(Uint256([29926566036044560, 0, 0, 0])), + AtcRational(Uint256([29584496084446084, 0, 0, 0])), + AtcRational(Uint256([29246328334168376, 0, 0, 0])), + AtcRational(Uint256([28912018446910460, 0, 0, 0])), + AtcRational(Uint256([28581522584147772, 0, 0, 0])), + AtcRational(Uint256([28254797401590164, 0, 0, 0])), + AtcRational(Uint256([27931800043699132, 0, 0, 0])), + AtcRational(Uint256([27612488138263732, 0, 0, 0])), + AtcRational(Uint256([27296819791035000, 0, 0, 0])), + AtcRational(Uint256([26984753580417632, 0, 0, 0])), + AtcRational(Uint256([26676248552219052, 0, 0, 0])), + AtcRational(Uint256([26371264214454720, 0, 0, 0])), + AtcRational(Uint256([26069760532209384, 0, 0, 0])), + AtcRational(Uint256([25771697922553848, 0, 0, 0])), + AtcRational(Uint256([25477037249516400, 0, 0, 0])), + AtcRational(Uint256([25185739819108396, 0, 0, 0])), + AtcRational(Uint256([24897767374403864, 0, 0, 0])), + AtcRational(Uint256([24613082090671888, 0, 0, 0])), + AtcRational(Uint256([24331646570561924, 0, 0, 0])), + AtcRational(Uint256([24053423839341064, 0, 0, 0])), + AtcRational(Uint256([23778377340182780, 0, 0, 0])), + AtcRational(Uint256([23506470929506944, 0, 0, 0])), + AtcRational(Uint256([23237668872370196, 0, 0, 0])), + AtcRational(Uint256([22971935837906256, 0, 0, 0])), + AtcRational(Uint256([22709236894815996, 0, 0, 0])), + AtcRational(Uint256([22449537506906248, 0, 0, 0])), + AtcRational(Uint256([22192803528677148, 0, 0, 0])), + AtcRational(Uint256([21939001200957664, 0, 0, 0])), + AtcRational(Uint256([21688097146588316, 0, 0, 0])), + AtcRational(Uint256([21440058366151208, 0, 0, 0])), + AtcRational(Uint256([21194852233746400, 0, 0, 0])), + AtcRational(Uint256([20952446492814320, 0, 0, 0])), + AtcRational(Uint256([20712809252003940, 0, 0, 0])), + AtcRational(Uint256([20475908981085852, 0, 0, 0])), + AtcRational(Uint256([20241714506910040, 0, 0, 0])), + AtcRational(Uint256([20010195009407928, 0, 0, 0])), + AtcRational(Uint256([19781320017637956, 0, 0, 0])), + AtcRational(Uint256([19555059405874636, 0, 0, 0])), + AtcRational(Uint256([19331383389740252, 0, 0, 0])), + AtcRational(Uint256([19110262522378940, 0, 0, 0])), + AtcRational(Uint256([18891667690672852, 0, 0, 0])), + AtcRational(Uint256([18675570111499620, 0, 0, 0])), + AtcRational(Uint256([18461941328030932, 0, 0, 0])), + AtcRational(Uint256([18250753206071836, 0, 0, 0])), + AtcRational(Uint256([18041977930440052, 0, 0, 0])), + AtcRational(Uint256([17835588001385282, 0, 0, 0])), +]; From 275b200b823c0bae86f2923292181edc5e06cf87 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 09:59:50 -0400 Subject: [PATCH 02/56] chore: expose mining commit window constant --- stacks-common/src/libcommon.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stacks-common/src/libcommon.rs b/stacks-common/src/libcommon.rs index 0a9fa9d64..7f8824aba 100644 --- a/stacks-common/src/libcommon.rs +++ b/stacks-common/src/libcommon.rs @@ -38,6 +38,8 @@ use crate::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, SortitionId pub mod consts { use crate::types::chainstate::{BlockHeaderHash, ConsensusHash}; + pub use crate::types::MINING_COMMITMENT_WINDOW; + pub const TOKEN_TRANSFER_MEMO_LENGTH: usize = 34; // same as it is in Stacks v1 pub const BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT: u64 = 0; From 2cf2fba065355add9a30aab10423bfdbc918f5b4 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:00:11 -0400 Subject: [PATCH 03/56] feat: mining commit and frequency functions for epochs --- stacks-common/src/types/mod.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index 265234727..0ed0187ad 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -60,6 +60,16 @@ pub const PEER_VERSION_EPOCH_2_0: u8 = 0x00; pub const PEER_VERSION_EPOCH_2_05: u8 = 0x05; pub const PEER_VERSION_EPOCH_2_1: u8 = 0x06; +// sliding burnchain window over which a miner's past block-commit payouts will be used to weight +// its current block-commit in a sortition. +// This is the value used in epoch 2.x +pub const MINING_COMMITMENT_WINDOW: u8 = 6; + +// how often a miner must commit in its mining commitment window in order to even be considered for +// sortition. +// Only relevant for Nakamoto (epoch 3.x) +pub const MINING_COMMITMENT_FREQUENCY_NAKAMOTO: u8 = 3; + #[repr(u32)] #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Copy, Serialize, Deserialize)] pub enum StacksEpochId { @@ -117,6 +127,27 @@ impl StacksEpochId { pub fn supports_pox_missed_slot_unlocks(&self) -> bool { self < &StacksEpochId::Epoch25 } + + /// What is the sortition mining commitment window for this epoch? + pub fn mining_commitment_window(&self) -> u8 { + MINING_COMMITMENT_WINDOW + } + + /// How often must a miner mine in order to be considered for sortition in its commitment + /// window? + pub fn mining_commitment_frequency(&self) -> u8 { + match self { + StacksEpochId::Epoch10 + | StacksEpochId::Epoch20 + | StacksEpochId::Epoch2_05 + | StacksEpochId::Epoch21 + | StacksEpochId::Epoch22 + | StacksEpochId::Epoch23 + | StacksEpochId::Epoch24 + | StacksEpochId::Epoch25 => 0, + StacksEpochId::Epoch30 => MINING_COMMITMENT_FREQUENCY_NAKAMOTO, + } + } } impl std::fmt::Display for StacksEpochId { From e240d533fdf4362c9aa78412d49190ffb13ef641 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:00:39 -0400 Subject: [PATCH 04/56] refactor: put some functionality pertaining to sortition windows into the BurnchainStateTransition struct --- stackslib/src/burnchains/burnchain.rs | 65 ++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index d85631c14..5e5ec5dfb 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -61,8 +61,8 @@ use crate::chainstate::stacks::address::{PoxAddress, StacksAddressExtensions}; use crate::chainstate::stacks::boot::{POX_2_MAINNET_CODE, POX_2_TESTNET_CODE}; use crate::chainstate::stacks::StacksPublicKey; use crate::core::{ - StacksEpoch, StacksEpochId, MINING_COMMITMENT_WINDOW, NETWORK_ID_MAINNET, NETWORK_ID_TESTNET, - PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, STACKS_2_0_LAST_BLOCK_TO_PROCESS, + StacksEpoch, StacksEpochId, NETWORK_ID_MAINNET, NETWORK_ID_TESTNET, PEER_VERSION_MAINNET, + PEER_VERSION_TESTNET, STACKS_2_0_LAST_BLOCK_TO_PROCESS, }; use crate::deps; use crate::monitoring::update_burnchain_height; @@ -89,6 +89,56 @@ impl BurnchainStateTransition { burn_dist: vec![], accepted_ops: vec![], consumed_leader_keys: vec![], + windowed_block_commits: vec![], + windowed_missed_commits: vec![], + } + } + + /// Get the transaction IDs of all accepted burnchain operations in this block + pub fn txids(&self) -> Vec { + self.accepted_ops.iter().map(|ref op| op.txid()).collect() + } + + /// Get the sum of all burnchain tokens spent in this burnchain block's accepted operations + /// (i.e. applies to block commits). + /// Returns None on overflow. + pub fn total_burns(&self) -> Option { + self.accepted_ops.iter().try_fold(0u64, |acc, op| { + let bf = match op { + BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, + _ => 0, + }; + acc.checked_add(bf) + }) + } + + /// Get the median block burn from the window. If the window length is even, then the average + /// of the two middle-most values will be returned. + pub fn windowed_median_burns(&self) -> Option { + let block_total_burn_opts = self.windowed_block_commits.iter().map(|block_commits| { + block_commits + .iter() + .try_fold(0u64, |acc, op| acc.checked_add(op.burn_fee)) + }); + + let mut block_total_burns = vec![]; + for burn_opt in block_total_burn_opts.into_iter() { + block_total_burns.push(burn_opt?); + } + + block_total_burns.sort(); + + if block_total_burns.len() == 0 { + return Some(0); + } else if block_total_burns.len() % 2 != 0 { + let idx = block_total_burns.len() / 2; + return block_total_burns.get(idx).map(|b| *b); + } else { + let idx_left = block_total_burns.len() / 2; + let idx_right = block_total_burns.len() / 2 + 1; + let burn_left = block_total_burns.get(idx_left)?; + let burn_right = block_total_burns.get(idx_right)?; + return Some((burn_left + burn_right) / 2); } } @@ -176,11 +226,11 @@ impl BurnchainStateTransition { } } - for blocks_back in 0..(MINING_COMMITMENT_WINDOW - 1) { + for blocks_back in 0..(epoch_id.mining_commitment_window() - 1) { if parent_snapshot.block_height < (blocks_back as u64) { debug!("Mining commitment window shortened because block height is less than window size"; "block_height" => %parent_snapshot.block_height, - "window_size" => %MINING_COMMITMENT_WINDOW); + "window_size" => %epoch_id.mining_commitment_window()); break; } let block_height = parent_snapshot.block_height - (blocks_back as u64); @@ -243,8 +293,9 @@ impl BurnchainStateTransition { // 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, + epoch_id.mining_commitment_window(), + windowed_block_commits.clone(), + windowed_missed_commits.clone(), burn_blocks, ); BurnSamplePoint::prometheus_update_miner_commitments(&burn_dist); @@ -275,6 +326,8 @@ impl BurnchainStateTransition { burn_dist, accepted_ops, consumed_leader_keys, + windowed_block_commits, + windowed_missed_commits, }) } } From 3c852474167d3c00e116d8ef890c63532ee9b0e5 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:01:03 -0400 Subject: [PATCH 05/56] refactor: BurnchainStateTransition to store the windowed and missed block commits --- stackslib/src/burnchains/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index 26511e152..ca41ccae0 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -48,6 +48,9 @@ use crate::core::*; use crate::net::neighbors::MAX_NEIGHBOR_BLOCK_DELAY; use crate::util_lib::db::Error as db_error; +use crate::chainstate::burn::operations::leader_block_commit::MissedBlockCommit; +use crate::chainstate::burn::operations::LeaderBlockCommitOp; + /// This module contains drivers and types for all burn chains we support. pub mod affirmation; pub mod bitcoin; @@ -646,6 +649,8 @@ pub struct BurnchainStateTransition { pub burn_dist: Vec, pub accepted_ops: Vec, pub consumed_leader_keys: Vec, + pub windowed_block_commits: Vec>, + pub windowed_missed_commits: Vec>, } /// The burnchain block's state transition's ops: From 0261c25ddd39247f133f5d22ee8b15ce09ca6500 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:01:25 -0400 Subject: [PATCH 06/56] fix: refactor --- stackslib/src/burnchains/tests/burnchain.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stackslib/src/burnchains/tests/burnchain.rs b/stackslib/src/burnchains/tests/burnchain.rs index 97c9366fe..e9a54bd04 100644 --- a/stackslib/src/burnchains/tests/burnchain.rs +++ b/stackslib/src/burnchains/tests/burnchain.rs @@ -547,11 +547,12 @@ fn test_process_block_ops() { // everything will be included let block_opshash_124 = OpsHash::from_txids( - &block_ops_124 + block_ops_124 .clone() .into_iter() .map(|bo| bo.txid()) - .collect(), + .collect::>() + .as_slice(), ); let block_prev_chs_124 = vec![ block_123_snapshot.consensus_hash.clone(), From 026baf0a9e2c50dc2f59be53051034088707662d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:01:44 -0400 Subject: [PATCH 07/56] refactor: just pass the BurnchainStateTransition since it can provide the requisite data --- .../src/chainstate/burn/db/processing.rs | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/stackslib/src/chainstate/burn/db/processing.rs b/stackslib/src/chainstate/burn/db/processing.rs index 760188829..d6c33ab60 100644 --- a/stackslib/src/chainstate/burn/db/processing.rs +++ b/stackslib/src/chainstate/burn/db/processing.rs @@ -130,23 +130,6 @@ impl<'a> SortitionHandleTx<'a> { e })?; - let total_burn = state_transition - .accepted_ops - .iter() - .try_fold(0u64, |acc, op| { - let bf = match op { - BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, - _ => 0, - }; - acc.checked_add(bf) - }); - - let txids = state_transition - .accepted_ops - .iter() - .map(|ref op| op.txid()) - .collect(); - let next_pox = SortitionDB::make_next_pox_id(parent_pox.clone(), next_pox_info.as_ref()); let next_sortition_id = SortitionDB::make_next_sortition_id( parent_pox.clone(), @@ -162,9 +145,7 @@ impl<'a> SortitionHandleTx<'a> { &next_pox, parent_snapshot, block_header, - &state_transition.burn_dist, - &txids, - total_burn, + &state_transition, initial_mining_bonus_ustx, ) .map_err(|e| { From 1e337472a9b9f43033de75713c19bc9888a8d2a0 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:02:33 -0400 Subject: [PATCH 08/56] chore: docs for BurnSamplePoint --- stackslib/src/chainstate/burn/distribution.rs | 88 ++++++++++++++++--- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/stackslib/src/chainstate/burn/distribution.rs b/stackslib/src/chainstate/burn/distribution.rs index 2a1689710..9f0b5ec1b 100644 --- a/stackslib/src/chainstate/burn/distribution.rs +++ b/stackslib/src/chainstate/burn/distribution.rs @@ -31,15 +31,22 @@ use crate::chainstate::burn::operations::{ BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::stacks::StacksPublicKey; -use crate::core::MINING_COMMITMENT_WINDOW; use crate::monitoring; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BurnSamplePoint { + /// min(median_burn, most_recent_burn) pub burns: u128, + /// median burn over the UTXO chain pub median_burn: u128, + /// how many times did this miner mine in the window (i.e. how long is the UTXO chain for this + /// candidate in this window). + pub frequency: u8, + /// distribution range start in a [0, 2**256) interval pub range_start: Uint256, + /// distribution range end in a [0, 2**256) interval pub range_end: Uint256, + /// block-commit from the miner candidate pub candidate: LeaderBlockCommitOp, } @@ -94,11 +101,25 @@ impl LinkedCommitIdentifier { } impl BurnSamplePoint { + pub fn zero(candidate: LeaderBlockCommitOp) -> Self { + Self { + burns: 0, + median_burn: 0, + frequency: 0, + range_start: Uint256::zero(), + range_end: Uint256::zero(), + candidate, + } + } + fn sanity_check_window( + miner_commitment_window: u8, block_commits: &Vec>, missed_commits: &Vec>, ) { - assert!(block_commits.len() <= (MINING_COMMITMENT_WINDOW as usize)); + assert!( + block_commits.len() <= usize::try_from(miner_commitment_window).expect("infallible") + ); assert_eq!(missed_commits.len() + 1, block_commits.len()); let mut block_height_at_index = None; for (index, commits) in block_commits.iter().enumerate() { @@ -151,6 +172,7 @@ impl BurnSamplePoint { /// `OP_RETURN` payload. The length of this vector must be equal to the length of the /// `block_commits` vector. `burn_blocks[i]` is `true` if the `ith` block-commit must be PoB. pub fn make_min_median_distribution( + mining_commitment_window: u8, mut block_commits: Vec>, mut missed_commits: Vec>, burn_blocks: Vec, @@ -158,7 +180,11 @@ impl BurnSamplePoint { // sanity check let window_size = block_commits.len() as u8; assert!(window_size > 0); - BurnSamplePoint::sanity_check_window(&block_commits, &missed_commits); + BurnSamplePoint::sanity_check_window( + mining_commitment_window, + &block_commits, + &missed_commits, + ); assert_eq!(burn_blocks.len(), block_commits.len()); // first, let's link all of the current block commits to the priors @@ -283,9 +309,20 @@ impl BurnSamplePoint { "median_burn" => %median_burn, "all_burns" => %format!("{:?}", all_burns)); + let frequency = linked_commits.iter().fold(0u8, |count, commit_opt| { + if commit_opt.is_some() { + count + .checked_add(1) + .expect("infallable -- commit window exceeds u8::MAX") + } else { + count + } + }); + BurnSamplePoint { burns, median_burn, + frequency, range_start: Uint256::zero(), // To be filled in range_end: Uint256::zero(), // To be filled in candidate, @@ -324,14 +361,6 @@ impl BurnSamplePoint { } } - #[cfg(test)] - pub fn make_distribution( - all_block_candidates: Vec, - _consumed_leader_keys: Vec, - ) -> Vec { - Self::make_min_median_distribution(vec![all_block_candidates], vec![], vec![true]) - } - /// Calculate the ranges between 0 and 2**256 - 1 over which each point in the burn sample /// applies, so we can later select which block to use. fn make_sortition_ranges(burn_sample: &mut Vec) -> () { @@ -423,6 +452,21 @@ mod tests { use crate::chainstate::stacks::StacksPublicKey; use crate::core::MINING_COMMITMENT_WINDOW; + impl BurnSamplePoint { + pub fn make_distribution( + mining_commitment_window: u8, + all_block_candidates: Vec, + _consumed_leader_keys: Vec, + ) -> Vec { + Self::make_min_median_distribution( + mining_commitment_window, + vec![all_block_candidates], + vec![], + vec![true], + ) + } + } + struct BurnDistFixture { consumed_leader_keys: Vec, block_commits: Vec, @@ -531,6 +575,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + MINING_COMMITMENT_WINDOW, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, true, true, true], @@ -564,6 +609,7 @@ mod tests { // miner 2 => min = 1, median = 3, last_burn = 3 let mut result = BurnSamplePoint::make_min_median_distribution( + MINING_COMMITMENT_WINDOW, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, true, true, true], @@ -624,6 +670,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + MINING_COMMITMENT_WINDOW, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, false, false, false], @@ -677,6 +724,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + MINING_COMMITMENT_WINDOW, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, false, false, false], @@ -733,6 +781,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + MINING_COMMITMENT_WINDOW, commits.clone(), missed_commits.clone(), vec![false, false, false, false, false, false], @@ -998,6 +1047,7 @@ mod tests { median_burn: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256::max(), + frequency: 10, candidate: block_commit_1.clone(), }], }, @@ -1016,12 +1066,14 @@ mod tests { 0xffffffffffffffff, 0x7fffffffffffffff, ]), + frequency: 10, candidate: block_commit_1.clone(), }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1041,6 +1093,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1054,6 +1107,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1073,6 +1127,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1086,6 +1141,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1105,6 +1161,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1118,6 +1175,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1137,6 +1195,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1150,6 +1209,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1169,6 +1229,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1182,6 +1243,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), + frequency: 10, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1208,6 +1270,7 @@ mod tests { BurnSamplePoint { burns: block_commit_1.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), + frequency: 10, range_start: Uint256::zero(), range_end: Uint256([ 0x3ed94d3cb0a84709, @@ -1220,6 +1283,7 @@ mod tests { BurnSamplePoint { burns: block_commit_2.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), + frequency: 10, range_start: Uint256([ 0x3ed94d3cb0a84709, 0x0963dded799a7c1a, @@ -1237,6 +1301,7 @@ mod tests { BurnSamplePoint { burns: (block_commit_3.burn_fee).into(), median_burn: block_commit_3.burn_fee.into(), + frequency: 10, range_start: Uint256([ 0x7db29a7961508e12, 0x12c7bbdaf334f834, @@ -1254,6 +1319,7 @@ mod tests { let f = &fixtures[i]; eprintln!("Fixture #{}", i); let dist = BurnSamplePoint::make_distribution( + MINING_COMMITMENT_WINDOW, f.block_commits.iter().cloned().collect(), f.consumed_leader_keys.iter().cloned().collect(), ); From cb529b89b216fa3a7dae7d48338abe1eeb1ebd31 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:02:47 -0400 Subject: [PATCH 09/56] refactor: from slice not vec --- stackslib/src/chainstate/burn/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/burn/mod.rs b/stackslib/src/chainstate/burn/mod.rs index b764344eb..be92c3088 100644 --- a/stackslib/src/chainstate/burn/mod.rs +++ b/stackslib/src/chainstate/burn/mod.rs @@ -38,6 +38,7 @@ use crate::chainstate::burn::db::sortdb::SortitionHandleTx; use crate::core::SYSTEM_FORK_SET_VERSION; use crate::util_lib::db::Error as db_error; +pub mod atc; /// This module contains the code for processing the burn chain state database pub mod db; pub mod distribution; @@ -223,7 +224,7 @@ impl Opcodes { } impl OpsHash { - pub fn from_txids(txids: &Vec) -> OpsHash { + pub fn from_txids(txids: &[Txid]) -> OpsHash { // NOTE: unlike stacks v1, we calculate the ops hash simply // from a hash-chain of txids. There is no weird serialization // of operations, and we don't construct a merkle tree over From c40b801b375645f48583f83a8a8b0371aa15c08e Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:03:02 -0400 Subject: [PATCH 10/56] feat: implement ATC-C anti-MEV measure -- the miner has to consistently commit in order to even be considered the winner of a sortition --- stackslib/src/chainstate/burn/sortition.rs | 673 +++++++++++++++++++-- 1 file changed, 627 insertions(+), 46 deletions(-) diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 7d86f8480..186acfdca 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -26,7 +26,8 @@ use stacks_common::util::log; use stacks_common::util::uint::{BitArray, Uint256, Uint512}; use crate::burnchains::{ - Address, Burnchain, BurnchainBlock, BurnchainBlockHeader, PublicKey, Txid, + Address, Burnchain, BurnchainBlock, BurnchainBlockHeader, BurnchainStateTransition, PublicKey, + Txid, }; use crate::chainstate::burn::db::sortdb::SortitionHandleTx; use crate::chainstate::burn::distribution::BurnSamplePoint; @@ -42,6 +43,12 @@ use crate::chainstate::stacks::index::{ClarityMarfTrieId, MarfTrieId, TrieHashEx use crate::core::*; use crate::util_lib::db::Error as db_error; +use crate::chainstate::burn::db::sortdb::SortitionDB; + +use crate::burnchains::BurnchainSigner; + +use crate::chainstate::burn::atc::{AtcRational, ATC_LOOKUP}; + impl BlockSnapshot { /// Creates an "empty" (i.e. zeroed out) BlockSnapshot, to make a basis for creating /// `BlockSnapshot` with a few key fields filled. @@ -143,8 +150,8 @@ impl BlockSnapshot { for i in 0..dist.len() { if (dist[i].range_start <= index) && (index < dist[i].range_end) { debug!( - "Sampled {}: sortition index = {}", - dist[i].candidate.block_header_hash, &index + "Sampled {}: i = {}, sortition index = {}", + dist[i].candidate.block_header_hash, i, &index ); return Some(i); } @@ -154,6 +161,36 @@ impl BlockSnapshot { panic!("FATAL ERROR: unable to map {} to a range", index); } + /// Get the last winning miner's VRF seed in this block's fork. + /// Returns Ok(VRF seed) on success + /// Returns Err(..) on DB error + /// An initial VRF seed value will be returned if there are no prior commits. + fn get_last_vrf_seed( + sort_tx: &mut SortitionHandleTx, + block_header: &BurnchainBlockHeader, + ) -> Result { + let burn_block_height = block_header.block_height; + + // get the last winner's VRF seed in this block's fork + let last_sortition_snapshot = + sort_tx.get_last_snapshot_with_sortition(burn_block_height - 1)?; + + let vrf_seed = if last_sortition_snapshot.is_initial() { + // this is the sentinal "first-sortition" block + VRFSeed::initial() + } else { + // there may have been a prior winning block commit. Use its VRF seed if possible + sort_tx + .get_block_commit( + &last_sortition_snapshot.winning_block_txid, + &last_sortition_snapshot.sortition_id, + )? + .expect("FATAL ERROR: no winning block commits in database (indicates corruption)") + .new_seed + }; + Ok(vrf_seed) + } + /// Select the next Stacks block header hash using cryptographic sortition. /// Go through all block commits at this height, find out how any burn tokens /// were spent for them, and select one at random using the relative burn amounts @@ -170,30 +207,12 @@ impl BlockSnapshot { block_header: &BurnchainBlockHeader, sortition_hash: &SortitionHash, burn_dist: &[BurnSamplePoint], - ) -> Result, db_error> { - let burn_block_height = block_header.block_height; - - // get the last winner's VRF seed in this block's fork - let last_sortition_snapshot = - sort_tx.get_last_snapshot_with_sortition(burn_block_height - 1)?; - - let VRF_seed = if last_sortition_snapshot.is_initial() { - // this is the sentinal "first-sortition" block - VRFSeed::initial() - } else { - // there may have been a prior winning block commit. Use its VRF seed if possible - sort_tx - .get_block_commit( - &last_sortition_snapshot.winning_block_txid, - &last_sortition_snapshot.sortition_id, - )? - .expect("FATAL ERROR: no winning block commits in database (indicates corruption)") - .new_seed - }; + ) -> Result, db_error> { + let vrf_seed = Self::get_last_vrf_seed(sort_tx, block_header)?; // pick the next winner let win_idx_opt = - BlockSnapshot::sample_burn_distribution(burn_dist, &VRF_seed, sortition_hash); + BlockSnapshot::sample_burn_distribution(burn_dist, &vrf_seed, sortition_hash); match win_idx_opt { None => { // no winner @@ -201,7 +220,7 @@ impl BlockSnapshot { } Some(win_idx) => { // winner! - Ok(Some(burn_dist[win_idx].candidate.clone())) + Ok(Some((burn_dist[win_idx].candidate.clone(), win_idx))) } } } @@ -216,7 +235,7 @@ impl BlockSnapshot { first_block_height: u64, burn_total: u64, sortition_hash: &SortitionHash, - txids: &Vec, + txids: &[Txid], accumulated_coinbase_ustx: u128, ) -> Result { let block_height = block_header.block_height; @@ -269,6 +288,221 @@ impl BlockSnapshot { }) } + /// Determine if we need to reject a block-commit due to miner inactivity. + /// Return true if the miner is sufficiently active. + /// Return false if not. + fn check_miner_is_active( + epoch_id: StacksEpochId, + sampled_window_len: usize, + winning_block_sender: &BurnchainSigner, + miner_frequency: u8, + ) -> bool { + // miner frequency only applies if the window is at least as long as the commit window + // sampled from the chain state (e.g. because this window can be 1 during the prepare + // phase) + let epoch_frequency_usize = + usize::try_from(epoch_id.mining_commitment_frequency()).expect("Infallible"); + if epoch_frequency_usize >= sampled_window_len + && miner_frequency < epoch_id.mining_commitment_frequency() + { + // this miner didn't mine often enough to win anyway + info!("Miner did not mine often enough to win"; + "miner_sender" => %winning_block_sender, + "miner_frequency" => miner_frequency, + "minimum_frequency" => epoch_id.mining_commitment_frequency(), + "window_length" => sampled_window_len); + + return false; + } + + true + } + + /// Determine the miner's assumed total commit carryover. + /// + /// total-block-spend + /// This is ATC = min(1, ----------------------------------- ) + /// median-windowed-total-block-spend + /// + /// Now, this value is 1.0 in the "happy path" case where miners commit the same BTC in this + /// block as they had done so over the majority of the windowed burnchain blocks. + /// + /// It's also 1.0 if miners spend _more_ than this median. + /// + /// It's between 0.0 and 1.0 only if miners spend _less_ than this median. At this point, it's + /// possible that the "null miner" can win sortition, and the probability of that null miner + /// winning is a function of (1.0 - ATC). + /// + /// Returns the ATC value, and whether or not it decreased. If the ATC decreased, then we must + /// invoke the null miner. + fn get_miner_commit_carryover( + total_burns: Option, + windowed_median_burns: Option, + ) -> (AtcRational, bool) { + let Some(block_burn_total) = total_burns else { + // overflow + return (AtcRational::zero(), false); + }; + + let Some(windowed_median_burns) = windowed_median_burns else { + // overflow + return (AtcRational::zero(), false); + }; + + if block_burn_total >= windowed_median_burns { + // clamp to 1.0, and ATC increased + return (AtcRational::one(), false); + } + + if windowed_median_burns == 0 { + // no carried commit, so null miner wins by default. + return (AtcRational::zero(), true); + } + + ( + AtcRational::frac(block_burn_total, windowed_median_burns), + true, + ) + } + + /// Evaluate the advantage logistic function on the given ATC value. + /// The ATC value will be used to index a lookup table of AtcRationals. + pub(crate) fn null_miner_logistic(atc: AtcRational) -> AtcRational { + let atc_clamp = atc.min(&AtcRational::one()); + let index_max = + u64::try_from(ATC_LOOKUP.len() - 1).expect("infallible -- u64 can't hold 1023usize"); + let index_u64 = if let Some(index_rational) = atc_clamp.mul(&AtcRational::frac(1024, 1)) { + // extract integer part + index_rational.ipart().min(index_max) + } else { + index_max + }; + let index = usize::try_from(index_u64) + .expect("infallible -- usize can't hold u64 integers in [0, 1024)"); + ATC_LOOKUP + .get(index) + .cloned() + .unwrap_or_else(|| ATC_LOOKUP.last().cloned().expect("infallible")) + } + + /// Determine the probability that the null miner will win, given the atc shortage. + /// + /// This is NullP(atc) = (1 - atc) + atc * adv(atc). + /// + /// Where atv(x) is an "advantage function", such that the null miner is more heavily favored + /// to win based on how comparatively little commit carryover there is. Here, adv(x) is a + /// logistic function. + /// + /// In a linear setting -- i.e. the probability of the null miner winning being proportional to + /// the missing carryover -- the probability would simply be (1 - atc). If miners spent only + /// X% of the assumed total commit, then the null miner ought to win with probability (1 - X)%. + /// However, the null miner is advantaged more if the missing carryover is smaller. This is + /// captured with the extra `atc * adv(atc)` term. + pub(crate) fn null_miner_probability(atc: AtcRational) -> AtcRational { + // compute min(1.0, (1.0 - atc) + (atc * adv)) + let adv = Self::null_miner_logistic(atc); + let Some(one_minus_atc) = AtcRational::one().sub(&atc) else { + // somehow, ATC > 1.0, then miners spent more than they did in the last sortition. + // So, the null miner loses. + warn!("ATC > 1.0 ({})", &atc.to_hex()); + return AtcRational::zero(); + }; + + let Some(atc_prod_adv) = atc.mul(&adv) else { + // if this is somehow too big (impossible), it would otherwise imply that the null + // miner advantage is overwhelming + warn!("ATC * ADV == INF ({} * {})", &atc.to_hex(), &adv.to_hex()); + return AtcRational::one(); + }; + + let Some(sum) = one_minus_atc.add(&atc_prod_adv) else { + // if this is somehow too big (impossible), it would otherwise imply that the null + // miner advantage is overwhelming + warn!( + "(1.0 - ATC) + (ATC * ADV) == INF ({} * {})", + &one_minus_atc.to_hex(), + &atc_prod_adv.to_hex() + ); + return AtcRational::one(); + }; + sum.min(&AtcRational::one()) + } + + /// Determine whether or not the null miner has won sortition. + /// This works by creating a second burn distribution: one with the winning block-commit, and + /// one with the null miner. The null miner's mining power will be computed as a function of + /// their ATC advantage. + fn null_miner_wins( + sort_tx: &mut SortitionHandleTx, + block_header: &BurnchainBlockHeader, + sortition_hash: &SortitionHash, + commit_winner: &LeaderBlockCommitOp, + atc: AtcRational, + ) -> Result { + let vrf_seed = Self::get_last_vrf_seed(sort_tx, block_header)?; + + let mut null_winner = commit_winner.clone(); + null_winner.block_header_hash = { + // make the block header hash different, to render it different from the winner. + // Just flip the block header bits. + let mut bhh_bytes = null_winner.block_header_hash.0.clone(); + for i in 0..bhh_bytes.len() { + bhh_bytes[i] = !bhh_bytes[i]; + } + BlockHeaderHash(bhh_bytes) + }; + + let mut null_sample_winner = BurnSamplePoint::zero(null_winner.clone()); + let mut burn_sample_winner = BurnSamplePoint::zero(commit_winner.clone()); + + let null_prob = Self::null_miner_probability(atc); + + // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the + // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction + // fo mining power the null miner has. + let null_prob_u256 = if null_prob.0 >= AtcRational::one().0 { + // prevent left-shift overflow + AtcRational::one_sup().into_inner() << 192 + } else { + null_prob.into_inner() << 192 + }; + + test_debug!( + "atc = {}, null_prob = {}, null_prob_u256 = {}, sortition_hash: {}", + atc.to_hex(), + null_prob.to_hex(), + null_prob_u256.to_hex_be(), + sortition_hash + ); + null_sample_winner.range_start = Uint256::zero(); + null_sample_winner.range_end = null_prob_u256; + + burn_sample_winner.range_start = null_prob_u256; + burn_sample_winner.range_end = Uint256::max(); + + let burn_dist = [ + // the only fields that matter here are: + // * range_start + // * range_end + // * candidate + null_sample_winner, + burn_sample_winner, + ]; + + // pick the next winner + let Some(win_idx) = + BlockSnapshot::sample_burn_distribution(&burn_dist, &vrf_seed, sortition_hash) + else { + // miner wins by default if there's no winner index + return Ok(false); + }; + + test_debug!("win_idx = {}", win_idx); + + // null miner is index 0 + Ok(win_idx == 0) + } + /// Make a block snapshot from is block's data and the previous block. /// This process will: /// * calculate the new consensus hash @@ -286,10 +520,42 @@ impl BlockSnapshot { my_pox_id: &PoxId, parent_snapshot: &BlockSnapshot, block_header: &BurnchainBlockHeader, - burn_dist: &[BurnSamplePoint], - txids: &Vec, - block_burn_total: Option, + state_transition: &BurnchainStateTransition, initial_mining_bonus_ustx: u128, + ) -> Result { + // what epoch will this snapshot be in? + let epoch_id = SortitionDB::get_stacks_epoch(sort_tx, parent_snapshot.block_height + 1)? + .unwrap_or_else(|| { + panic!( + "FATAL: no epoch defined at burn height {}", + parent_snapshot.block_height + 1 + ) + }) + .epoch_id; + + Self::make_snapshot_in_epoch( + sort_tx, + burnchain, + my_sortition_id, + my_pox_id, + parent_snapshot, + block_header, + state_transition, + initial_mining_bonus_ustx, + epoch_id, + ) + } + + pub fn make_snapshot_in_epoch( + sort_tx: &mut SortitionHandleTx, + burnchain: &Burnchain, + my_sortition_id: &SortitionId, + my_pox_id: &PoxId, + parent_snapshot: &BlockSnapshot, + block_header: &BurnchainBlockHeader, + state_transition: &BurnchainStateTransition, + initial_mining_bonus_ustx: u128, + epoch_id: StacksEpochId, ) -> Result { assert_eq!( parent_snapshot.burn_header_hash, @@ -332,12 +598,12 @@ impl BlockSnapshot { first_block_height, last_burn_total, &next_sortition_hash, - &txids, + &state_transition.txids(), accumulated_coinbase_ustx, ) }; - if burn_dist.len() == 0 { + if state_transition.burn_dist.len() == 0 { // no burns happened debug!( "No burns happened in block"; @@ -350,7 +616,7 @@ impl BlockSnapshot { // NOTE: this only counts burns from leader block commits and user burns that match them. // It ignores user burns that don't match any block. - let block_burn_total = match block_burn_total { + let block_burn_total = match state_transition.total_burns() { Some(total) => { if total == 0 { // no one burned, so no sortition @@ -384,18 +650,76 @@ impl BlockSnapshot { }; // Try to pick a next block. - let winning_block = BlockSnapshot::select_winning_block( + let (winning_block, winning_block_burn_dist_index) = BlockSnapshot::select_winning_block( sort_tx, block_header, &next_sortition_hash, - burn_dist, + &state_transition.burn_dist, )? .expect("FATAL: there must be a winner if the burn distribution has 1 or more points"); + // in epoch 3.x and later (Nakamoto and later), there's two additional changes: + // * if the winning miner didn't mine in more than k of n blocks of the window, then their chances of + // winning are 0. + // * There exists a "null miner" that can win sortition, in which case there is no + // sortition. This happens if the assumed total commit with carry-over is sufficently low. + let mut reject_winner_reason = None; + if epoch_id >= StacksEpochId::Epoch30 { + if !Self::check_miner_is_active( + epoch_id, + state_transition.windowed_block_commits.len(), + &winning_block.apparent_sender, + state_transition.burn_dist[winning_block_burn_dist_index].frequency, + ) { + reject_winner_reason = Some("Miner did not mine often enough to win".to_string()); + } + let (atc, null_active) = Self::get_miner_commit_carryover( + state_transition.total_burns(), + state_transition.windowed_median_burns(), + ); + if null_active && reject_winner_reason.is_none() { + // there's a chance the null miner can win + if Self::null_miner_wins( + sort_tx, + block_header, + &next_sortition_hash, + &winning_block, + atc, + )? { + // null wins + reject_winner_reason = Some( + "Null miner defeats block winner due to insufficient commit carryover" + .to_string(), + ); + } + } + } + + if let Some(reject_winner_reason) = reject_winner_reason { + info!("SORTITION({}): WINNER REJECTED: {}", block_height, &reject_winner_reason; + "txid" => %winning_block.txid, + "block_hash" => %winning_block.block_header_hash); + + // N.B. can't use `make_snapshot_no_sortition()` helper here because then `sort_tx` + // would be mutably borrowed twice. + return BlockSnapshot::make_snapshot_no_sortition( + sort_tx, + my_sortition_id, + my_pox_id, + parent_snapshot, + block_header, + first_block_height, + last_burn_total, + &next_sortition_hash, + &state_transition.txids(), + accumulated_coinbase_ustx, + ); + } + // mix in the winning block's VRF seed to the sortition hash. The next block commits must // prove on this final sortition hash. let final_sortition_hash = next_sortition_hash.mix_VRF_seed(&winning_block.new_seed); - let next_ops_hash = OpsHash::from_txids(&txids); + let next_ops_hash = OpsHash::from_txids(&state_transition.txids()); let next_ch = ConsensusHash::from_parent_block_data( sort_tx, &next_ops_hash, @@ -406,7 +730,7 @@ impl BlockSnapshot { my_pox_id, )?; - debug!( + info!( "SORTITION({}): WINNER IS {:?} (from {:?})", block_height, &winning_block.block_header_hash, &winning_block.txid ); @@ -462,10 +786,16 @@ mod test { use super::*; use crate::burnchains::tests::*; use crate::burnchains::*; + use crate::chainstate::burn::db::sortdb::tests::test_append_snapshot_with_winner; use crate::chainstate::burn::db::sortdb::*; + use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use crate::chainstate::burn::operations::*; use crate::chainstate::stacks::*; + use crate::burnchains::BurnchainSigner; + + use crate::chainstate::burn::atc::AtcRational; + fn test_make_snapshot( sort_tx: &mut SortitionHandleTx, burnchain: &Burnchain, @@ -473,10 +803,8 @@ mod test { my_pox_id: &PoxId, parent_snapshot: &BlockSnapshot, block_header: &BurnchainBlockHeader, - burn_dist: &[BurnSamplePoint], - txids: &Vec, + burnchain_state_transition: &BurnchainStateTransition, ) -> Result { - let total_burn = BurnSamplePoint::get_total_burns(burn_dist); BlockSnapshot::make_snapshot( sort_tx, burnchain, @@ -484,9 +812,7 @@ mod test { my_pox_id, parent_snapshot, block_header, - burn_dist, - txids, - total_burn, + burnchain_state_transition, 0, ) } @@ -540,8 +866,7 @@ mod test { &pox_id, &initial_snapshot, &empty_block_header, - &vec![], - &vec![], + &BurnchainStateTransition::noop(), ) .unwrap(); sn @@ -567,6 +892,7 @@ mod test { 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, ]), + frequency: 10, candidate: LeaderBlockCommitOp::initial( &BlockHeaderHash([1u8; 32]), first_block_height + 1, @@ -594,8 +920,11 @@ mod test { &pox_id, &initial_snapshot, &empty_block_header, - &vec![empty_burn_point.clone()], - &vec![key.txid.clone()], + &BurnchainStateTransition { + burn_dist: vec![empty_burn_point.clone()], + accepted_ops: vec![BlockstackOperationType::LeaderKeyRegister(key.clone())], + ..BurnchainStateTransition::noop() + }, ) .unwrap(); sn @@ -604,4 +933,256 @@ mod test { assert!(!snapshot_no_burns.sortition); assert_eq!(snapshot_no_transactions.total_burn, 0); } + + #[test] + fn test_check_is_miner_active() { + assert_eq!(StacksEpochId::Epoch30.mining_commitment_frequency(), 10); + assert_eq!(StacksEpochId::Epoch25.mining_commitment_frequency(), 6); + + // reward phase + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 10 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 9 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 8 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 7 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 6 + )); + assert!(!BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 10, + &BurnchainSigner("".to_string()), + 5 + )); + + // prepare phase + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 5 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 4 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 3 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 2 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 1 + )); + assert!(!BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 1, + &BurnchainSigner("".to_string()), + 0 + )); + } + + #[test] + fn test_get_miner_commit_carryover() { + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(None, None), + (AtcRational::zero(), false) + ); + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(None, Some(1)), + (AtcRational::zero(), false) + ); + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(Some(1), None), + (AtcRational::zero(), false) + ); + + // ATC increased + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(Some(1), Some(1)), + (AtcRational::one(), false) + ); + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(Some(2), Some(1)), + (AtcRational::one(), false) + ); + + // no carried commit + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(Some(2), Some(0)), + (AtcRational::zero(), false) + ); + + // assumed carryover + assert_eq!( + BlockSnapshot::get_miner_commit_carryover(Some(2), Some(4)), + (AtcRational::frac(2, 4), true) + ); + } + + #[test] + fn test_null_miner_logistic() { + for i in 0..1024 { + let atc_u256 = ATC_LOOKUP[i]; + let null_miner_lgst = + BlockSnapshot::null_miner_logistic(AtcRational::frac(i as u64, 1024)); + assert_eq!(null_miner_lgst, atc_u256); + } + assert_eq!( + BlockSnapshot::null_miner_logistic(AtcRational::zero()), + ATC_LOOKUP[0] + ); + assert_eq!( + BlockSnapshot::null_miner_logistic(AtcRational::one()), + *ATC_LOOKUP.last().as_ref().cloned().unwrap() + ); + assert_eq!( + BlockSnapshot::null_miner_logistic(AtcRational::frac(100, 1)), + *ATC_LOOKUP.last().as_ref().cloned().unwrap() + ); + } + + #[test] + fn test_null_miner_wins() { + let first_burn_hash = BurnchainHeaderHash([0xfe; 32]); + let parent_first_burn_hash = BurnchainHeaderHash([0xff; 32]); + let first_block_height = 120; + + let mut prev_block_header = BurnchainBlockHeader { + block_height: first_block_height, + block_hash: first_burn_hash.clone(), + parent_block_hash: parent_first_burn_hash.clone(), + num_txs: 0, + timestamp: 12345, + }; + + let burnchain = Burnchain { + pox_constants: PoxConstants::test_default(), + peer_version: 0x012345678, + network_id: 0x9abcdef0, + chain_name: "bitcoin".to_string(), + network_name: "testnet".to_string(), + working_dir: "/nope".to_string(), + consensus_hash_lifetime: 24, + stable_confirmations: 7, + first_block_timestamp: 0, + first_block_height, + initial_reward_start_block: first_block_height, + first_block_hash: first_burn_hash.clone(), + }; + + let mut db = SortitionDB::connect_test(first_block_height, &first_burn_hash).unwrap(); + + for i in 0..100 { + let header = BurnchainBlockHeader { + block_height: prev_block_header.block_height + 1, + block_hash: BurnchainHeaderHash([i as u8; 32]), + parent_block_hash: prev_block_header.block_hash.clone(), + num_txs: 0, + timestamp: prev_block_header.timestamp + (i as u64) + 1, + }; + + let sortition_hash = SortitionHash([i as u8; 32]); + + let commit_winner = LeaderBlockCommitOp { + sunset_burn: 0, + block_header_hash: BlockHeaderHash([i as u8; 32]), + new_seed: VRFSeed([i as u8; 32]), + parent_block_ptr: 0, + parent_vtxindex: 0, + key_block_ptr: 0, + key_vtxindex: 0, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: 100, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner(format!("signer {}", i)), + txid: Txid([i as u8; 32]), + vtxindex: 0, + block_height: header.block_height, + burn_parent_modulus: (i % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: header.block_hash.clone(), + }; + + let tip = SortitionDB::get_canonical_burn_chain_tip(db.conn()).unwrap(); + test_append_snapshot_with_winner( + &mut db, + header.block_hash.clone(), + &vec![BlockstackOperationType::LeaderBlockCommit( + commit_winner.clone(), + )], + Some(tip), + Some(commit_winner.clone()), + ); + + let mut sort_tx = db.tx_begin_at_tip(); + + for j in 0..100 { + let atc = AtcRational::from_f64_unit((j as f64) / 100.0); + let null_prob = BlockSnapshot::null_miner_probability(atc); + let null_prob_u256 = if null_prob.0 >= AtcRational::one().0 { + // prevent left-shift overflow + AtcRational::one_sup().into_inner() << 192 + } else { + null_prob.into_inner() << 192 + }; + + let null_wins = BlockSnapshot::null_miner_wins( + &mut sort_tx, + &header, + &sortition_hash, + &commit_winner, + atc, + ) + .unwrap(); + debug!("null_wins: {},{}: {}", i, j, null_wins); + + let vrf_seed = BlockSnapshot::get_last_vrf_seed(&mut sort_tx, &header).unwrap(); + let index = sortition_hash.mix_VRF_seed(&vrf_seed).to_uint256(); + + if index < null_prob_u256 { + assert!(null_wins); + } else { + assert!(!null_wins); + } + } + + prev_block_header = header.clone(); + } + } } From bbade1533561f296a5b34d0cfcff265658a7e82d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:03:27 -0400 Subject: [PATCH 11/56] refactor: some constants moved --- stackslib/src/core/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/stackslib/src/core/mod.rs b/stackslib/src/core/mod.rs index 629cb02c9..fc4159518 100644 --- a/stackslib/src/core/mod.rs +++ b/stackslib/src/core/mod.rs @@ -40,7 +40,9 @@ pub type StacksEpoch = GenericStacksEpoch; pub const SYSTEM_FORK_SET_VERSION: [u8; 4] = [23u8, 0u8, 0u8, 0u8]; // chain id -pub use stacks_common::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, STACKS_EPOCH_MAX}; +pub use stacks_common::consts::{ + CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, MINING_COMMITMENT_WINDOW, STACKS_EPOCH_MAX, +}; // peer version (big-endian) // first byte == major network protocol version (currently 0x18) @@ -74,10 +76,6 @@ pub const NETWORK_ID_TESTNET: u32 = 0xff000000; // default port pub const NETWORK_P2P_PORT: u16 = 6265; -// sliding burnchain window over which a miner's past block-commit payouts will be used to weight -// its current block-commit in a sortition -pub const MINING_COMMITMENT_WINDOW: u8 = 6; - // Number of previous burnchain blocks to search to find burnchain-hosted Stacks operations pub const BURNCHAIN_TX_SEARCH_WINDOW: u8 = 6; From 05f812da0c349d9a7170d2ab5832bc7fcaafab3b Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 10:03:47 -0400 Subject: [PATCH 12/56] feat: add tool to stacks-inspect to analyze Bitcoin MEV --- stackslib/src/main.rs | 132 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 1040f867c..7d3f725e9 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -33,7 +33,7 @@ use tikv_jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs::{File, OpenOptions}; use std::io::prelude::*; use std::io::BufReader; @@ -87,6 +87,12 @@ use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey}; use stacks_common::util::vrf::VRFProof; use stacks_common::util::{get_epoch_time_ms, log, sleep_ms}; +use blockstack_lib::chainstate::burn::db::sortdb::get_block_commit_by_txid; +use blockstack_lib::chainstate::burn::db::sortdb::SortitionHandle; +use blockstack_lib::chainstate::burn::BlockSnapshot; +use blockstack_lib::chainstate::coordinator::get_reward_cycle_info; +use blockstack_lib::chainstate::coordinator::OnChainRewardSetProvider; + fn main() { let mut argv: Vec = env::args().collect(); if argv.len() < 2 { @@ -1017,6 +1023,130 @@ simulating a miner. process::exit(0); } + if argv[1] == "analyze-sortition-mev" { + if argv.len() < 7 { + eprintln!( + "Usage: {} /path/to/burnchain/db /path/to/sortition/db /path/to/chainstate/db start_height end_height", + &argv[0] + ); + process::exit(1); + } + + let burnchaindb_path = argv[2].clone(); + let sortdb_path = argv[3].clone(); + let chainstate_path = argv[4].clone(); + let start_height: u64 = argv[5].parse().unwrap(); + let end_height: u64 = argv[6].parse().unwrap(); + + let mut sortdb = + SortitionDB::open(&sortdb_path, true, PoxConstants::mainnet_default()).unwrap(); + let burnchain = Burnchain::new(&burnchaindb_path, "bitcoin", "mainnet").unwrap(); + let burnchaindb = BurnchainDB::connect(&burnchaindb_path, &burnchain, true).unwrap(); + let (mut chainstate, _) = + StacksChainState::open(true, 0x00000001, &chainstate_path, None).unwrap(); + + let mut wins_epoch2 = BTreeMap::new(); + let mut wins_epoch3 = BTreeMap::new(); + + for height in start_height..end_height { + let (tip_sort_id, ancestor_sn) = { + let mut sort_tx = sortdb.tx_begin_at_tip(); + let tip_sort_id = sort_tx.tip(); + let ancestor_sn = sort_tx + .get_block_snapshot_by_height(height) + .unwrap() + .unwrap(); + (tip_sort_id, ancestor_sn) + }; + let rc_info_opt = get_reward_cycle_info( + ancestor_sn.block_height + 1, + &ancestor_sn.burn_header_hash, + &tip_sort_id, + &burnchain, + &burnchaindb, + &mut chainstate, + &mut sortdb, + &OnChainRewardSetProvider::new(), + false, + ) + .unwrap(); + + let burn_block = + BurnchainDB::get_burnchain_block(burnchaindb.conn(), &ancestor_sn.burn_header_hash) + .unwrap(); + let (next_sn, state_transition) = sortdb + .evaluate_sortition( + &burn_block.header, + burn_block.ops.clone(), + &burnchain, + &tip_sort_id, + rc_info_opt, + |_| (), + ) + .unwrap(); + + let mut sort_tx = sortdb.tx_begin_at_tip(); + let tip_pox_id = sort_tx.get_pox_id().unwrap(); + let next_sn_nakamoto = BlockSnapshot::make_snapshot_in_epoch( + &mut sort_tx, + &burnchain, + &ancestor_sn.sortition_id, + &tip_pox_id, + &ancestor_sn, + &burn_block.header, + &state_transition, + 0, + StacksEpochId::Epoch30, + ) + .unwrap(); + + assert_eq!(next_sn.block_height, next_sn_nakamoto.block_height); + assert_eq!(next_sn.burn_header_hash, next_sn_nakamoto.burn_header_hash); + + let winner_epoch2 = + get_block_commit_by_txid(&sort_tx, &tip_sort_id, &next_sn.winning_block_txid) + .unwrap() + .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) + .unwrap_or("(null)".to_string()); + + let winner_epoch3 = get_block_commit_by_txid( + &sort_tx, + &tip_sort_id, + &next_sn_nakamoto.winning_block_txid, + ) + .unwrap() + .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) + .unwrap_or("(null)".to_string()); + + wins_epoch2.insert( + (next_sn.block_height, next_sn.burn_header_hash), + winner_epoch2, + ); + wins_epoch3.insert( + ( + next_sn_nakamoto.block_height, + next_sn_nakamoto.burn_header_hash, + ), + winner_epoch3, + ); + } + + println!("Wins epoch 2"); + println!("------------"); + println!("height,burn_header_hash,winner"); + for ((height, bhh), winner) in wins_epoch2.into_iter() { + println!("{},{},{}", &height, &bhh, &winner); + } + + println!("------------"); + println!("Wins epoch 3"); + println!("------------"); + println!("height,burn_header_hash,winner"); + for ((height, bhh), winner) in wins_epoch3.into_iter() { + println!("{},{},{}", &height, &bhh, &winner); + } + } + if argv[1] == "replay-chainstate" { if argv.len() < 7 { eprintln!("Usage: {} OLD_CHAINSTATE_PATH OLD_SORTITION_DB_PATH OLD_BURNCHAIN_DB_PATH NEW_CHAINSTATE_PATH NEW_BURNCHAIN_DB_PATH", &argv[0]); From a5bda558330c92aa2717d4013a2bf821752ff8b4 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Thu, 25 Apr 2024 10:38:41 -0700 Subject: [PATCH 13/56] feat: signer monitoring server --- Cargo.lock | 5 + libsigner/Cargo.toml | 5 + stacks-signer/Cargo.toml | 6 + stacks-signer/src/client/stacks_client.rs | 2 +- stacks-signer/src/config.rs | 44 ++++- stacks-signer/src/lib.rs | 3 + stacks-signer/src/main.rs | 5 + stacks-signer/src/monitoring/mod.rs | 162 +++++++++++++++++ stacks-signer/src/monitoring/prometheus.rs | 98 ++++++++++ stacks-signer/src/monitoring/server.rs | 197 +++++++++++++++++++++ stacks-signer/src/signer.rs | 15 ++ stacks-signer/src/tests/conf/signer-0.toml | 1 + testnet/stacks-node/Cargo.toml | 2 +- testnet/stacks-node/src/tests/signer.rs | 37 +++- 14 files changed, 578 insertions(+), 4 deletions(-) create mode 100644 stacks-signer/src/monitoring/mod.rs create mode 100644 stacks-signer/src/monitoring/prometheus.rs create mode 100644 stacks-signer/src/monitoring/server.rs diff --git a/Cargo.lock b/Cargo.lock index c1c512409..fa89992f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,9 +1921,11 @@ version = "0.0.1" dependencies = [ "clarity", "hashbrown 0.14.3", + "lazy_static", "libc", "libstackerdb", "mutants", + "prometheus", "rand 0.8.5", "rand_core 0.6.4", "secp256k1", @@ -3451,10 +3453,12 @@ dependencies = [ "clap 4.5.0", "clarity", "hashbrown 0.14.3", + "lazy_static", "libsigner", "libstackerdb", "num-traits", "polynomial", + "prometheus", "rand 0.8.5", "rand_core 0.6.4", "reqwest", @@ -3470,6 +3474,7 @@ dependencies = [ "stacks-common", "stackslib", "thiserror", + "tiny_http", "toml 0.5.11", "tracing", "tracing-subscriber", diff --git a/libsigner/Cargo.toml b/libsigner/Cargo.toml index 8a0e5fc65..7da980167 100644 --- a/libsigner/Cargo.toml +++ b/libsigner/Cargo.toml @@ -18,8 +18,10 @@ path = "./src/libsigner.rs" [dependencies] clarity = { path = "../clarity" } hashbrown = { workspace = true } +lazy_static = "1.4.0" libc = "0.2" libstackerdb = { path = "../libstackerdb" } +prometheus = { version = "0.9", optional = true } serde = "1" serde_derive = "1" serde_stacker = "0.1" @@ -50,3 +52,6 @@ sha2 = { version = "0.10", features = ["asm"] } [target.'cfg(any(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")), any(target_os = "windows")))'.dependencies] sha2 = { version = "0.10" } + +[features] +monitoring_prom = ["prometheus"] \ No newline at end of file diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml index 57b2e8080..087a0a447 100644 --- a/stacks-signer/Cargo.toml +++ b/stacks-signer/Cargo.toml @@ -24,8 +24,10 @@ backoff = "0.4" clarity = { path = "../clarity" } clap = { version = "4.1.1", features = ["derive", "env"] } hashbrown = { workspace = true } +lazy_static = "1.4.0" libsigner = { path = "../libsigner" } libstackerdb = { path = "../libstackerdb" } +prometheus = { version = "0.9", optional = true } rand_core = "0.6" reqwest = { version = "0.11.22", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = "1" @@ -37,6 +39,7 @@ slog-term = "2.6.0" stacks-common = { path = "../stacks-common" } stackslib = { path = "../stackslib" } thiserror = "1.0" +tiny_http = { version = "0.12", optional = true } toml = "0.5.6" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } @@ -60,3 +63,6 @@ features = ["arbitrary_precision", "unbounded_depth"] [dependencies.secp256k1] version = "0.24.3" features = ["serde", "recovery"] + +[features] +monitoring_prom = ["libsigner/monitoring_prom", "prometheus", "tiny_http"] \ No newline at end of file diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index cae312639..f1a4043e8 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -458,7 +458,7 @@ impl StacksClient { } /// Helper function to retrieve the account info from the stacks node for a specific address - fn get_account_entry( + pub fn get_account_entry( &self, address: &StacksAddress, ) -> Result { diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index a028d190d..720afa258 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -186,6 +186,8 @@ pub struct GlobalConfig { pub auth_password: String, /// The path to the signer's database file pub db_path: PathBuf, + /// Metrics endpoint + pub metrics_endpoint: Option, } /// Internal struct for loading up the config file @@ -221,6 +223,8 @@ struct RawConfigFile { pub auth_password: String, /// The path to the signer's database file or :memory: for an in-memory database pub db_path: String, + /// Metrics endpoint + pub metrics_endpoint: Option, } impl RawConfigFile { @@ -298,6 +302,19 @@ impl TryFrom for GlobalConfig { let sign_timeout = raw_data.sign_timeout_ms.map(Duration::from_millis); let db_path = raw_data.db_path.into(); + let metrics_endpoint = match raw_data.metrics_endpoint { + Some(endpoint) => Some( + endpoint + .to_socket_addrs() + .map_err(|_| ConfigError::BadField("endpoint".to_string(), endpoint.clone()))? + .next() + .ok_or_else(|| { + ConfigError::BadField("endpoint".to_string(), endpoint.clone()) + })?, + ), + None => None, + }; + Ok(Self { node_host: raw_data.node_host, endpoint, @@ -315,6 +332,7 @@ impl TryFrom for GlobalConfig { max_tx_fee_ustx: raw_data.max_tx_fee_ustx, auth_password: raw_data.auth_password, db_path, + metrics_endpoint, }) } } @@ -345,6 +363,10 @@ impl GlobalConfig { 0 => "default".to_string(), _ => (self.tx_fee_ustx as f64 / 1_000_000.0).to_string(), }; + let metrics_endpoint = match &self.metrics_endpoint { + Some(endpoint) => endpoint.to_string(), + None => "None".to_string(), + }; format!( r#" Stacks node host: {node_host} @@ -354,6 +376,7 @@ Public key: {public_key} Network: {network} Database path: {db_path} DKG transaction fee: {tx_fee} uSTX +Metrics endpoint: {metrics_endpoint} "#, node_host = self.node_host, endpoint = self.endpoint, @@ -361,7 +384,8 @@ DKG transaction fee: {tx_fee} uSTX public_key = StacksPublicKey::from_private(&self.stacks_private_key).to_hex(), network = self.network, db_path = self.db_path.to_str().unwrap_or_default(), - tx_fee = tx_fee + tx_fee = tx_fee, + metrics_endpoint = metrics_endpoint, ) } } @@ -384,6 +408,7 @@ pub fn build_signer_config_tomls( mut port_start: usize, max_tx_fee_ustx: Option, tx_fee_ustx: Option, + mut metrics_port_start: Option, ) -> Vec { let mut signer_config_tomls = vec![]; @@ -438,6 +463,17 @@ tx_fee_ustx = {tx_fee_ustx} ) } + if let Some(metrics_port) = metrics_port_start { + let metrics_endpoint = format!("localhost:{}", metrics_port); + signer_config_toml = format!( + r#" +{signer_config_toml} +metrics_endpoint = "{metrics_endpoint}" +"# + ); + metrics_port_start = Some(metrics_port + 1); + } + signer_config_tomls.push(signer_config_toml); } @@ -469,6 +505,7 @@ mod tests { 3000, None, None, + Some(4000), ); let config = @@ -477,6 +514,7 @@ mod tests { assert_eq!(config.auth_password, "melon"); assert!(config.max_tx_fee_ustx.is_none()); assert!(config.tx_fee_ustx.is_none()); + assert_eq!(config.metrics_endpoint, Some("localhost:4000".to_string())); } #[test] @@ -501,6 +539,7 @@ mod tests { 3000, None, None, + None, ); let config = @@ -526,6 +565,7 @@ mod tests { 3000, max_tx_fee_ustx, tx_fee_ustx, + None, ); let config = @@ -546,6 +586,7 @@ mod tests { 3000, max_tx_fee_ustx, None, + None, ); let config = @@ -570,6 +611,7 @@ mod tests { 3000, None, tx_fee_ustx, + None, ); let config = diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index 9dcd0a069..d326eef10 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -34,3 +34,6 @@ pub mod runloop; pub mod signer; /// The state module for the signer pub mod signerdb; + +/// The monitoring server for the signer +pub mod monitoring; diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 858ea9c66..3573ac1d6 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -93,6 +93,10 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); let ev = SignerEventReceiver::new(config.network.is_mainnet()); + #[cfg(feature = "monitoring_prom")] + { + stacks_signer::monitoring::start_serving_monitoring_metrics(config.clone()).ok(); + } let runloop = RunLoop::from(config); let mut signer: Signer, RunLoop, SignerEventReceiver> = Signer::new(runloop, ev, cmd_recv, res_send); @@ -305,6 +309,7 @@ fn handle_generate_files(args: GenerateFilesArgs) { 3000, None, None, + None, ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); for (i, file_contents) in signer_config_tomls.iter().enumerate() { diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs new file mode 100644 index 000000000..7a8a84656 --- /dev/null +++ b/stacks-signer/src/monitoring/mod.rs @@ -0,0 +1,162 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 . + +#[cfg(feature = "monitoring_prom")] +use slog::slog_error; +#[cfg(not(feature = "monitoring_prom"))] +use slog::slog_warn; +#[cfg(feature = "monitoring_prom")] +use stacks_common::error; +#[cfg(not(feature = "monitoring_prom"))] +use stacks_common::warn; + +use crate::config::GlobalConfig; + +#[cfg(feature = "monitoring_prom")] +mod prometheus; + +#[cfg(feature = "monitoring_prom")] +mod server; + +/// Update stacks tip height guage +#[allow(unused_variables)] +pub fn update_stacks_tip_height(height: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::STACKS_TIP_HEIGHT_GUAGE.set(height); +} + +/// Update the current reward cycle +#[allow(unused_variables)] +pub fn update_reward_cycle(reward_cycle: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::CURRENT_REWARD_CYCLE.set(reward_cycle); +} + +/// Increment the block validation responses counter +#[allow(unused_variables)] +pub fn increment_block_validation_responses(accepted: bool) { + #[cfg(feature = "monitoring_prom")] + { + let label_value = if accepted { "accepted" } else { "rejected" }; + prometheus::BLOCK_VALIDATION_RESPONSES + .with_label_values(&[label_value]) + .inc(); + } +} + +/// Increment the block responses sent counter +#[allow(unused_variables)] +pub fn increment_block_responses_sent(accepted: bool) { + #[cfg(feature = "monitoring_prom")] + { + let label_value = if accepted { "accepted" } else { "rejected" }; + prometheus::BLOCK_RESPONSES_SENT + .with_label_values(&[label_value]) + .inc(); + } +} + +/// Increment the signer inbound messages counter +#[allow(unused_variables)] +pub fn increment_signer_inbound_messages(amount: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::SIGNER_INBOUND_MESSAGES.inc_by(amount); +} + +/// Increment the coordinator inbound messages counter +#[allow(unused_variables)] +pub fn increment_coordinator_inbound_messages(amount: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::COORDINATOR_INBOUND_MESSAGES.inc_by(amount); +} + +/// Increment the number of inbound packets received +#[allow(unused_variables)] +pub fn increment_inbound_packets(amount: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::INBOUND_PACKETS_RECEIVED.inc_by(amount); +} + +/// Increment the number of commands processed +#[allow(unused_variables)] +pub fn increment_commands_processed(command_type: &str) { + #[cfg(feature = "monitoring_prom")] + prometheus::COMMANDS_PROCESSED + .with_label_values(&[command_type]) + .inc(); +} + +/// Increment the number of DKG votes submitted +#[allow(unused_variables)] +pub fn increment_dkg_votes_submitted() { + #[cfg(feature = "monitoring_prom")] + prometheus::DGK_VOTES_SUBMITTED.inc(); +} + +/// Increment the number of commands processed +#[allow(unused_variables)] +pub fn increment_operation_results(operation_type: &str) { + #[cfg(feature = "monitoring_prom")] + prometheus::OPERATION_RESULTS + .with_label_values(&[operation_type]) + .inc(); +} + +/// Increment the number of block proposals received +#[allow(unused_variables)] +pub fn increment_block_proposals_received() { + #[cfg(feature = "monitoring_prom")] + prometheus::BLOCK_PROPOSALS_RECEIVED.inc(); +} + +/// Update the stx balance of the signer +#[allow(unused_variables)] +pub fn update_signer_stx_balance(balance: i64) { + #[cfg(feature = "monitoring_prom")] + prometheus::SIGNER_STX_BALANCE.set(balance); +} + +/// Update the signer nonce metric +#[allow(unused_variables)] +pub fn update_signer_nonce(nonce: u64) { + #[cfg(feature = "monitoring_prom")] + prometheus::SIGNER_NONCE.set(nonce as i64); +} + +/// Start serving monitoring metrics. +/// This will only serve the metrics if the `monitoring_prom` feature is enabled. +#[allow(unused_variables)] +pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> { + #[cfg(feature = "monitoring_prom")] + { + if config.metrics_endpoint.is_none() { + return Ok(()); + } + let thread = std::thread::Builder::new() + .name("signer_metrics".to_string()) + .spawn(move || { + if let Err(monitoring_err) = server::MonitoringServer::start(&config) { + error!( + "Monitoring: Error starting metrics server: {:?}", + monitoring_err + ); + } + }); + } + #[cfg(not(feature = "monitoring_prom"))] + warn!("Not starting monitoring metrics server as the monitoring_prom feature is not enabled"); + Ok(()) +} diff --git a/stacks-signer/src/monitoring/prometheus.rs b/stacks-signer/src/monitoring/prometheus.rs new file mode 100644 index 000000000..885351ef1 --- /dev/null +++ b/stacks-signer/src/monitoring/prometheus.rs @@ -0,0 +1,98 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 . + +use lazy_static::lazy_static; +use prometheus::{ + gather, opts, register_int_counter, register_int_counter_vec, register_int_gauge, Encoder, + IntCounter, IntCounterVec, IntGauge, TextEncoder, +}; + +lazy_static! { + pub static ref STACKS_TIP_HEIGHT_GUAGE: IntGauge = register_int_gauge!(opts!( + "stacks_signer_stacks_node_height", + "The current height of the Stacks node" + )) + .unwrap(); + pub static ref BLOCK_VALIDATION_RESPONSES: IntCounterVec = register_int_counter_vec!( + "stacks_signer_block_validation_responses", + "The number of block validation responses. `response_type` is either 'accepted' or 'rejected'", + &["response_type"] + ) + .unwrap(); + pub static ref BLOCK_RESPONSES_SENT: IntCounterVec = register_int_counter_vec!( + "stacks_signer_block_responses_sent", + "The number of block responses sent. `response_type` is either 'accepted' or 'rejected'", + &["response_type"] + ) + .unwrap(); + pub static ref SIGNER_INBOUND_MESSAGES: IntCounter = register_int_counter!(opts!( + "stacks_signer_inbound_messages", + "The number of inbound messages received by the signer" + )) + .unwrap(); + pub static ref COORDINATOR_INBOUND_MESSAGES: IntCounter = register_int_counter!(opts!( + "stacks_signer_coordinator_inbound_messages", + "The number of inbound messages received as a coordinator" + )) + .unwrap(); + pub static ref INBOUND_PACKETS_RECEIVED: IntCounter = register_int_counter!(opts!( + "stacks_signer_inbound_packets_received", + "The number of inbound packets received by the signer" + )) + .unwrap(); + pub static ref COMMANDS_PROCESSED: IntCounterVec = register_int_counter_vec!( + "stacks_signer_commands_processed", + "The number of commands processed by the signer", + &["command_type"] + ) + .unwrap(); + pub static ref DGK_VOTES_SUBMITTED: IntCounter = register_int_counter!(opts!( + "stacks_signer_dgk_votes_submitted", + "The number of DGK votes submitted by the signer" + )) + .unwrap(); + pub static ref OPERATION_RESULTS: IntCounterVec = register_int_counter_vec!( + "stacks_signer_operation_results_dkg", + "The number of DKG operation results", + &["operation_type"] + ) + .unwrap(); + pub static ref BLOCK_PROPOSALS_RECEIVED: IntCounter = register_int_counter!(opts!( + "stacks_signer_block_proposals_received", + "The number of block proposals received by the signer" + )) + .unwrap(); + pub static ref CURRENT_REWARD_CYCLE: IntGauge = register_int_gauge!(opts!( + "stacks_signer_current_reward_cycle", + "The current reward cycle" + )).unwrap(); + pub static ref SIGNER_STX_BALANCE: IntGauge = register_int_gauge!(opts!( + "stacks_signer_stx_balance", + "The current STX balance of the signer" + )).unwrap(); + pub static ref SIGNER_NONCE: IntGauge = register_int_gauge!(opts!( + "stacks_signer_nonce", + "The current nonce of the signer" + )).unwrap(); +} + +pub fn gather_metrics_string() -> String { + let mut buffer = Vec::new(); + let encoder = TextEncoder::new(); + let metrics_families = gather(); + encoder.encode(&metrics_families, &mut buffer).unwrap(); + String::from_utf8(buffer).unwrap() +} diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs new file mode 100644 index 000000000..cfcd0b55b --- /dev/null +++ b/stacks-signer/src/monitoring/server.rs @@ -0,0 +1,197 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 . + +use std::net::SocketAddr; +use std::time::Instant; + +use clarity::util::hash::to_hex; +use clarity::util::secp256k1::Secp256k1PublicKey; +use slog::{slog_debug, slog_error, slog_info, slog_warn}; +use stacks_common::{debug, error, info, warn}; +use tiny_http::{Response as HttpResponse, Server as HttpServer}; + +use super::{update_reward_cycle, update_signer_stx_balance}; +use crate::client::{ClientError, StacksClient}; +use crate::config::{GlobalConfig, Network}; +use crate::monitoring::prometheus::gather_metrics_string; +use crate::monitoring::{update_signer_nonce, update_stacks_tip_height}; + +#[derive(Debug)] +/// Monitoring server errors +pub enum MonitoringError { + /// Already bound to an address + AlreadyBound, + /// Server terminated + Terminated, + /// No endpoint configured + EndpointNotConfigured, + /// Error fetching metrics from stacks node + FetchError(ClientError), +} + +/// Metrics and monitoring server +pub struct MonitoringServer { + http_server: HttpServer, + local_addr: SocketAddr, + stacks_client: StacksClient, + last_metrics_poll: Instant, + network: Network, + public_key: Secp256k1PublicKey, +} + +impl MonitoringServer { + pub fn new( + http_server: HttpServer, + local_addr: SocketAddr, + stacks_client: StacksClient, + network: Network, + public_key: Secp256k1PublicKey, + ) -> Self { + Self { + http_server, + local_addr, + stacks_client, + last_metrics_poll: Instant::now(), + network, + public_key, + } + } + + /// Start and run the metrics server + pub fn start(config: &GlobalConfig) -> Result<(), MonitoringError> { + let Some(endpoint) = config.metrics_endpoint else { + return Err(MonitoringError::EndpointNotConfigured); + }; + let stacks_client = StacksClient::from(config); + let http_server = HttpServer::http(endpoint).map_err(|_| MonitoringError::AlreadyBound)?; + let public_key = Secp256k1PublicKey::from_private(&config.stacks_private_key); + let mut server = MonitoringServer::new( + http_server, + endpoint, + stacks_client, + config.network.clone(), + public_key, + ); + server.update_metrics()?; + server.main_loop() + } + + // /// Start and run the metrics server + // pub fn run(endpoint: SocketAddr, stacks_client: StacksClient) -> Result<(), MonitoringError> { + // let http_server = HttpServer::http(endpoint).map_err(|_| MonitoringError::AlreadyBound)?; + // let mut server = PrometheusMetrics::new(http_server, endpoint, stacks_client); + // server.main_loop() + // } + + /// Main listener loop of metrics server + pub fn main_loop(&mut self) -> Result<(), MonitoringError> { + info!("{}: Starting Prometheus metrics server", self); + loop { + if let Err(err) = self.refresh_metrics() { + error!("Monitoring: Error refreshing metrics: {:?}", err); + } + let request = match self.http_server.recv() { + Ok(request) => request, + Err(err) => { + error!("Monitoring: Error receiving request: {:?}", err); + return Err(MonitoringError::Terminated); + } + }; + + debug!("{}: received request {}", self, request.url()); + + if request.url() == "/metrics" { + let response = HttpResponse::from_string(gather_metrics_string()); + request.respond(response).expect("Failed to send response"); + continue; + } + + // unknown request, return 200 ok + request + .respond(HttpResponse::from_string(self.get_info_response())) + .expect("Failed to respond to request"); + } + } + + /// Check to see if metrics need to be refreshed + fn refresh_metrics(&mut self) -> Result<(), MonitoringError> { + let now = Instant::now(); + if now.duration_since(self.last_metrics_poll).as_secs() > 60 { + self.last_metrics_poll = now; + self.update_metrics()?; + } + Ok(()) + } + + /// Update metrics by making RPC calls to the Stacks node + fn update_metrics(&self) -> Result<(), MonitoringError> { + debug!("{}: Updating metrics", self); + let peer_info = self + .stacks_client + .get_peer_info() + .map_err(|e| MonitoringError::FetchError(e))?; + if let Ok(height) = i64::try_from(peer_info.stacks_tip_height) { + update_stacks_tip_height(height); + } else { + warn!( + "Failed to parse stacks tip height: {}", + peer_info.stacks_tip_height + ); + } + let pox_info = self + .stacks_client + .get_pox_data() + .map_err(|e| MonitoringError::FetchError(e))?; + if let Ok(reward_cycle) = i64::try_from(pox_info.reward_cycle_id) { + update_reward_cycle(reward_cycle); + } + let signer_stx_addr = self.stacks_client.get_signer_address(); + let account_entry = self + .stacks_client + .get_account_entry(&signer_stx_addr) + .map_err(|e| MonitoringError::FetchError(e))?; + let balance = i64::from_str_radix(&account_entry.balance[2..], 16).map_err(|e| { + MonitoringError::FetchError(ClientError::MalformedClarityValue(format!( + "Failed to parse balance: {} with err: {}", + &account_entry.balance, e, + ))) + })?; + if let Ok(nonce) = u64::try_from(account_entry.nonce) { + update_signer_nonce(nonce); + } else { + warn!("Failed to parse nonce: {}", account_entry.nonce); + } + update_signer_stx_balance(balance); + Ok(()) + } + + /// Build a JSON response for non-metrics requests + fn get_info_response(&self) -> String { + // let public_key = Secp256k1PublicKey::from_private(&self.stacks_client.publ); + serde_json::to_string(&serde_json::json!({ + "signerPublicKey": to_hex(&self.public_key.to_bytes_compressed()), + "network": self.network.to_string(), + "stxAddress": self.stacks_client.get_signer_address().to_string(), + })) + .expect("Failed to serialize JSON") + } +} + +impl std::fmt::Display for MonitoringServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Signer monitoring server ({})", self.local_addr) + } +} diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 4908965c7..d3bf2146e 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -413,6 +413,7 @@ impl Signer { fn execute_command(&mut self, stacks_client: &StacksClient, command: &Command) { match command { Command::Dkg => { + crate::monitoring::increment_commands_processed(&"dkg"); if self.approved_aggregate_public_key.is_some() { debug!("Reward cycle #{} Signer #{}: Already have an aggregate key. Ignoring DKG command.", self.reward_cycle, self.signer_id); return; @@ -449,6 +450,7 @@ impl Signer { is_taproot, merkle_root, } => { + crate::monitoring::increment_commands_processed(&"sign"); if self.approved_aggregate_public_key.is_none() { debug!("{self}: Cannot sign a block without an approved aggregate public key. Ignore it."); return; @@ -548,6 +550,7 @@ impl Signer { ) { let mut block_info = match block_validate_response { BlockValidateResponse::Ok(block_validate_ok) => { + crate::monitoring::increment_block_validation_responses(true); let signer_signature_hash = block_validate_ok.signer_signature_hash; // For mutability reasons, we need to take the block_info out of the map and add it back after processing let mut block_info = match self @@ -578,6 +581,7 @@ impl Signer { block_info } BlockValidateResponse::Reject(block_validate_reject) => { + crate::monitoring::increment_block_validation_responses(false); let signer_signature_hash = block_validate_reject.signer_signature_hash; let mut block_info = match self .signer_db @@ -680,6 +684,9 @@ impl Signer { packets: &[Packet], current_reward_cycle: u64, ) { + if let Ok(packets_len) = packets.len().try_into() { + crate::monitoring::increment_inbound_packets(packets_len); + } let signer_outbound_messages = self .state_machine .process_inbound_messages(packets) @@ -1036,20 +1043,25 @@ impl Signer { // Signers only every trigger non-taproot signing rounds over blocks. Ignore SignTaproot results match operation_result { OperationResult::Sign(signature) => { + crate::monitoring::increment_operation_results(&"sign"); debug!("{self}: Received signature result"); self.process_signature(signature); } OperationResult::SignTaproot(_) => { + crate::monitoring::increment_operation_results(&"sign_taproot"); debug!("{self}: Received a signature result for a taproot signature. Nothing to broadcast as we currently sign blocks with a FROST signature."); } OperationResult::Dkg(aggregate_key) => { + crate::monitoring::increment_operation_results(&"dkg"); self.process_dkg(stacks_client, aggregate_key); } OperationResult::SignError(e) => { + crate::monitoring::increment_operation_results(&"sign_error"); warn!("{self}: Received a Sign error: {e:?}"); self.process_sign_error(e); } OperationResult::DkgError(e) => { + crate::monitoring::increment_operation_results(&"dkg_error"); warn!("{self}: Received a DKG error: {e:?}"); // TODO: process these errors and track malicious signers to report } @@ -1200,6 +1212,7 @@ impl Signer { debug!("{self}: Received a DKG result, but are in an unsupported epoch. Do not broadcast the transaction ({}).", new_transaction.txid()); return Ok(()); } + crate::monitoring::increment_dkg_votes_submitted(); // For all Pox-4 epochs onwards, broadcast the results also to stackerDB for other signers/miners to observe signer_transactions.push(new_transaction); let signer_message = SignerMessage::Transactions(signer_transactions); @@ -1219,9 +1232,11 @@ impl Signer { }; let block_submission = if block_vote.rejected { + crate::monitoring::increment_block_responses_sent(false); // We signed a rejection message. Return a rejection message BlockResponse::rejected(block_vote.signer_signature_hash, signature.clone()) } else { + crate::monitoring::increment_block_responses_sent(true); // we agreed to sign the block hash. Return an approval message BlockResponse::accepted(block_vote.signer_signature_hash, signature.clone()) }; diff --git a/stacks-signer/src/tests/conf/signer-0.toml b/stacks-signer/src/tests/conf/signer-0.toml index 32183e0e7..19002c191 100644 --- a/stacks-signer/src/tests/conf/signer-0.toml +++ b/stacks-signer/src/tests/conf/signer-0.toml @@ -4,3 +4,4 @@ endpoint = "localhost:30000" network = "testnet" auth_password = "12345" db_path = ":memory:" +metrics_endpoint = "0.0.0.0:9090" \ No newline at end of file diff --git a/testnet/stacks-node/Cargo.toml b/testnet/stacks-node/Cargo.toml index 72cc8d249..bceb484cd 100644 --- a/testnet/stacks-node/Cargo.toml +++ b/testnet/stacks-node/Cargo.toml @@ -62,7 +62,7 @@ name = "stacks-events" path = "src/stacks_events.rs" [features] -monitoring_prom = ["stacks/monitoring_prom"] +monitoring_prom = ["stacks/monitoring_prom", "libsigner/monitoring_prom"] slog_json = ["stacks/slog_json", "stacks-common/slog_json", "clarity/slog_json"] prod-genesis-chainstate = [] default = [] diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 68515d130..c6edf9bce 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -125,6 +125,7 @@ impl SignerTest { 3000, Some(100_000), None, + Some(9000), ); let mut running_signers = Vec::new(); @@ -511,6 +512,23 @@ impl SignerTest { entries.public_keys } + fn get_signer_metrics(&self) -> String { + #[cfg(feature = "monitoring_prom")] + { + let client = reqwest::blocking::Client::new(); + let res = client + .get("http://localhost:9000/metrics") + .send() + .unwrap() + .text() + .unwrap(); + + return res; + } + #[cfg(not(feature = "monitoring_prom"))] + return String::new(); + } + fn generate_invalid_transactions(&self) -> Vec { let host = self .running_nodes @@ -741,6 +759,7 @@ impl SignerTest { 3000 + signer_idx, Some(100_000), None, + Some(9000 + signer_idx), ) .pop() .unwrap(); @@ -782,6 +801,10 @@ fn spawn_signer( let config = SignerConfig::load_from_str(data).unwrap(); let ev = SignerEventReceiver::new(config.network.is_mainnet()); let endpoint = config.endpoint; + #[cfg(feature = "monitoring_prom")] + { + stacks_signer::monitoring::start_serving_monitoring_metrics(config.clone()).ok(); + } let runloop: stacks_signer::runloop::RunLoop = stacks_signer::runloop::RunLoop::from(config); let mut signer: Signer< RunLoopCommand, @@ -1329,7 +1352,8 @@ fn stackerdb_block_proposal() { .init(); info!("------------------------- Test Setup -------------------------"); - let mut signer_test = SignerTest::new(5); + let num_signers = 5; + let mut signer_test = SignerTest::new(num_signers); let timeout = Duration::from_secs(200); let short_timeout = Duration::from_secs(30); @@ -1347,6 +1371,17 @@ fn stackerdb_block_proposal() { .0 .verify(&key, proposed_signer_signature_hash.as_bytes())); + // Test prometheus metrics response + #[cfg(feature = "monitoring_prom")] + { + let metrics_response = signer_test.get_signer_metrics(); + + // Because 5 signers are running in the same process, the prometheus metrics + // are incremented once for every signer. This is why we expect the metric to be + // `5`, even though there is only one block proposed. + let expected_result = format!("stacks_signer_block_proposals_received {}", num_signers); + assert!(metrics_response.contains(&expected_result)); + } signer_test.shutdown(); } From 544fcf63cca3a4b2f4d179c5d9a705d5a70c65e1 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 29 Apr 2024 14:55:28 -0700 Subject: [PATCH 14/56] crc: 404 for unknown monitoring request, only increment dkg after submission --- stacks-signer/src/monitoring/server.rs | 19 +++++++++++++++++-- stacks-signer/src/signer.rs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs index cfcd0b55b..8eb03c37a 100644 --- a/stacks-signer/src/monitoring/server.rs +++ b/stacks-signer/src/monitoring/server.rs @@ -119,9 +119,24 @@ impl MonitoringServer { continue; } - // unknown request, return 200 ok + if request.url() == "/info" { + request + .respond(HttpResponse::from_string(self.get_info_response())) + .expect("Failed to respond to request"); + continue; + } + + // return 200 OK for "/" + if request.url() == "/" { + request + .respond(HttpResponse::from_string("OK")) + .expect("Failed to respond to request"); + continue; + } + + // unknown request, return 404 request - .respond(HttpResponse::from_string(self.get_info_response())) + .respond(HttpResponse::from_string("Not Found").with_status_code(404)) .expect("Failed to respond to request"); } } diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index d3bf2146e..4f0b6d254 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -1212,11 +1212,11 @@ impl Signer { debug!("{self}: Received a DKG result, but are in an unsupported epoch. Do not broadcast the transaction ({}).", new_transaction.txid()); return Ok(()); } - crate::monitoring::increment_dkg_votes_submitted(); // For all Pox-4 epochs onwards, broadcast the results also to stackerDB for other signers/miners to observe signer_transactions.push(new_transaction); let signer_message = SignerMessage::Transactions(signer_transactions); self.stackerdb.send_message_with_retry(signer_message)?; + crate::monitoring::increment_dkg_votes_submitted(); info!("{self}: Broadcasted DKG vote transaction ({txid}) to stacker DB"); Ok(()) } From 9154bb7935ac3f8af44c612215bf1b227a3b0904 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 29 Apr 2024 16:41:53 -0700 Subject: [PATCH 15/56] feat: histogram for signer -> node RPC latency --- stacks-signer/src/client/stacks_client.rs | 40 ++++++++++++++++++++++ stacks-signer/src/monitoring/mod.rs | 12 +++++++ stacks-signer/src/monitoring/prometheus.rs | 11 ++++-- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index f1a4043e8..a2dfc09ea 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -207,6 +207,9 @@ impl StacksClient { estimated_len: Some(tx.tx_len()), transaction_payload: to_hex(&tx.payload.serialize_to_vec()), }; + #[cfg(feature = "monitoring_prom")] + let timer = + crate::monitoring::new_rpc_call_timer(&self.fees_transaction_path(), &self.http_origin); let send_request = || { self.stacks_node_client .post(self.fees_transaction_path()) @@ -215,6 +218,8 @@ impl StacksClient { .send() .map_err(backoff::Error::transient) }; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); let response = retry_with_exponential_backoff(send_request)?; if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -268,6 +273,9 @@ impl StacksClient { block, chain_id: self.chain_id, }; + #[cfg(feature = "monitoring_prom")] + let timer = + crate::monitoring::new_rpc_call_timer(&self.block_proposal_path(), &self.http_origin); let send_request = || { self.stacks_node_client .post(self.block_proposal_path()) @@ -279,6 +287,8 @@ impl StacksClient { }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -355,6 +365,9 @@ impl StacksClient { /// Get the current peer info data from the stacks node pub fn get_peer_info(&self) -> Result { debug!("Getting stacks node info..."); + #[cfg(feature = "monitoring_prom")] + let timer = + crate::monitoring::new_rpc_call_timer(&self.core_info_path(), &self.http_origin); let send_request = || { self.stacks_node_client .get(self.core_info_path()) @@ -362,6 +375,8 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -402,6 +417,11 @@ impl StacksClient { reward_cycle: u64, ) -> Result>, ClientError> { debug!("Getting reward set for reward cycle {reward_cycle}..."); + #[cfg(feature = "monitoring_prom")] + let timer = crate::monitoring::new_rpc_call_timer( + &self.reward_set_path(reward_cycle), + &self.http_origin, + ); let send_request = || { self.stacks_node_client .get(self.reward_set_path(reward_cycle)) @@ -409,6 +429,8 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -419,6 +441,8 @@ impl StacksClient { /// Retreive the current pox data from the stacks node pub fn get_pox_data(&self) -> Result { debug!("Getting pox data..."); + #[cfg(feature = "monitoring_prom")] + let timer = crate::monitoring::new_rpc_call_timer(&self.pox_path(), &self.http_origin); let send_request = || { self.stacks_node_client .get(self.pox_path()) @@ -426,6 +450,8 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -463,6 +489,9 @@ impl StacksClient { address: &StacksAddress, ) -> Result { debug!("Getting account info..."); + #[cfg(feature = "monitoring_prom")] + let timer = + crate::monitoring::new_rpc_call_timer(&self.accounts_path(address), &self.http_origin); let send_request = || { self.stacks_node_client .get(self.accounts_path(address)) @@ -470,6 +499,8 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -536,6 +567,9 @@ impl StacksClient { pub fn submit_transaction(&self, tx: &StacksTransaction) -> Result { let txid = tx.txid(); let tx = tx.serialize_to_vec(); + #[cfg(feature = "monitoring_prom")] + let timer = + crate::monitoring::new_rpc_call_timer(&self.transaction_path(), &self.http_origin); let send_request = || { self.stacks_node_client .post(self.transaction_path()) @@ -548,6 +582,8 @@ impl StacksClient { }) }; let response = retry_with_exponential_backoff(send_request)?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } @@ -576,12 +612,16 @@ impl StacksClient { let body = json!({"sender": self.stacks_address.to_string(), "arguments": args}).to_string(); let path = self.read_only_path(contract_addr, contract_name, function_name); + #[cfg(feature = "monitoring_prom")] + let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin); let response = self .stacks_node_client .post(path) .header("Content-Type", "application/json") .body(body) .send()?; + #[cfg(feature = "monitoring_prom")] + timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs index 7a8a84656..9a1411224 100644 --- a/stacks-signer/src/monitoring/mod.rs +++ b/stacks-signer/src/monitoring/mod.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#[cfg(feature = "monitoring_prom")] +use ::prometheus::HistogramTimer; #[cfg(feature = "monitoring_prom")] use slog::slog_error; #[cfg(not(feature = "monitoring_prom"))] @@ -136,6 +138,16 @@ pub fn update_signer_nonce(nonce: u64) { prometheus::SIGNER_NONCE.set(nonce as i64); } +/// Start a new RPC call timer. +/// The `origin` parameter is the base path of the RPC call, e.g. `http://node.com`. +/// The `origin` parameter is removed from `full_path` when storing in prometheus. +#[cfg(feature = "monitoring_prom")] +pub fn new_rpc_call_timer(full_path: &str, origin: &String) -> HistogramTimer { + let path = &full_path[origin.len()..]; + let histogram = prometheus::SIGNER_RPC_CALL_LATENCIES_HISTOGRAM.with_label_values(&[path]); + histogram.start_timer() +} + /// Start serving monitoring metrics. /// This will only serve the metrics if the `monitoring_prom` feature is enabled. #[allow(unused_variables)] diff --git a/stacks-signer/src/monitoring/prometheus.rs b/stacks-signer/src/monitoring/prometheus.rs index 885351ef1..18226ae9f 100644 --- a/stacks-signer/src/monitoring/prometheus.rs +++ b/stacks-signer/src/monitoring/prometheus.rs @@ -16,8 +16,9 @@ use lazy_static::lazy_static; use prometheus::{ - gather, opts, register_int_counter, register_int_counter_vec, register_int_gauge, Encoder, - IntCounter, IntCounterVec, IntGauge, TextEncoder, + gather, histogram_opts, opts, register_histogram_vec, register_int_counter, + register_int_counter_vec, register_int_gauge, Encoder, HistogramVec, IntCounter, IntCounterVec, + IntGauge, TextEncoder, }; lazy_static! { @@ -87,6 +88,12 @@ lazy_static! { "stacks_signer_nonce", "The current nonce of the signer" )).unwrap(); + + pub static ref SIGNER_RPC_CALL_LATENCIES_HISTOGRAM: HistogramVec = register_histogram_vec!(histogram_opts!( + "stacks_signer_node_rpc_call_latencies_histogram", + "Time (seconds) measuring round-trip RPC call latency to the Stacks node" + // Will use DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] by default + ), &["path"]).unwrap(); } pub fn gather_metrics_string() -> String { From 7318f5fd74686b9d1d8cc61f7c6d48b5c7fb4c84 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 29 Apr 2024 16:44:27 -0700 Subject: [PATCH 16/56] fix: typo spelling "gauge" --- stacks-signer/src/monitoring/mod.rs | 4 ++-- stacks-signer/src/monitoring/prometheus.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs index 9a1411224..3674685da 100644 --- a/stacks-signer/src/monitoring/mod.rs +++ b/stacks-signer/src/monitoring/mod.rs @@ -33,11 +33,11 @@ mod prometheus; #[cfg(feature = "monitoring_prom")] mod server; -/// Update stacks tip height guage +/// Update stacks tip height gauge #[allow(unused_variables)] pub fn update_stacks_tip_height(height: i64) { #[cfg(feature = "monitoring_prom")] - prometheus::STACKS_TIP_HEIGHT_GUAGE.set(height); + prometheus::STACKS_TIP_HEIGHT_GAUGE.set(height); } /// Update the current reward cycle diff --git a/stacks-signer/src/monitoring/prometheus.rs b/stacks-signer/src/monitoring/prometheus.rs index 18226ae9f..c78db1299 100644 --- a/stacks-signer/src/monitoring/prometheus.rs +++ b/stacks-signer/src/monitoring/prometheus.rs @@ -22,7 +22,7 @@ use prometheus::{ }; lazy_static! { - pub static ref STACKS_TIP_HEIGHT_GUAGE: IntGauge = register_int_gauge!(opts!( + pub static ref STACKS_TIP_HEIGHT_GAUGE: IntGauge = register_int_gauge!(opts!( "stacks_signer_stacks_node_height", "The current height of the Stacks node" )) From 24a0e973e7ae2ae22ec054bcd25236f6e0ae2086 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 29 Apr 2024 16:56:01 -0700 Subject: [PATCH 17/56] feat: add heartbeat check to signer monitor --- stacks-signer/src/monitoring/server.rs | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs index 8eb03c37a..4330f2f8d 100644 --- a/stacks-signer/src/monitoring/server.rs +++ b/stacks-signer/src/monitoring/server.rs @@ -50,6 +50,8 @@ pub struct MonitoringServer { last_metrics_poll: Instant, network: Network, public_key: Secp256k1PublicKey, + stacks_node_client: reqwest::blocking::Client, + stacks_node_origin: String, } impl MonitoringServer { @@ -59,6 +61,7 @@ impl MonitoringServer { stacks_client: StacksClient, network: Network, public_key: Secp256k1PublicKey, + stacks_node_origin: String, ) -> Self { Self { http_server, @@ -67,6 +70,8 @@ impl MonitoringServer { last_metrics_poll: Instant::now(), network, public_key, + stacks_node_client: reqwest::blocking::Client::new(), + stacks_node_origin, } } @@ -84,6 +89,7 @@ impl MonitoringServer { stacks_client, config.network.clone(), public_key, + format!("http://{}", config.node_host), ); server.update_metrics()?; server.main_loop() @@ -134,6 +140,19 @@ impl MonitoringServer { continue; } + // Run heartbeat check to test connection to the node + if request.url() == "/heartbeat" { + let (msg, status) = if self.heartbeat() { + ("OK", 200) + } else { + ("Failed", 500) + }; + request + .respond(HttpResponse::from_string(msg).with_status_code(status)) + .expect("Failed to respond to request"); + continue; + } + // unknown request, return 404 request .respond(HttpResponse::from_string("Not Found").with_status_code(404)) @@ -203,6 +222,29 @@ impl MonitoringServer { })) .expect("Failed to serialize JSON") } + + /// Poll the Stacks node's `v2/info` endpoint to validate the connection + fn heartbeat(&self) -> bool { + let url = format!("{}/v2/info", self.stacks_node_origin); + let response = self.stacks_node_client.get(&url).send(); + match response { + Ok(response) => { + if response.status().is_success() { + true + } else { + warn!( + "Monitoring: Heartbeat failed with status: {}", + response.status() + ); + false + } + } + Err(err) => { + warn!("Monitoring: Heartbeat failed with error: {:?}", err); + false + } + } + } } impl std::fmt::Display for MonitoringServer { From 3b6df3ec9041366328a829d171b52673429d02bb Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:21:18 -0400 Subject: [PATCH 18/56] fix: format --- stackslib/src/burnchains/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index ca41ccae0..23dc50f62 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -37,9 +37,11 @@ use self::bitcoin::{ Error as btc_error, }; use crate::chainstate::burn::distribution::BurnSamplePoint; -use crate::chainstate::burn::operations::leader_block_commit::OUTPUTS_PER_COMMIT; +use crate::chainstate::burn::operations::leader_block_commit::{ + MissedBlockCommit, OUTPUTS_PER_COMMIT, +}; use crate::chainstate::burn::operations::{ - BlockstackOperationType, Error as op_error, LeaderKeyRegisterOp, + BlockstackOperationType, Error as op_error, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::stacks::address::PoxAddress; use crate::chainstate::stacks::boot::{POX_1_NAME, POX_2_NAME, POX_3_NAME, POX_4_NAME}; @@ -48,9 +50,6 @@ use crate::core::*; use crate::net::neighbors::MAX_NEIGHBOR_BLOCK_DELAY; use crate::util_lib::db::Error as db_error; -use crate::chainstate::burn::operations::leader_block_commit::MissedBlockCommit; -use crate::chainstate::burn::operations::LeaderBlockCommitOp; - /// This module contains drivers and types for all burn chains we support. pub mod affirmation; pub mod bitcoin; From 3194d52e7a61b5fad316325d4be66962e2d10ca3 Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:21:29 -0400 Subject: [PATCH 19/56] fix: format --- stackslib/src/chainstate/burn/atc.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index 68716acf0..ac35f2c65 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::stacks_common::util::uint::BitArray; use stacks_common::util::uint::Uint256; +use crate::stacks_common::util::uint::BitArray; + /// A fixed-point numerical representation for ATC. THe integer and fractional parts are both 64 /// bits. /// @@ -108,11 +109,12 @@ impl AtcRational { #[cfg(test)] mod test { - use crate::chainstate::burn::atc::AtcRational; - use crate::chainstate::burn::BlockSnapshot; use stacks_common::util::hash::to_hex; use stacks_common::util::uint::Uint256; + use crate::chainstate::burn::atc::AtcRational; + use crate::chainstate::burn::BlockSnapshot; + impl AtcRational { /// Convert to f64, and panic on conversion failure pub fn to_f64(&self) -> f64 { From cb0c9073a343950bdf435e993b9e34d40c7e815c Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:21:43 -0400 Subject: [PATCH 20/56] feat: add a `dryrun` option that's off by default, but permits testing out making snapshots (but not saving them) --- stackslib/src/chainstate/burn/db/sortdb.rs | 63 +++++++++++++++------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index a505e1330..9c9e851ca 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -770,6 +770,7 @@ const SORTITION_DB_INDEXES: &'static [&'static str] = &[ pub struct SortitionDB { pub readwrite: bool, + pub dryrun: bool, pub marf: MARF, pub first_block_height: u64, pub first_burn_header_hash: BurnchainHeaderHash, @@ -781,6 +782,7 @@ pub struct SortitionDB { pub struct SortitionDBTxContext { pub first_block_height: u64, pub pox_constants: PoxConstants, + pub dryrun: bool, } #[derive(Clone)] @@ -788,6 +790,7 @@ pub struct SortitionHandleContext { pub first_block_height: u64, pub pox_constants: PoxConstants, pub chain_tip: SortitionId, + pub dryrun: bool, } pub type SortitionDBConn<'a> = IndexDBConn<'a, SortitionDBTxContext, SortitionId>; @@ -1130,6 +1133,7 @@ impl<'a> SortitionHandleTx<'a> { chain_tip: parent_chain_tip.clone(), first_block_height: conn.first_block_height, pox_constants: conn.pox_constants.clone(), + dryrun: conn.dryrun, }, ); @@ -2019,6 +2023,7 @@ impl<'a> SortitionHandleConn<'a> { chain_tip: chain_tip.clone(), first_block_height: connection.context.first_block_height, pox_constants: connection.context.pox_constants.clone(), + dryrun: connection.context.dryrun, }, index: &connection.index, }) @@ -2586,6 +2591,7 @@ impl SortitionDB { SortitionDBTxContext { first_block_height: self.first_block_height, pox_constants: self.pox_constants.clone(), + dryrun: self.dryrun, }, ); Ok(index_tx) @@ -2598,6 +2604,7 @@ impl SortitionDB { SortitionDBTxContext { first_block_height: self.first_block_height, pox_constants: self.pox_constants.clone(), + dryrun: self.dryrun, }, ) } @@ -2609,6 +2616,7 @@ impl SortitionDB { first_block_height: self.first_block_height, chain_tip: chain_tip.clone(), pox_constants: self.pox_constants.clone(), + dryrun: self.dryrun, }, ) } @@ -2620,13 +2628,13 @@ impl SortitionDB { if !self.readwrite { return Err(db_error::ReadOnly); } - Ok(SortitionHandleTx::new( &mut self.marf, SortitionHandleContext { first_block_height: self.first_block_height, chain_tip: chain_tip.clone(), pox_constants: self.pox_constants.clone(), + dryrun: self.dryrun, }, )) } @@ -2666,6 +2674,7 @@ impl SortitionDB { path: path.to_string(), marf, readwrite, + dryrun: false, pox_constants, first_block_height, first_burn_header_hash, @@ -2723,6 +2732,7 @@ impl SortitionDB { path: path.to_string(), marf, readwrite, + dryrun: false, first_block_height, pox_constants, first_burn_header_hash: first_burn_hash.clone(), @@ -3350,6 +3360,7 @@ impl SortitionDB { path: path.to_string(), marf, readwrite: true, + dryrun: false, first_block_height: migrator.get_burnchain().first_block_height, first_burn_header_hash: migrator.get_burnchain().first_block_hash.clone(), pox_constants: migrator.get_burnchain().pox_constants.clone(), @@ -3555,6 +3566,7 @@ impl<'a> SortitionDBConn<'a> { first_block_height: self.context.first_block_height.clone(), chain_tip: chain_tip.clone(), pox_constants: self.context.pox_constants.clone(), + dryrun: self.context.dryrun, }, } } @@ -4030,6 +4042,7 @@ impl SortitionDB { next_pox_info: Option, announce_to: F, ) -> Result<(BlockSnapshot, BurnchainStateTransition), BurnchainError> { + let dryrun = self.dryrun; let parent_sort_id = self .get_sortition_id(&burn_header.parent_block_hash, from_tip)? .ok_or_else(|| { @@ -4109,14 +4122,19 @@ impl SortitionDB { initial_mining_bonus, )?; - sortition_db_handle.store_transition_ops(&new_snapshot.0.sortition_id, &new_snapshot.1)?; + if !dryrun { + sortition_db_handle + .store_transition_ops(&new_snapshot.0.sortition_id, &new_snapshot.1)?; + } announce_to(reward_set_info); - // commit everything! - sortition_db_handle.commit().expect( - "Failed to commit to sortition db after announcing reward set info, state corrupted.", - ); + if !dryrun { + // commit everything! + sortition_db_handle.commit().expect( + "Failed to commit to sortition db after announcing reward set info, state corrupted.", + ); + } Ok((new_snapshot.0, new_snapshot.1)) } @@ -5182,6 +5200,11 @@ impl<'a> SortitionHandleTx<'a> { sn.canonical_stacks_tip_consensus_hash = parent_sn.canonical_stacks_tip_consensus_hash; } + if self.context.dryrun { + // don't do any inserts + return Ok(root_hash); + } + self.insert_block_snapshot(&sn, pox_payout)?; for block_op in block_ops { @@ -5989,19 +6012,23 @@ impl<'a> SortitionHandleTx<'a> { vec![] }; - // commit to all newly-arrived blocks - let (mut block_arrival_keys, mut block_arrival_values) = - self.process_new_block_arrivals(parent_snapshot)?; - keys.append(&mut block_arrival_keys); - values.append(&mut block_arrival_values); - // store each indexed field - let root_hash = self.put_indexed_all( - &parent_snapshot.sortition_id, - &snapshot.sortition_id, - &keys, - &values, - )?; + let root_hash = if !self.context.dryrun { + // commit to all newly-arrived blocks + let (mut block_arrival_keys, mut block_arrival_values) = + self.process_new_block_arrivals(parent_snapshot)?; + keys.append(&mut block_arrival_keys); + values.append(&mut block_arrival_values); + + self.put_indexed_all( + &parent_snapshot.sortition_id, + &snapshot.sortition_id, + &keys, + &values, + )? + } else { + TrieHash([0x00; 32]) + }; // pox payout addrs must include burn addresses let num_pox_payouts = self.get_num_pox_payouts(snapshot.block_height); From 4f44917193a8941fe71e6454536bdd7b2bf59df9 Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:22:10 -0400 Subject: [PATCH 21/56] fix: count the last block-commit in frequency --- stackslib/src/chainstate/burn/distribution.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/stackslib/src/chainstate/burn/distribution.rs b/stackslib/src/chainstate/burn/distribution.rs index 9f0b5ec1b..5b6db6397 100644 --- a/stackslib/src/chainstate/burn/distribution.rs +++ b/stackslib/src/chainstate/burn/distribution.rs @@ -294,6 +294,17 @@ impl BurnSamplePoint { }; let burns = cmp::min(median_burn, most_recent_burn); + + let frequency = linked_commits.iter().fold(0u8, |count, commit_opt| { + if commit_opt.is_some() { + count + .checked_add(1) + .expect("infallable -- commit window exceeds u8::MAX") + } else { + count + } + }); + let candidate = if let LinkedCommitIdentifier::Valid(op) = linked_commits.remove(0).unwrap().op { @@ -307,18 +318,9 @@ impl BurnSamplePoint { "txid" => %candidate.txid.to_string(), "most_recent_burn" => %most_recent_burn, "median_burn" => %median_burn, + "frequency" => frequency, "all_burns" => %format!("{:?}", all_burns)); - let frequency = linked_commits.iter().fold(0u8, |count, commit_opt| { - if commit_opt.is_some() { - count - .checked_add(1) - .expect("infallable -- commit window exceeds u8::MAX") - } else { - count - } - }); - BurnSamplePoint { burns, median_burn, From 7d6b6ead325e414a3c7abda009dbacb221051afd Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:22:23 -0400 Subject: [PATCH 22/56] fix: only check if the miner is active if the search window is greater than 1 (i.e. we're not in the prepare phase) --- stackslib/src/chainstate/burn/sortition.rs | 23 ++++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 186acfdca..61207c506 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -26,10 +26,11 @@ use stacks_common::util::log; use stacks_common::util::uint::{BitArray, Uint256, Uint512}; use crate::burnchains::{ - Address, Burnchain, BurnchainBlock, BurnchainBlockHeader, BurnchainStateTransition, PublicKey, - Txid, + Address, Burnchain, BurnchainBlock, BurnchainBlockHeader, BurnchainSigner, + BurnchainStateTransition, PublicKey, Txid, }; -use crate::chainstate::burn::db::sortdb::SortitionHandleTx; +use crate::chainstate::burn::atc::{AtcRational, ATC_LOOKUP}; +use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleTx}; use crate::chainstate::burn::distribution::BurnSamplePoint; use crate::chainstate::burn::operations::{ BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, @@ -43,12 +44,6 @@ use crate::chainstate::stacks::index::{ClarityMarfTrieId, MarfTrieId, TrieHashEx use crate::core::*; use crate::util_lib::db::Error as db_error; -use crate::chainstate::burn::db::sortdb::SortitionDB; - -use crate::burnchains::BurnchainSigner; - -use crate::chainstate::burn::atc::{AtcRational, ATC_LOOKUP}; - impl BlockSnapshot { /// Creates an "empty" (i.e. zeroed out) BlockSnapshot, to make a basis for creating /// `BlockSnapshot` with a few key fields filled. @@ -302,7 +297,8 @@ impl BlockSnapshot { // phase) let epoch_frequency_usize = usize::try_from(epoch_id.mining_commitment_frequency()).expect("Infallible"); - if epoch_frequency_usize >= sampled_window_len + if sampled_window_len > 1 + && epoch_frequency_usize >= sampled_window_len && miner_frequency < epoch_id.mining_commitment_frequency() { // this miner didn't mine often enough to win anyway @@ -785,17 +781,14 @@ mod test { use super::*; use crate::burnchains::tests::*; - use crate::burnchains::*; + use crate::burnchains::{BurnchainSigner, *}; + use crate::chainstate::burn::atc::AtcRational; use crate::chainstate::burn::db::sortdb::tests::test_append_snapshot_with_winner; use crate::chainstate::burn::db::sortdb::*; use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use crate::chainstate::burn::operations::*; use crate::chainstate::stacks::*; - use crate::burnchains::BurnchainSigner; - - use crate::chainstate::burn::atc::AtcRational; - fn test_make_snapshot( sort_tx: &mut SortitionHandleTx, burnchain: &Burnchain, From 8b90a54c2807fcd45cc5779781048aa4d9ccb996 Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 29 Apr 2024 21:22:43 -0400 Subject: [PATCH 23/56] fix: fix various bugs in the MEV reporter --- stackslib/src/main.rs | 151 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 28 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index e3eb43696..bb42b1e7a 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -47,8 +47,12 @@ use blockstack_lib::burnchains::db::{BurnchainBlockData, BurnchainDB}; use blockstack_lib::burnchains::{ Address, Burnchain, PoxConstants, Txid, BLOCKSTACK_MAGIC_MAINNET, }; -use blockstack_lib::chainstate::burn::db::sortdb::SortitionDB; -use blockstack_lib::chainstate::burn::ConsensusHash; +use blockstack_lib::chainstate::burn::db::sortdb::{ + get_block_commit_by_txid, SortitionDB, SortitionHandle, +}; +use blockstack_lib::chainstate::burn::operations::BlockstackOperationType; +use blockstack_lib::chainstate::burn::{BlockSnapshot, ConsensusHash}; +use blockstack_lib::chainstate::coordinator::{get_reward_cycle_info, OnChainRewardSetProvider}; use blockstack_lib::chainstate::nakamoto::NakamotoChainState; use blockstack_lib::chainstate::stacks::db::blocks::{DummyEventDispatcher, StagingBlock}; use blockstack_lib::chainstate::stacks::db::{ @@ -87,12 +91,6 @@ use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey}; use stacks_common::util::vrf::VRFProof; use stacks_common::util::{get_epoch_time_ms, log, sleep_ms}; -use blockstack_lib::chainstate::burn::db::sortdb::get_block_commit_by_txid; -use blockstack_lib::chainstate::burn::db::sortdb::SortitionHandle; -use blockstack_lib::chainstate::burn::BlockSnapshot; -use blockstack_lib::chainstate::coordinator::get_reward_cycle_info; -use blockstack_lib::chainstate::coordinator::OnChainRewardSetProvider; - fn main() { let mut argv: Vec = env::args().collect(); if argv.len() < 2 { @@ -1024,9 +1022,9 @@ simulating a miner. } if argv[1] == "analyze-sortition-mev" { - if argv.len() < 7 { + if argv.len() < 7 || (argv.len() >= 7 && argv.len() % 2 != 1) { eprintln!( - "Usage: {} /path/to/burnchain/db /path/to/sortition/db /path/to/chainstate/db start_height end_height", + "Usage: {} /path/to/burnchain/db /path/to/sortition/db /path/to/chainstate/db start_height end_height [advantage_miner advantage_burn ..]", &argv[0] ); process::exit(1); @@ -1038,8 +1036,23 @@ simulating a miner. let start_height: u64 = argv[5].parse().unwrap(); let end_height: u64 = argv[6].parse().unwrap(); + let mut advantages = HashMap::new(); + if argv.len() >= 7 { + let mut i = 7; + loop { + let advantaged_miner = argv[i].clone(); + let advantage: u64 = argv[i + 1].parse().unwrap(); + advantages.insert(advantaged_miner, advantage); + i += 2; + if i >= argv.len() { + break; + } + } + } + let mut sortdb = SortitionDB::open(&sortdb_path, true, PoxConstants::mainnet_default()).unwrap(); + sortdb.dryrun = true; let burnchain = Burnchain::new(&burnchaindb_path, "bitcoin", "mainnet").unwrap(); let burnchaindb = BurnchainDB::connect(&burnchaindb_path, &burnchain, true).unwrap(); let (mut chainstate, _) = @@ -1049,18 +1062,32 @@ simulating a miner. let mut wins_epoch3 = BTreeMap::new(); for height in start_height..end_height { - let (tip_sort_id, ancestor_sn) = { + debug!("Get ancestor snapshots for {}", height); + let (tip_sort_id, parent_ancestor_sn, ancestor_sn) = { let mut sort_tx = sortdb.tx_begin_at_tip(); let tip_sort_id = sort_tx.tip(); let ancestor_sn = sort_tx .get_block_snapshot_by_height(height) .unwrap() .unwrap(); - (tip_sort_id, ancestor_sn) + let parent_ancestor_sn = sort_tx + .get_block_snapshot_by_height(height - 1) + .unwrap() + .unwrap(); + (tip_sort_id, parent_ancestor_sn, ancestor_sn) }; + + let mut burn_block = + BurnchainDB::get_burnchain_block(burnchaindb.conn(), &ancestor_sn.burn_header_hash) + .unwrap(); + + debug!( + "Get reward cycle info at {}", + burn_block.header.block_height + ); let rc_info_opt = get_reward_cycle_info( - ancestor_sn.block_height + 1, - &ancestor_sn.burn_header_hash, + burn_block.header.block_height, + &burn_block.header.parent_block_hash, &tip_sort_id, &burnchain, &burnchaindb, @@ -1071,9 +1098,22 @@ simulating a miner. ) .unwrap(); - let burn_block = - BurnchainDB::get_burnchain_block(burnchaindb.conn(), &ancestor_sn.burn_header_hash) - .unwrap(); + let mut ops = burn_block.ops.clone(); + for op in ops.iter_mut() { + if let BlockstackOperationType::LeaderBlockCommit(op) = op { + if let Some(extra_burn) = advantages.get(&op.apparent_sender.to_string()) { + debug!( + "Miner {} gets {} extra burn fee", + &op.apparent_sender.to_string(), + extra_burn + ); + op.burn_fee += *extra_burn; + } + } + } + burn_block.ops = ops; + + debug!("Re-evaluate sortition at height {}", height); let (next_sn, state_transition) = sortdb .evaluate_sortition( &burn_block.header, @@ -1085,6 +1125,9 @@ simulating a miner. ) .unwrap(); + assert_eq!(next_sn.block_height, ancestor_sn.block_height); + assert_eq!(next_sn.burn_header_hash, ancestor_sn.burn_header_hash); + let mut sort_tx = sortdb.tx_begin_at_tip(); let tip_pox_id = sort_tx.get_pox_id().unwrap(); let next_sn_nakamoto = BlockSnapshot::make_snapshot_in_epoch( @@ -1092,7 +1135,7 @@ simulating a miner. &burnchain, &ancestor_sn.sortition_id, &tip_pox_id, - &ancestor_sn, + &parent_ancestor_sn, &burn_block.header, &state_transition, 0, @@ -1103,15 +1146,18 @@ simulating a miner. assert_eq!(next_sn.block_height, next_sn_nakamoto.block_height); assert_eq!(next_sn.burn_header_hash, next_sn_nakamoto.burn_header_hash); - let winner_epoch2 = - get_block_commit_by_txid(&sort_tx, &tip_sort_id, &next_sn.winning_block_txid) - .unwrap() - .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) - .unwrap_or("(null)".to_string()); + let winner_epoch2 = get_block_commit_by_txid( + &sort_tx, + &ancestor_sn.sortition_id, + &next_sn.winning_block_txid, + ) + .unwrap() + .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) + .unwrap_or("(null)".to_string()); let winner_epoch3 = get_block_commit_by_txid( &sort_tx, - &tip_sort_id, + &ancestor_sn.sortition_id, &next_sn_nakamoto.winning_block_txid, ) .unwrap() @@ -1131,20 +1177,69 @@ simulating a miner. ); } + let mut all_wins_epoch2 = BTreeMap::new(); + let mut all_wins_epoch3 = BTreeMap::new(); + println!("Wins epoch 2"); println!("------------"); println!("height,burn_header_hash,winner"); - for ((height, bhh), winner) in wins_epoch2.into_iter() { - println!("{},{},{}", &height, &bhh, &winner); + for ((height, bhh), winner) in wins_epoch2.iter() { + println!("{},{},{}", height, bhh, winner); + if let Some(cnt) = all_wins_epoch2.get_mut(winner) { + *cnt += 1; + } else { + all_wins_epoch2.insert(winner, 1); + } } println!("------------"); println!("Wins epoch 3"); println!("------------"); println!("height,burn_header_hash,winner"); - for ((height, bhh), winner) in wins_epoch3.into_iter() { - println!("{},{},{}", &height, &bhh, &winner); + for ((height, bhh), winner) in wins_epoch3.iter() { + println!("{},{},{}", height, bhh, winner); + if let Some(cnt) = all_wins_epoch3.get_mut(winner) { + *cnt += 1; + } else { + all_wins_epoch3.insert(winner, 1); + } } + + println!("---------------"); + println!("Differences"); + println!("---------------"); + println!("height,burn_header_hash,winner_epoch2,winner_epoch3"); + for ((height, bhh), winner) in wins_epoch2.iter() { + let Some(epoch3_winner) = wins_epoch3.get(&(*height, *bhh)) else { + continue; + }; + if epoch3_winner != winner { + println!("{},{},{},{}", height, bhh, winner, epoch3_winner); + } + } + + println!("---------------"); + println!("All epoch2 wins"); + println!("---------------"); + println!("miner,count"); + for (winner, count) in all_wins_epoch2.iter() { + println!("{},{}", winner, count); + } + + println!("---------------"); + println!("All epoch3 wins"); + println!("---------------"); + println!("miner,count,degradation"); + for (winner, count) in all_wins_epoch3.into_iter() { + let degradation = (count as f64) + / (all_wins_epoch2 + .get(&winner) + .map(|cnt| *cnt as f64) + .unwrap_or(0.00000000000001f64)); + println!("{},{},{}", &winner, count, degradation); + } + + process::exit(0); } if argv[1] == "replay-chainstate" { From 88e821279f182aa7bffc8171ac461c0c8e85d288 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 21:48:43 -0400 Subject: [PATCH 24/56] chore: cargo fmt --- stacks-common/src/libcommon.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/stacks-common/src/libcommon.rs b/stacks-common/src/libcommon.rs index 9a4346de0..fef8f0bba 100644 --- a/stacks-common/src/libcommon.rs +++ b/stacks-common/src/libcommon.rs @@ -37,7 +37,6 @@ use crate::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, SortitionId pub mod consts { use crate::types::chainstate::{BlockHeaderHash, ConsensusHash}; - pub use crate::types::MINING_COMMITMENT_WINDOW; pub const TOKEN_TRANSFER_MEMO_LENGTH: usize = 34; // same as it is in Stacks v1 From 1b1615c98141deea5280395483a22bf06901e7a1 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 22:19:22 -0400 Subject: [PATCH 25/56] fix: api sync --- testnet/stacks-node/src/chain_data.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testnet/stacks-node/src/chain_data.rs b/testnet/stacks-node/src/chain_data.rs index 4170cf6f6..0fcc7ca86 100644 --- a/testnet/stacks-node/src/chain_data.rs +++ b/testnet/stacks-node/src/chain_data.rs @@ -163,6 +163,11 @@ impl MinerStats { // 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( + if burnchain.is_in_prepare_phase(burn_block_height) { + 1 + } else { + MINING_COMMITMENT_WINDOW + }, windowed_block_commits, windowed_missed_commits, burn_blocks, @@ -647,6 +652,7 @@ pub mod tests { }; let burn_dist = vec![ BurnSamplePoint { + frequency: 10, burns: block_commit_1.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), range_start: Uint256::zero(), @@ -659,6 +665,7 @@ pub mod tests { candidate: block_commit_1.clone(), }, BurnSamplePoint { + frequency: 10, burns: block_commit_2.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), range_start: Uint256([ @@ -676,6 +683,7 @@ pub mod tests { candidate: block_commit_2.clone(), }, BurnSamplePoint { + frequency: 10, burns: (block_commit_3.burn_fee).into(), median_burn: block_commit_3.burn_fee.into(), range_start: Uint256([ From 9909a3c3f039caa5741069276557e78d4e676a2d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 29 Apr 2024 23:01:55 -0400 Subject: [PATCH 26/56] fix: dryrun --- stackslib/src/chainstate/burn/db/sortdb.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 5bf031d0c..c71f32f85 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -6547,6 +6547,7 @@ pub mod tests { path: path.to_string(), marf, readwrite, + dryrun: false, first_block_height, first_burn_header_hash: first_burn_hash.clone(), pox_constants: PoxConstants::test_default(), From ae5b71673492426561c9212af5d997ca93966256 Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Tue, 30 Apr 2024 06:59:59 -0700 Subject: [PATCH 27/56] fix: config_to_string test with metrics endpoint --- stacks-signer/src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 720afa258..d96017484 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -640,6 +640,7 @@ Public key: 03bc489f27da3701d9f9e577c88de5567cf4023111b7577042d55cde4d823a3505 Network: testnet Database path: :memory: DKG transaction fee: 0.01 uSTX +metrics_endpoint: 0.0.0.0:9090 "# ) ); From 7fb0fedc398e0d8c3bf8df515a3bead8132cf468 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Wed, 1 May 2024 00:10:58 -0400 Subject: [PATCH 28/56] chore: fix failing unit tests --- stackslib/src/chainstate/burn/distribution.rs | 32 ++++----- stackslib/src/chainstate/burn/sortition.rs | 69 ++++++++----------- 2 files changed, 46 insertions(+), 55 deletions(-) diff --git a/stackslib/src/chainstate/burn/distribution.rs b/stackslib/src/chainstate/burn/distribution.rs index 5b6db6397..8687de754 100644 --- a/stackslib/src/chainstate/burn/distribution.rs +++ b/stackslib/src/chainstate/burn/distribution.rs @@ -1049,7 +1049,7 @@ mod tests { median_burn: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256::max(), - frequency: 10, + frequency: 1, candidate: block_commit_1.clone(), }], }, @@ -1068,14 +1068,14 @@ mod tests { 0xffffffffffffffff, 0x7fffffffffffffff, ]), - frequency: 10, + frequency: 1, candidate: block_commit_1.clone(), }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1095,7 +1095,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1109,7 +1109,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1129,7 +1129,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1143,7 +1143,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1163,7 +1163,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1177,7 +1177,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1197,7 +1197,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1211,7 +1211,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1231,7 +1231,7 @@ mod tests { burns: block_commit_1.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1245,7 +1245,7 @@ mod tests { burns: block_commit_2.burn_fee.into(), median_burn: ((block_commit_1.burn_fee + block_commit_2.burn_fee) / 2) .into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1272,7 +1272,7 @@ mod tests { BurnSamplePoint { burns: block_commit_1.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), - frequency: 10, + frequency: 1, range_start: Uint256::zero(), range_end: Uint256([ 0x3ed94d3cb0a84709, @@ -1285,7 +1285,7 @@ mod tests { BurnSamplePoint { burns: block_commit_2.burn_fee.into(), median_burn: block_commit_2.burn_fee.into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0x3ed94d3cb0a84709, 0x0963dded799a7c1a, @@ -1303,7 +1303,7 @@ mod tests { BurnSamplePoint { burns: (block_commit_3.burn_fee).into(), median_burn: block_commit_3.burn_fee.into(), - frequency: 10, + frequency: 1, range_start: Uint256([ 0x7db29a7961508e12, 0x12c7bbdaf334f834, diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 61207c506..825a5d3ad 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -297,10 +297,7 @@ impl BlockSnapshot { // phase) let epoch_frequency_usize = usize::try_from(epoch_id.mining_commitment_frequency()).expect("Infallible"); - if sampled_window_len > 1 - && epoch_frequency_usize >= sampled_window_len - && miner_frequency < epoch_id.mining_commitment_frequency() - { + if usize::from(miner_frequency) < epoch_frequency_usize.min(sampled_window_len) { // this miner didn't mine often enough to win anyway info!("Miner did not mine often enough to win"; "miner_sender" => %winning_block_sender, @@ -345,16 +342,16 @@ impl BlockSnapshot { return (AtcRational::zero(), false); }; - if block_burn_total >= windowed_median_burns { - // clamp to 1.0, and ATC increased - return (AtcRational::one(), false); - } - if windowed_median_burns == 0 { // no carried commit, so null miner wins by default. return (AtcRational::zero(), true); } + if block_burn_total >= windowed_median_burns { + // clamp to 1.0, and ATC increased + return (AtcRational::one(), false); + } + ( AtcRational::frac(block_burn_total, windowed_median_burns), true, @@ -929,46 +926,40 @@ mod test { #[test] fn test_check_is_miner_active() { - assert_eq!(StacksEpochId::Epoch30.mining_commitment_frequency(), 10); - assert_eq!(StacksEpochId::Epoch25.mining_commitment_frequency(), 6); + assert_eq!(StacksEpochId::Epoch30.mining_commitment_frequency(), 3); + assert_eq!(StacksEpochId::Epoch25.mining_commitment_frequency(), 0); // reward phase assert!(BlockSnapshot::check_miner_is_active( StacksEpochId::Epoch30, - 10, - &BurnchainSigner("".to_string()), - 10 - )); - assert!(BlockSnapshot::check_miner_is_active( - StacksEpochId::Epoch30, - 10, - &BurnchainSigner("".to_string()), - 9 - )); - assert!(BlockSnapshot::check_miner_is_active( - StacksEpochId::Epoch30, - 10, - &BurnchainSigner("".to_string()), - 8 - )); - assert!(BlockSnapshot::check_miner_is_active( - StacksEpochId::Epoch30, - 10, - &BurnchainSigner("".to_string()), - 7 - )); - assert!(BlockSnapshot::check_miner_is_active( - StacksEpochId::Epoch30, - 10, + 6, &BurnchainSigner("".to_string()), 6 )); - assert!(!BlockSnapshot::check_miner_is_active( + assert!(BlockSnapshot::check_miner_is_active( StacksEpochId::Epoch30, - 10, + 6, &BurnchainSigner("".to_string()), 5 )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 6, + &BurnchainSigner("".to_string()), + 4 + )); + assert!(BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 6, + &BurnchainSigner("".to_string()), + 3 + )); + assert!(!BlockSnapshot::check_miner_is_active( + StacksEpochId::Epoch30, + 6, + &BurnchainSigner("".to_string()), + 2 + )); // prepare phase assert!(BlockSnapshot::check_miner_is_active( @@ -1037,7 +1028,7 @@ mod test { // no carried commit assert_eq!( BlockSnapshot::get_miner_commit_carryover(Some(2), Some(0)), - (AtcRational::zero(), false) + (AtcRational::zero(), true) ); // assumed carryover From e35c15ca56afb047b77ee1742387c070febddd8e Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Wed, 1 May 2024 00:15:44 -0400 Subject: [PATCH 29/56] fix: check bounds before indexing --- stackslib/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index bb42b1e7a..1047ae04d 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1039,14 +1039,11 @@ simulating a miner. let mut advantages = HashMap::new(); if argv.len() >= 7 { let mut i = 7; - loop { + while i + 2 < argv.len() { let advantaged_miner = argv[i].clone(); let advantage: u64 = argv[i + 1].parse().unwrap(); advantages.insert(advantaged_miner, advantage); i += 2; - if i >= argv.len() { - break; - } } } From 73a2735e46148c2e5d800fa22614452d2c8fa121 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Wed, 1 May 2024 13:24:01 -0400 Subject: [PATCH 30/56] fix: address PR feedback, and make it so that the test framework generates the correct UTXO linkage data (so the new MEV algorithm won't disqualify Nakamoto miners using TestPeer) --- stackslib/src/burnchains/burnchain.rs | 8 ++++++-- stackslib/src/burnchains/tests/mod.rs | 2 +- stackslib/src/chainstate/burn/atc.rs | 14 +++++++++----- stackslib/src/chainstate/burn/sortition.rs | 12 ++++++------ 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 87758bcef..29361d96e 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -131,12 +131,15 @@ impl BurnchainStateTransition { if block_total_burns.len() == 0 { return Some(0); + } else if block_total_burns.len() == 1 { + return Some(block_total_burns[0]) } else if block_total_burns.len() % 2 != 0 { let idx = block_total_burns.len() / 2; return block_total_burns.get(idx).map(|b| *b); } else { - let idx_left = block_total_burns.len() / 2; - let idx_right = block_total_burns.len() / 2 + 1; + // NOTE: the `- 1` is safe because block_total_burns.len() >= 2 + let idx_left = block_total_burns.len() / 2 - 1; + let idx_right = block_total_burns.len() / 2; let burn_left = block_total_burns.get(idx_left)?; let burn_right = block_total_burns.get(idx_right)?; return Some((burn_left + burn_right) / 2); @@ -252,6 +255,7 @@ impl BurnchainStateTransition { windowed_missed_commits.push(missed_commits_at_height); } + test_debug!("Block {} is in a reward phase with PoX. Miner commit window is {}: {:?}", parent_snapshot.block_height + 1, windowed_block_commits.len(), &windowed_block_commits); } else { // PoX reward-phase is not active debug!( diff --git a/stackslib/src/burnchains/tests/mod.rs b/stackslib/src/burnchains/tests/mod.rs index fc7f6993a..f09757eee 100644 --- a/stackslib/src/burnchains/tests/mod.rs +++ b/stackslib/src/burnchains/tests/mod.rs @@ -452,7 +452,7 @@ impl TestBurnchainBlock { leader_key.block_height as u32, leader_key.vtxindex as u16, burn_fee, - &input, + &(parent.txid.clone(), (1 + parent.commit_outs.len()) as u32), &apparent_sender, ); txop diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index ac35f2c65..e14574220 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -18,12 +18,16 @@ use stacks_common::util::uint::Uint256; use crate::stacks_common::util::uint::BitArray; -/// A fixed-point numerical representation for ATC. THe integer and fractional parts are both 64 -/// bits. +/// A fixed-point numerical representation for ATC. The integer and fractional parts are both 64 +/// bits. Internally, this is a Uint256 so that safe addition and multiplication can be done. +/// +/// Bits 0-63 are the fraction. +/// Bits 64-127 are the integer. +/// Bits 128-256 are 0's to facilitate safe addition and multiplication. /// /// The reasons we use this instead of f64 for ATC calculations are as follows: /// * This avoids unrepresentable states, like NaN or +/- INF -/// * This avoids ambituous states, like +0.0 and -0.0. +/// * This avoids ambiguous states, like +0.0 and -0.0. /// * This integrates better into the sortition-sampling system, which uses a u256 to represent a /// probability range (which is what this is going to be used for) #[derive(Debug, Clone, PartialEq, Copy, Eq, Hash)] @@ -271,7 +275,7 @@ mod test { /// Calculate the logic advantage curve for the null miner. /// This function's parameters are chosen such that: - /// * if the ATC carryover has diminished by less than 20%, the null miner has negligeable + /// * if the ATC carryover has diminished by less than 20%, the null miner has negligible /// chances of winning. This is to avoid punishing honest miners when there are flash blocks. /// * If the ATC carryover has diminished by between 20% and 80%, the null miner has a /// better-than-linear probability of winning. That is, if the burnchain MEV miner pays less @@ -365,7 +369,7 @@ mod test { /// assumed total commit carryover -- the ratio between what the winning miner paid in this /// block-commit to the median of what they historically paid (for an epoch-defined search window /// size). A value greater than 1.0 means that the miner paid all of the assumed commit -/// carry-over, and the null miner has negligeable chances of winning. A value less than 1.0 means +/// carry-over, and the null miner has negligible chances of winning. A value less than 1.0 means /// that the miner underpaid relative to their past performance, and the closer to 0.0 this ratio /// is, the more likely the null miner wins and this miner loses. /// diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 825a5d3ad..fa6fda26b 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -187,7 +187,7 @@ impl BlockSnapshot { } /// Select the next Stacks block header hash using cryptographic sortition. - /// Go through all block commits at this height, find out how any burn tokens + /// Go through all block commits at this height, find out how many burn tokens /// were spent for them, and select one at random using the relative burn amounts /// to weight the sample. Use HASH(sortition_hash ++ last_VRF_seed) to pick the /// winning block commit, and by extension, the next VRF seed. @@ -382,7 +382,7 @@ impl BlockSnapshot { /// /// This is NullP(atc) = (1 - atc) + atc * adv(atc). /// - /// Where atv(x) is an "advantage function", such that the null miner is more heavily favored + /// Where adv(x) is an "advantage function", such that the null miner is more heavily favored /// to win based on how comparatively little commit carryover there is. Here, adv(x) is a /// logistic function. /// @@ -439,8 +439,8 @@ impl BlockSnapshot { // make the block header hash different, to render it different from the winner. // Just flip the block header bits. let mut bhh_bytes = null_winner.block_header_hash.0.clone(); - for i in 0..bhh_bytes.len() { - bhh_bytes[i] = !bhh_bytes[i]; + for byte in bhh_bytes.iter_mut() { + *byte = !*byte; } BlockHeaderHash(bhh_bytes) }; @@ -452,7 +452,7 @@ impl BlockSnapshot { // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction - // fo mining power the null miner has. + // of mining power the null miner has. let null_prob_u256 = if null_prob.0 >= AtcRational::one().0 { // prevent left-shift overflow AtcRational::one_sup().into_inner() << 192 @@ -655,7 +655,7 @@ impl BlockSnapshot { // * if the winning miner didn't mine in more than k of n blocks of the window, then their chances of // winning are 0. // * There exists a "null miner" that can win sortition, in which case there is no - // sortition. This happens if the assumed total commit with carry-over is sufficently low. + // sortition. This happens if the assumed total commit with carry-over is sufficiently low. let mut reject_winner_reason = None; if epoch_id >= StacksEpochId::Epoch30 { if !Self::check_miner_is_active( From 9f79afcb2662c7330399ca985c359c6453a66002 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Wed, 1 May 2024 13:27:40 -0400 Subject: [PATCH 31/56] fix: test the ATC lookup table generation, and cargo-fmt --- stackslib/src/burnchains/burnchain.rs | 9 +++++++-- stackslib/src/chainstate/burn/atc.rs | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 29361d96e..9b91c6d43 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -132,7 +132,7 @@ impl BurnchainStateTransition { if block_total_burns.len() == 0 { return Some(0); } else if block_total_burns.len() == 1 { - return Some(block_total_burns[0]) + return Some(block_total_burns[0]); } else if block_total_burns.len() % 2 != 0 { let idx = block_total_burns.len() / 2; return block_total_burns.get(idx).map(|b| *b); @@ -255,7 +255,12 @@ impl BurnchainStateTransition { windowed_missed_commits.push(missed_commits_at_height); } - test_debug!("Block {} is in a reward phase with PoX. Miner commit window is {}: {:?}", parent_snapshot.block_height + 1, windowed_block_commits.len(), &windowed_block_commits); + test_debug!( + "Block {} is in a reward phase with PoX. Miner commit window is {}: {:?}", + parent_snapshot.block_height + 1, + windowed_block_commits.len(), + &windowed_block_commits + ); } else { // PoX reward-phase is not active debug!( diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index e14574220..0a4912753 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -345,13 +345,14 @@ mod test { } #[test] - #[ignore] fn make_null_miner_lookup_table() { + use crate::chainstate::burn::atc::ATC_LOOKUP; let mut lookup_table = Vec::with_capacity(1024); for atc in 0..1024 { let fatc = (atc as f64) / 1024.0; let lgst_fatc = null_miner_logistic(fatc); let lgst_rational = AtcRational::from_f64_unit(lgst_fatc); + assert_eq!(ATC_LOOKUP[atc], lgst_rational); lookup_table.push(lgst_rational); } println!("["); From b1e09ddfc971c8f5ba66e334f3aeba11123d8f1c Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Thu, 2 May 2024 06:48:35 -0700 Subject: [PATCH 32/56] fix: fixed config_to_log_string test --- stacks-signer/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index d96017484..819eeadb3 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -640,7 +640,7 @@ Public key: 03bc489f27da3701d9f9e577c88de5567cf4023111b7577042d55cde4d823a3505 Network: testnet Database path: :memory: DKG transaction fee: 0.01 uSTX -metrics_endpoint: 0.0.0.0:9090 +Metrics endpoint: 0.0.0.0:9090 "# ) ); From 2c6cddafe04464b1e6e390b2278e406faae571ad Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 2 May 2024 22:25:15 -0400 Subject: [PATCH 33/56] fix: disable windowing when we transition to epoch 3.0 for the duration of one window length --- stackslib/src/burnchains/burnchain.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 9b91c6d43..1f326f4f3 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -211,10 +211,26 @@ impl BurnchainStateTransition { }) .epoch_id; + // what was the epoch at the start of this window? + let window_start_epoch_id = SortitionDB::get_stacks_epoch( + sort_tx, + parent_snapshot + .block_height + .saturating_sub(epoch_id.mining_commitment_window().into()), + )? + .unwrap_or_else(|| { + panic!( + "FATAL: no epoch defined at burn height {}", + parent_snapshot.block_height + 1 + ) + }) + .epoch_id; + if !burnchain.is_in_prepare_phase(parent_snapshot.block_height + 1) && !burnchain .pox_constants .is_after_pox_sunset_end(parent_snapshot.block_height + 1, epoch_id) + && (epoch_id < StacksEpochId::Epoch30 || window_start_epoch_id == epoch_id) { // PoX reward-phase is active! // build a map of intended sortition -> missed commit for the missed commits @@ -262,9 +278,9 @@ impl BurnchainStateTransition { &windowed_block_commits ); } else { - // PoX reward-phase is not active + // PoX reward-phase is not active, or we're starting a new epoch debug!( - "Block {} is in a prepare phase or post-PoX sunset, so no windowing will take place", + "Block {} is in a prepare phase, in the post-PoX sunset, or in an epoch transition, so no windowing will take place", parent_snapshot.block_height + 1 ); From 09454b702585ec2aa3ee8bbd090692f8dda2a45b Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 2 May 2024 22:25:51 -0400 Subject: [PATCH 34/56] fix: generate the expected block commit input so that mining works in nakamoto --- stackslib/src/burnchains/tests/mod.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/stackslib/src/burnchains/tests/mod.rs b/stackslib/src/burnchains/tests/mod.rs index f09757eee..10e83605b 100644 --- a/stackslib/src/burnchains/tests/mod.rs +++ b/stackslib/src/burnchains/tests/mod.rs @@ -405,7 +405,6 @@ impl TestBurnchainBlock { new_seed: Option, epoch_marker: u8, ) -> LeaderBlockCommitOp { - let input = (Txid([0; 32]), 0); let pubks = miner .privks .iter() @@ -442,8 +441,22 @@ impl TestBurnchainBlock { &last_snapshot_with_sortition.sortition_id, ) .expect("FATAL: failed to read block commit"); + + let input = SortitionDB::get_last_block_commit_by_sender(ic.conn(), &apparent_sender) + .unwrap() + .map(|commit| (commit.txid.clone(), 1 + (commit.commit_outs.len() as u32))) + .unwrap_or((Txid([0x00; 32]), 0)); + + test_debug!("Last input from {} is {:?}", &apparent_sender, &input); + let mut txop = match get_commit_res { Some(parent) => { + test_debug!( + "Block-commit for {} (burn height {}) builds on leader block-commit {:?}", + block_hash, + self.block_height, + &parent + ); let txop = LeaderBlockCommitOp::new( block_hash, self.block_height, @@ -452,7 +465,7 @@ impl TestBurnchainBlock { leader_key.block_height as u32, leader_key.vtxindex as u16, burn_fee, - &(parent.txid.clone(), (1 + parent.commit_outs.len()) as u32), + &input, &apparent_sender, ); txop From 754cd6043f03da10149685f1a17410cec8f4372b Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 2 May 2024 22:26:11 -0400 Subject: [PATCH 35/56] chore: index block-commits by apparent sender, so we can get the last commit by sender --- stackslib/src/chainstate/burn/db/sortdb.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index c71f32f85..674bb5563 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -50,9 +50,9 @@ use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry}; use crate::burnchains::bitcoin::BitcoinNetworkType; use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{ - Address, Burnchain, BurnchainBlockHeader, BurnchainRecipient, BurnchainStateTransition, - BurnchainStateTransitionOps, BurnchainTransaction, BurnchainView, Error as BurnchainError, - PoxConstants, PublicKey, Txid, + Address, Burnchain, BurnchainBlockHeader, BurnchainRecipient, BurnchainSigner, + BurnchainStateTransition, BurnchainStateTransitionOps, BurnchainTransaction, BurnchainView, + Error as BurnchainError, PoxConstants, PublicKey, Txid, }; use crate::chainstate::burn::operations::leader_block_commit::{ MissedBlockCommit, RewardSetInfo, OUTPUTS_PER_COMMIT, @@ -744,7 +744,7 @@ const SORTITION_DB_SCHEMA_8: &'static [&'static str] = &[ );"#, ]; -const LAST_SORTITION_DB_INDEX: &'static str = "index_vote_for_aggregate_key_burn_header_hash"; +const LAST_SORTITION_DB_INDEX: &'static str = "index_block_commits_by_sender"; const SORTITION_DB_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS snapshots_block_hashes ON snapshots(block_height,index_root,winning_stacks_block_hash);", "CREATE INDEX IF NOT EXISTS snapshots_block_stacks_hashes ON snapshots(num_sortitions,index_root,winning_stacks_block_hash);", @@ -766,6 +766,8 @@ const SORTITION_DB_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS index_burn_header_hash_pox_valid ON snapshots(burn_header_hash,pox_valid);", "CREATE INDEX IF NOT EXISTS index_delegate_stx_burn_header_hash ON delegate_stx(burn_header_hash);", "CREATE INDEX IF NOT EXISTS index_vote_for_aggregate_key_burn_header_hash ON vote_for_aggregate_key(burn_header_hash);", + "CREATE INDEX IF NOT EXISTS index_block_commits_by_burn_height ON block_commits(block_height);", + "CREATE INDEX IF NOT EXISTS index_block_commits_by_sender ON block_commits(apparent_sender);" ]; pub struct SortitionDB { @@ -3613,6 +3615,18 @@ impl SortitionDB { .try_into() .ok() } + + /// Get the last block-commit from a given sender + pub fn get_last_block_commit_by_sender( + conn: &DBConn, + sender: &BurnchainSigner, + ) -> Result, db_error> { + let apparent_sender_str = + serde_json::to_string(sender).map_err(|e| db_error::SerializationError(e))?; + let sql = "SELECT * FROM block_commits WHERE apparent_sender = ?1 ORDER BY block_height DESC LIMIT 1"; + let args = rusqlite::params![&apparent_sender_str]; + query_row(conn, sql, args) + } } impl<'a> SortitionDBTx<'a> { From d45faf9a08b365ec4357938b0ac3a13f69db4bd0 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 2 May 2024 22:26:41 -0400 Subject: [PATCH 36/56] fix: typos --- stackslib/src/chainstate/burn/operations/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index 90f7f7929..ffb66d0db 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -39,9 +39,9 @@ use crate::chainstate::burn::{ConsensusHash, Opcodes}; use crate::chainstate::stacks::address::PoxAddress; use crate::util_lib::db::{DBConn, DBTx, Error as db_error}; +/// This module contains all burn-chain operations pub mod delegate_stx; pub mod leader_block_commit; -/// This module contains all burn-chain operations pub mod leader_key_register; pub mod stack_stx; pub mod transfer_stx; @@ -269,7 +269,7 @@ pub struct DelegateStxOp { pub sender: StacksAddress, pub delegate_to: StacksAddress, /// a tuple representing the output index of the reward address in the BTC transaction, - // and the actual PoX reward address. + /// and the actual PoX reward address. /// NOTE: the address in .pox-2 will be tagged as either p2pkh or p2sh; it's impossible to tell /// if it's a segwit-p2sh since that looks identical to a p2sh address. pub reward_addr: Option<(u32, PoxAddress)>, From c52e9bfe7a9b71e9b07baa84d9ab1b4b3a7482e5 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 14:43:32 -0400 Subject: [PATCH 37/56] chore: use the burnchain indexer to find the right block header --- stackslib/src/burnchains/db.rs | 10 +++++++--- stackslib/src/chainstate/coordinator/mod.rs | 11 +++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/stackslib/src/burnchains/db.rs b/stackslib/src/burnchains/db.rs index f7da4a0ae..3171ec3c9 100644 --- a/stackslib/src/burnchains/db.rs +++ b/stackslib/src/burnchains/db.rs @@ -1123,12 +1123,16 @@ impl BurnchainDB { Ok(res.is_some()) } - pub fn get_burnchain_header( + pub fn get_burnchain_header( conn: &DBConn, + indexer: &B, height: u64, ) -> Result, BurnchainError> { - let qry = "SELECT * FROM burnchain_db_block_headers WHERE block_height = ?1"; - let args = &[&u64_to_sql(height)?]; + let Some(hdr) = indexer.read_burnchain_header(height)? else { + return Ok(None); + }; + let qry = "SELECT * FROM burnchain_db_block_headers WHERE block_hash = ?1"; + let args = &[&hdr.block_hash]; let res: Option = query_row(conn, qry, args)?; Ok(res) } diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index e54e9f020..96eae4464 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1603,6 +1603,7 @@ impl< /// block can be re-processed in that event. fn undo_stacks_block_orphaning( burnchain_conn: &DBConn, + burnchain_indexer: &B, ic: &SortitionDBConn, chainstate_db_tx: &mut DBTx, first_invalidate_start_block: u64, @@ -1613,8 +1614,11 @@ impl< first_invalidate_start_block, last_invalidate_start_block ); for burn_height in first_invalidate_start_block..(last_invalidate_start_block + 1) { - let burn_header = match BurnchainDB::get_burnchain_header(burnchain_conn, burn_height)? - { + let burn_header = match BurnchainDB::get_burnchain_header( + burnchain_conn, + burnchain_indexer, + burn_height, + )? { Some(hdr) => hdr, None => { continue; @@ -1840,6 +1844,7 @@ impl< // sortitions let revalidated_burn_header = BurnchainDB::get_burnchain_header( self.burnchain_blocks_db.conn(), + &self.burnchain_indexer, first_invalidate_start_block - 1, ) .expect("FATAL: failed to read burnchain DB") @@ -1854,6 +1859,7 @@ impl< // invalidate all descendant sortitions, no matter what. let invalidated_burn_header = BurnchainDB::get_burnchain_header( self.burnchain_blocks_db.conn(), + &self.burnchain_indexer, last_invalidate_start_block - 1, ) .expect("FATAL: failed to read burnchain DB") @@ -2045,6 +2051,7 @@ impl< // un-orphan blocks that had been orphaned but were tied to this now-revalidated sortition history Self::undo_stacks_block_orphaning( &self.burnchain_blocks_db.conn(), + &self.burnchain_indexer, &ic, &mut chainstate_db_tx, first_invalidate_start_block, From 61b37c0b275284a01b9e6337ff7057421c6dff34 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 15:11:26 -0400 Subject: [PATCH 38/56] fix: optionally disable resolving bootstrap nodes, so the caller can avoid needless network I/O if it's not needed --- .../burnchains/bitcoin_regtest_controller.rs | 2 +- testnet/stacks-node/src/config.rs | 47 ++++++++++++++----- testnet/stacks-node/src/main.rs | 16 ++++--- testnet/stacks-node/src/nakamoto_node.rs | 2 +- .../stacks-node/src/nakamoto_node/miner.rs | 2 +- testnet/stacks-node/src/neon_node.rs | 8 ++-- testnet/stacks-node/src/run_loop/nakamoto.rs | 2 +- testnet/stacks-node/src/run_loop/neon.rs | 2 +- .../src/tests/neon_integrations.rs | 2 +- 9 files changed, 53 insertions(+), 30 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 5c6ba0555..8cf9ac82b 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -704,7 +704,7 @@ impl BitcoinRegtestController { block_height: u64, ) -> Option { // if mock mining, do not even bother requesting UTXOs - if self.config.get_node_config().mock_mining { + if self.config.get_node_config(false).mock_mining { return None; } diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 0c2e16f8b..60b60cf9f 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -101,7 +101,8 @@ mod tests { seed = "invalid-hex-value" "#, ) - .unwrap() + .unwrap(), + false ) .unwrap_err() ); @@ -115,7 +116,8 @@ mod tests { local_peer_seed = "invalid-hex-value" "#, ) - .unwrap() + .unwrap(), + false ) .unwrap_err() ); @@ -130,6 +132,7 @@ mod tests { "#, ) .unwrap(), + false, ) .unwrap_err(); assert_eq!( @@ -137,7 +140,7 @@ mod tests { &actual_err_msg[..expected_err_prefix.len()] ); - assert!(Config::from_config_file(ConfigFile::from_str("").unwrap()).is_ok()); + assert!(Config::from_config_file(ConfigFile::from_str("").unwrap(), false).is_ok()); } #[test] @@ -195,6 +198,7 @@ mod tests { "#, ) .unwrap(), + false, ) .expect("Expected to be able to parse block proposal token from file"); @@ -218,6 +222,7 @@ mod tests { "# )) .expect("Expected to be able to parse config file from string"), + false, ) .expect("Expected to be able to parse affirmation map from file"); @@ -241,7 +246,7 @@ mod tests { )) .expect("Expected to be able to parse config file from string"); - assert!(Config::from_config_file(file).is_err()); + assert!(Config::from_config_file(file, false).is_err()); } #[test] @@ -249,6 +254,7 @@ mod tests { let config = Config::from_config_file( ConfigFile::from_str(r#""#) .expect("Expected to be able to parse config file from string"), + false, ) .expect("Expected to be able to parse affirmation map from file"); @@ -266,6 +272,7 @@ mod tests { "#, ) .expect("Expected to be able to parse config file from string"), + false, ) .expect("Expected to be able to parse affirmation map from file"); // Should default add xenon affirmation overrides @@ -291,6 +298,7 @@ mod tests { "#, )) .expect("Expected to be able to parse config file from string"), + false, ) .expect("Expected to be able to parse affirmation map from file"); // Should default add xenon affirmation overrides, but overwrite with the configured one above @@ -537,7 +545,7 @@ impl Config { let Ok(config_file) = ConfigFile::from_path(path.as_str()) else { return self.burnchain.clone(); }; - let Ok(config) = Config::from_config_file(config_file) else { + let Ok(config) = Config::from_config_file(config_file, false) else { return self.burnchain.clone(); }; config.burnchain @@ -552,20 +560,20 @@ impl Config { let Ok(config_file) = ConfigFile::from_path(path.as_str()) else { return self.miner.clone(); }; - let Ok(config) = Config::from_config_file(config_file) else { + let Ok(config) = Config::from_config_file(config_file, false) else { return self.miner.clone(); }; return config.miner; } - pub fn get_node_config(&self) -> NodeConfig { + pub fn get_node_config(&self, resolve_bootstrap_nodes: bool) -> NodeConfig { let Some(path) = &self.config_path else { return self.node.clone(); }; let Ok(config_file) = ConfigFile::from_path(path.as_str()) else { return self.node.clone(); }; - let Ok(config) = Config::from_config_file(config_file) else { + let Ok(config) = Config::from_config_file(config_file, resolve_bootstrap_nodes) else { return self.node.clone(); }; return config.node; @@ -941,11 +949,18 @@ impl Config { Ok(out_epochs) } - pub fn from_config_file(config_file: ConfigFile) -> Result { - Self::from_config_default(config_file, Config::default()) + pub fn from_config_file( + config_file: ConfigFile, + resolve_bootstrap_nodes: bool, + ) -> Result { + Self::from_config_default(config_file, Config::default(), resolve_bootstrap_nodes) } - fn from_config_default(config_file: ConfigFile, default: Config) -> Result { + fn from_config_default( + config_file: ConfigFile, + default: Config, + resolve_bootstrap_nodes: bool, + ) -> Result { let Config { node: default_node_config, burnchain: default_burnchain_config, @@ -996,9 +1011,15 @@ impl Config { }; if let Some(bootstrap_node) = bootstrap_node { - node.set_bootstrap_nodes(bootstrap_node, burnchain.chain_id, burnchain.peer_version); + if resolve_bootstrap_nodes { + node.set_bootstrap_nodes( + bootstrap_node, + burnchain.chain_id, + burnchain.peer_version, + ); + } } else { - if is_mainnet { + if is_mainnet && resolve_bootstrap_nodes { let bootstrap_node = ConfigFile::mainnet().node.unwrap().bootstrap_node.unwrap(); node.set_bootstrap_nodes( bootstrap_node, diff --git a/testnet/stacks-node/src/main.rs b/testnet/stacks-node/src/main.rs index cb512969c..41b742627 100644 --- a/testnet/stacks-node/src/main.rs +++ b/testnet/stacks-node/src/main.rs @@ -65,7 +65,7 @@ static GLOBAL: Jemalloc = Jemalloc; fn cli_pick_best_tip(config_path: &str, at_stacks_height: Option) -> TipCandidate { info!("Loading config at path {}", config_path); let config = match ConfigFile::from_path(config_path) { - Ok(config_file) => Config::from_config_file(config_file).unwrap(), + Ok(config_file) => Config::from_config_file(config_file, true).unwrap(), Err(e) => { warn!("Invalid config file: {}", e); process::exit(1); @@ -105,7 +105,7 @@ fn cli_get_miner_spend( ) -> u64 { info!("Loading config at path {}", config_path); let config = match ConfigFile::from_path(&config_path) { - Ok(config_file) => Config::from_config_file(config_file).unwrap(), + Ok(config_file) => Config::from_config_file(config_file, true).unwrap(), Err(e) => { warn!("Invalid config file: {}", e); process::exit(1); @@ -334,7 +334,7 @@ fn main() { process::exit(1); } }; - match Config::from_config_file(config_file) { + match Config::from_config_file(config_file, true) { Ok(_) => { info!("Loaded config!"); process::exit(0); @@ -365,9 +365,11 @@ fn main() { let seed = { let config_path: Option = args.opt_value_from_str("--config").unwrap(); if let Some(config_path) = config_path { - let conf = - Config::from_config_file(ConfigFile::from_path(&config_path).unwrap()) - .unwrap(); + let conf = Config::from_config_file( + ConfigFile::from_path(&config_path).unwrap(), + true, + ) + .unwrap(); args.finish(); conf.node.seed } else { @@ -416,7 +418,7 @@ fn main() { } }; - let conf = match Config::from_config_file(config_file) { + let conf = match Config::from_config_file(config_file, true) { Ok(conf) => conf, Err(e) => { warn!("Invalid config: {}", e); diff --git a/testnet/stacks-node/src/nakamoto_node.rs b/testnet/stacks-node/src/nakamoto_node.rs index 0cfc8130a..439486a34 100644 --- a/testnet/stacks-node/src/nakamoto_node.rs +++ b/testnet/stacks-node/src/nakamoto_node.rs @@ -167,7 +167,7 @@ impl StacksNode { let local_peer = p2p_net.local_peer.clone(); // setup initial key registration - let leader_key_registration_state = if config.get_node_config().mock_mining { + let leader_key_registration_state = if config.get_node_config(false).mock_mining { // mock mining, pretend to have a registered key let (vrf_public_key, _) = keychain.make_vrf_keypair(VRF_MOCK_MINER_KEY); LeaderKeyRegistrationState::Active(RegisteredKey { diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 7597b3b43..4b793d010 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -656,7 +656,7 @@ impl BlockMinerThread { fn make_vrf_proof(&mut self) -> Option { // if we're a mock miner, then make sure that the keychain has a keypair for the mocked VRF // key - let vrf_proof = if self.config.get_node_config().mock_mining { + let vrf_proof = if self.config.get_node_config(false).mock_mining { self.keychain.generate_proof( VRF_MOCK_MINER_KEY, self.burn_block.sortition_hash.as_bytes(), diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 5695ec81d..28e655277 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -1712,7 +1712,7 @@ impl BlockMinerThread { fn make_vrf_proof(&mut self) -> Option { // if we're a mock miner, then make sure that the keychain has a keypair for the mocked VRF // key - let vrf_proof = if self.config.get_node_config().mock_mining { + let vrf_proof = if self.config.get_node_config(false).mock_mining { self.keychain.generate_proof( VRF_MOCK_MINER_KEY, self.burn_block.sortition_hash.as_bytes(), @@ -2535,7 +2535,7 @@ impl BlockMinerThread { let res = bitcoin_controller.submit_operation(target_epoch_id, op, &mut op_signer, attempt); if res.is_none() { self.failed_to_submit_last_attempt = true; - if !self.config.get_node_config().mock_mining { + if !self.config.get_node_config(false).mock_mining { warn!("Relayer: Failed to submit Bitcoin transaction"); return None; } @@ -3518,7 +3518,7 @@ impl RelayerThread { return false; } - if !self.config.get_node_config().mock_mining { + if !self.config.get_node_config(false).mock_mining { // mock miner can't mine microblocks yet, so don't stop it from trying multiple // anchored blocks if self.mined_stacks_block && self.config.node.mine_microblocks { @@ -4777,7 +4777,7 @@ impl StacksNode { let local_peer = p2p_net.local_peer.clone(); // setup initial key registration - let leader_key_registration_state = if config.get_node_config().mock_mining { + let leader_key_registration_state = if config.get_node_config(false).mock_mining { // mock mining, pretend to have a registered key let (vrf_public_key, _) = keychain.make_vrf_keypair(VRF_MOCK_MINER_KEY); LeaderKeyRegistrationState::Active(RegisteredKey { diff --git a/testnet/stacks-node/src/run_loop/nakamoto.rs b/testnet/stacks-node/src/run_loop/nakamoto.rs index fa6045cd6..b7cdb8a05 100644 --- a/testnet/stacks-node/src/run_loop/nakamoto.rs +++ b/testnet/stacks-node/src/run_loop/nakamoto.rs @@ -195,7 +195,7 @@ impl RunLoop { return true; } } - if self.config.get_node_config().mock_mining { + if self.config.get_node_config(false).mock_mining { info!("No UTXOs found, but configured to mock mine"); return true; } else { diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index 2c1bc5b70..c9925ce19 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -373,7 +373,7 @@ impl RunLoop { return true; } } - if self.config.get_node_config().mock_mining { + if self.config.get_node_config(false).mock_mining { info!("No UTXOs found, but configured to mock mine"); return true; } else { diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 2dce43b66..20302d251 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -149,7 +149,7 @@ fn inner_neon_integration_test_conf(seed: Option>) -> (Config, StacksAdd burnchain.peer_host = Some("127.0.0.1".to_string()); } - let magic_bytes = Config::from_config_file(cfile) + let magic_bytes = Config::from_config_file(cfile, false) .unwrap() .burnchain .magic_bytes; From 56f754ed4bed278f3d0abc5230c0ae3046653a3b Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 16:58:58 -0400 Subject: [PATCH 39/56] chore: expose set_initial_peer --- stackslib/src/net/db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/net/db.rs b/stackslib/src/net/db.rs index 1c116a617..9ef77e169 100644 --- a/stackslib/src/net/db.rs +++ b/stackslib/src/net/db.rs @@ -1142,7 +1142,7 @@ impl PeerDB { } /// Set a peer as an initial peer - fn set_initial_peer( + pub fn set_initial_peer( tx: &Transaction, network_id: u32, peer_addr: &PeerAddress, From 7318babacf2f56bfdc4f86c0bb72191fbb59e8c8 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 17:03:43 -0400 Subject: [PATCH 40/56] fix: don't start from reward cycle 0 when doing an inv sync; start from connection_opts.inv_reward_cycles --- stackslib/src/net/inv/epoch2x.rs | 36 +++++++------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 8df013a8c..c0dd04152 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -52,10 +52,7 @@ pub const INV_SYNC_INTERVAL: u64 = 150; #[cfg(test)] pub const INV_SYNC_INTERVAL: u64 = 3; -#[cfg(not(test))] pub const INV_REWARD_CYCLES: u64 = 2; -#[cfg(test)] -pub const INV_REWARD_CYCLES: u64 = 1; #[derive(Debug, PartialEq, Clone)] pub struct PeerBlocksInv { @@ -1756,7 +1753,7 @@ impl PeerNetwork { } /// Determine at which reward cycle to begin scanning inventories - fn get_block_scan_start(&self, sortdb: &SortitionDB, highest_remote_reward_cycle: u64) -> u64 { + fn get_block_scan_start(&self, sortdb: &SortitionDB) -> u64 { // see if the stacks tip affirmation map and heaviest affirmation map diverge. If so, then // start scaning at the reward cycle just before that. let am_rescan_rc = self @@ -1783,18 +1780,15 @@ impl PeerNetwork { .block_height_to_reward_cycle(stacks_tip_burn_block_height) .unwrap_or(0); - let start_reward_cycle = cmp::min( - stacks_tip_rc, - highest_remote_reward_cycle.saturating_sub(self.connection_opts.inv_reward_cycles), - ); + let start_reward_cycle = + stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); let rescan_rc = cmp::min(am_rescan_rc, start_reward_cycle); test_debug!( - "begin blocks inv scan at {} = min({},{},{})", + "begin blocks inv scan at {} = min({},{})", rescan_rc, stacks_tip_rc, - highest_remote_reward_cycle.saturating_sub(self.connection_opts.inv_reward_cycles), am_rescan_rc ); rescan_rc @@ -1814,12 +1808,7 @@ impl PeerNetwork { Some(x) => x, None => { // proceed to block scan - let scan_start_rc = self.get_block_scan_start( - sortdb, - self.burnchain - .block_height_to_reward_cycle(stats.inv.get_block_height()) - .unwrap_or(0), - ); + let scan_start_rc = self.get_block_scan_start(sortdb); debug!("{:?}: cannot make any more GetPoxInv requests for {:?}; proceeding to block inventory scan at reward cycle {}", &self.local_peer, nk, scan_start_rc); stats.reset_block_scan(scan_start_rc); @@ -1876,12 +1865,7 @@ impl PeerNetwork { // proceed with block scan. // If we're in IBD, then this is an always-allowed peer and we should // react to divergences by deepening our rescan. - let scan_start_rc = self.get_block_scan_start( - sortdb, - self.burnchain - .block_height_to_reward_cycle(stats.inv.get_block_height()) - .unwrap_or(0), - ); + let scan_start_rc = self.get_block_scan_start(sortdb); debug!( "{:?}: proceeding to block inventory scan for {:?} (diverged) at reward cycle {} (ibd={})", &self.local_peer, nk, scan_start_rc, ibd @@ -1982,12 +1966,7 @@ impl PeerNetwork { } // proceed to block scan. - let scan_start = self.get_block_scan_start( - sortdb, - self.burnchain - .block_height_to_reward_cycle(stats.inv.get_block_height()) - .unwrap_or(0), - ); + let scan_start = self.get_block_scan_start(sortdb); debug!( "{:?}: proceeding to block inventory scan for {:?} at reward cycle {}", &self.local_peer, nk, scan_start @@ -2368,7 +2347,6 @@ impl PeerNetwork { .unwrap_or(network.burnchain.reward_cycle_to_block_height( network.get_block_scan_start( sortdb, - network.pox_id.num_inventory_reward_cycles() as u64, ), )) .saturating_sub(sortdb.first_block_height); From 30ff6d8583fd31c8ae18c9cef883e6ee12844c12 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 17:04:05 -0400 Subject: [PATCH 41/56] chore: add a mode of generating burnchain blocks in TestPeer that ensures that a block's hash will depend on the (number of) opts it contains --- stackslib/src/net/mod.rs | 53 ++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index ddf0e6a71..bd064774c 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -2641,6 +2641,26 @@ pub mod test { &self.network.local_peer } + pub fn add_neighbor( + &mut self, + n: &mut Neighbor, + stacker_dbs: Option<&[QualifiedContractIdentifier]>, + bootstrap: bool, + ) { + let mut tx = self.network.peerdb.tx_begin().unwrap(); + n.save(&mut tx, stacker_dbs).unwrap(); + if bootstrap { + PeerDB::set_initial_peer( + &tx, + self.config.network_id, + &n.addr.addrbytes, + n.addr.port, + ) + .unwrap(); + } + tx.commit().unwrap(); + } + // TODO: DRY up from PoxSyncWatchdog pub fn infer_initial_burnchain_block_download( burnchain: &Burnchain, @@ -2849,7 +2869,15 @@ pub mod test { &mut self, blockstack_ops: Vec, ) -> (u64, BurnchainHeaderHash, ConsensusHash) { - let x = self.inner_next_burnchain_block(blockstack_ops, true, true, true); + let x = self.inner_next_burnchain_block(blockstack_ops, true, true, true, false); + (x.0, x.1, x.2) + } + + pub fn next_burnchain_block_diverge( + &mut self, + blockstack_ops: Vec, + ) -> (u64, BurnchainHeaderHash, ConsensusHash) { + let x = self.inner_next_burnchain_block(blockstack_ops, true, true, true, true); (x.0, x.1, x.2) } @@ -2862,14 +2890,14 @@ pub mod test { ConsensusHash, Option, ) { - self.inner_next_burnchain_block(blockstack_ops, true, true, true) + self.inner_next_burnchain_block(blockstack_ops, true, true, true, false) } pub fn next_burnchain_block_raw( &mut self, blockstack_ops: Vec, ) -> (u64, BurnchainHeaderHash, ConsensusHash) { - let x = self.inner_next_burnchain_block(blockstack_ops, false, false, true); + let x = self.inner_next_burnchain_block(blockstack_ops, false, false, true, false); (x.0, x.1, x.2) } @@ -2877,7 +2905,7 @@ pub mod test { &mut self, blockstack_ops: Vec, ) -> (u64, BurnchainHeaderHash, ConsensusHash) { - let x = self.inner_next_burnchain_block(blockstack_ops, false, false, false); + let x = self.inner_next_burnchain_block(blockstack_ops, false, false, false, false); (x.0, x.1, x.2) } @@ -2890,7 +2918,7 @@ pub mod test { ConsensusHash, Option, ) { - self.inner_next_burnchain_block(blockstack_ops, false, false, true) + self.inner_next_burnchain_block(blockstack_ops, false, false, true, false) } pub fn set_ops_consensus_hash( @@ -2921,6 +2949,7 @@ pub mod test { tip_block_height: u64, tip_block_hash: &BurnchainHeaderHash, num_ops: u64, + ops_determine_block_header: bool, ) -> BurnchainBlockHeader { test_debug!( "make_next_burnchain_block: tip_block_height={} tip_block_hash={} num_ops={}", @@ -2939,8 +2968,16 @@ pub mod test { let now = BURNCHAIN_TEST_BLOCK_TIME; let block_header_hash = BurnchainHeaderHash::from_bitcoin_hash( - &BitcoinIndexer::mock_bitcoin_header(&parent_hdr.block_hash, now as u32) - .bitcoin_hash(), + &BitcoinIndexer::mock_bitcoin_header( + &parent_hdr.block_hash, + (now as u32) + + if ops_determine_block_header { + num_ops as u32 + } else { + 0 + }, + ) + .bitcoin_hash(), ); test_debug!( "Block header hash at {} is {}", @@ -3012,6 +3049,7 @@ pub mod test { set_consensus_hash: bool, set_burn_hash: bool, update_burnchain: bool, + ops_determine_block_header: bool, ) -> ( u64, BurnchainHeaderHash, @@ -3035,6 +3073,7 @@ pub mod test { tip.block_height, &tip.burn_header_hash, blockstack_ops.len() as u64, + ops_determine_block_header, ); if set_burn_hash { From 1f6e1290eb7f87a810dd1775cb5ff552a1355a4d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 17:04:29 -0400 Subject: [PATCH 42/56] chore: API sync --- stackslib/src/net/tests/download/nakamoto.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stackslib/src/net/tests/download/nakamoto.rs b/stackslib/src/net/tests/download/nakamoto.rs index 73472c9c5..31c42c8af 100644 --- a/stackslib/src/net/tests/download/nakamoto.rs +++ b/stackslib/src/net/tests/download/nakamoto.rs @@ -1954,6 +1954,7 @@ fn test_nakamoto_download_run_2_peers() { sn.block_height, &sn.burn_header_hash, ops.len() as u64, + false, ); TestPeer::add_burnchain_block(&boot_peer.config.burnchain, &block_header, ops.clone()); } @@ -2140,6 +2141,7 @@ fn test_nakamoto_unconfirmed_download_run_2_peers() { sn.block_height, &sn.burn_header_hash, ops.len() as u64, + false, ); TestPeer::add_burnchain_block(&boot_peer.config.burnchain, &block_header, ops.clone()); } From d6f5ce6c70755cca79844bf26456efe08faa75f8 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 3 May 2024 17:04:35 -0400 Subject: [PATCH 43/56] chore: get all unit tests to pass --- stackslib/src/net/tests/inv/epoch2x.rs | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 3d349e767..a2b2da5bb 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -1248,12 +1248,15 @@ fn test_sync_inv_2_peers_plain() { let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); let mut peer_2_config = TestPeerConfig::new(function_name!(), 0, 0); - peer_1_config.add_neighbor(&peer_2_config.to_neighbor()); - peer_2_config.add_neighbor(&peer_1_config.to_neighbor()); + peer_1_config.connection_opts.inv_reward_cycles = 10; + peer_2_config.connection_opts.inv_reward_cycles = 10; let mut peer_1 = TestPeer::new(peer_1_config); let mut peer_2 = TestPeer::new(peer_2_config); + peer_1.add_neighbor(&mut peer_2.to_neighbor(), None, true); + peer_2.add_neighbor(&mut peer_1.to_neighbor(), None, true); + let num_blocks = (GETPOXINV_MAX_BITLEN * 2) as u64; let first_stacks_block_height = { let sn = @@ -1422,12 +1425,15 @@ fn test_sync_inv_2_peers_stale() { let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); let mut peer_2_config = TestPeerConfig::new(function_name!(), 0, 0); - peer_1_config.add_neighbor(&peer_2_config.to_neighbor()); - peer_2_config.add_neighbor(&peer_1_config.to_neighbor()); + peer_1_config.connection_opts.inv_reward_cycles = 10; + peer_2_config.connection_opts.inv_reward_cycles = 10; let mut peer_1 = TestPeer::new(peer_1_config); let mut peer_2 = TestPeer::new(peer_2_config); + peer_1.add_neighbor(&mut peer_2.to_neighbor(), None, true); + peer_2.add_neighbor(&mut peer_1.to_neighbor(), None, true); + let num_blocks = (GETPOXINV_MAX_BITLEN * 2) as u64; let first_stacks_block_height = { let sn = @@ -1525,14 +1531,17 @@ fn test_sync_inv_2_peers_unstable() { let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); let mut peer_2_config = TestPeerConfig::new(function_name!(), 0, 0); - let stable_confs = peer_1_config.burnchain.stable_confirmations as u64; + peer_1_config.connection_opts.inv_reward_cycles = 10; + peer_2_config.connection_opts.inv_reward_cycles = 10; - peer_1_config.add_neighbor(&peer_2_config.to_neighbor()); - peer_2_config.add_neighbor(&peer_1_config.to_neighbor()); + let stable_confs = peer_1_config.burnchain.stable_confirmations as u64; let mut peer_1 = TestPeer::new(peer_1_config); let mut peer_2 = TestPeer::new(peer_2_config); + peer_1.add_neighbor(&mut peer_2.to_neighbor(), None, true); + peer_2.add_neighbor(&mut peer_1.to_neighbor(), None, true); + let num_blocks = (GETPOXINV_MAX_BITLEN * 2) as u64; let first_stacks_block_height = { @@ -1559,7 +1568,7 @@ fn test_sync_inv_2_peers_unstable() { } else { // peer 1 diverges test_debug!("Peer 1 diverges at {}", i + first_stacks_block_height); - peer_1.next_burnchain_block(vec![]); + peer_1.next_burnchain_block_diverge(vec![burn_ops[0].clone()]); } } @@ -1734,8 +1743,8 @@ fn test_sync_inv_2_peers_different_pox_vectors() { let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); let mut peer_2_config = TestPeerConfig::new(function_name!(), 0, 0); - peer_1_config.add_neighbor(&peer_2_config.to_neighbor()); - peer_2_config.add_neighbor(&peer_1_config.to_neighbor()); + peer_1_config.connection_opts.inv_reward_cycles = 10; + peer_2_config.connection_opts.inv_reward_cycles = 10; let reward_cycle_length = peer_1_config.burnchain.pox_constants.reward_cycle_length as u64; assert_eq!(reward_cycle_length, 5); @@ -1743,6 +1752,9 @@ fn test_sync_inv_2_peers_different_pox_vectors() { let mut peer_1 = TestPeer::new(peer_1_config); let mut peer_2 = TestPeer::new(peer_2_config); + peer_1.add_neighbor(&mut peer_2.to_neighbor(), None, true); + peer_2.add_neighbor(&mut peer_1.to_neighbor(), None, true); + let num_blocks = (GETPOXINV_MAX_BITLEN * 3) as u64; let first_stacks_block_height = { From 1f180d785559d956f03bbd3f1ba45b03b59e403b Mon Sep 17 00:00:00 2001 From: Hank Stoever Date: Mon, 6 May 2024 07:07:33 -0700 Subject: [PATCH 44/56] feat: add no-op metrics timer when feature disabled --- stacks-signer/src/client/stacks_client.rs | 16 +-------------- stacks-signer/src/monitoring/mod.rs | 24 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index a2dfc09ea..9cef3da9a 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -207,7 +207,6 @@ impl StacksClient { estimated_len: Some(tx.tx_len()), transaction_payload: to_hex(&tx.payload.serialize_to_vec()), }; - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&self.fees_transaction_path(), &self.http_origin); let send_request = || { @@ -218,12 +217,11 @@ impl StacksClient { .send() .map_err(backoff::Error::transient) }; - #[cfg(feature = "monitoring_prom")] - timer.stop_and_record(); let response = retry_with_exponential_backoff(send_request)?; if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); } + timer.stop_and_record(); let fee_estimate_response = response.json::()?; let fee = fee_estimate_response .estimations @@ -273,7 +271,6 @@ impl StacksClient { block, chain_id: self.chain_id, }; - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&self.block_proposal_path(), &self.http_origin); let send_request = || { @@ -287,7 +284,6 @@ impl StacksClient { }; let response = retry_with_exponential_backoff(send_request)?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -365,7 +361,6 @@ impl StacksClient { /// Get the current peer info data from the stacks node pub fn get_peer_info(&self) -> Result { debug!("Getting stacks node info..."); - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&self.core_info_path(), &self.http_origin); let send_request = || { @@ -375,7 +370,6 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -417,7 +411,6 @@ impl StacksClient { reward_cycle: u64, ) -> Result>, ClientError> { debug!("Getting reward set for reward cycle {reward_cycle}..."); - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer( &self.reward_set_path(reward_cycle), &self.http_origin, @@ -429,7 +422,6 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -489,7 +481,6 @@ impl StacksClient { address: &StacksAddress, ) -> Result { debug!("Getting account info..."); - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&self.accounts_path(address), &self.http_origin); let send_request = || { @@ -499,7 +490,6 @@ impl StacksClient { .map_err(backoff::Error::transient) }; let response = retry_with_exponential_backoff(send_request)?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -567,7 +557,6 @@ impl StacksClient { pub fn submit_transaction(&self, tx: &StacksTransaction) -> Result { let txid = tx.txid(); let tx = tx.serialize_to_vec(); - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&self.transaction_path(), &self.http_origin); let send_request = || { @@ -582,7 +571,6 @@ impl StacksClient { }) }; let response = retry_with_exponential_backoff(send_request)?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); @@ -612,7 +600,6 @@ impl StacksClient { let body = json!({"sender": self.stacks_address.to_string(), "arguments": args}).to_string(); let path = self.read_only_path(contract_addr, contract_name, function_name); - #[cfg(feature = "monitoring_prom")] let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin); let response = self .stacks_node_client @@ -620,7 +607,6 @@ impl StacksClient { .header("Content-Type", "application/json") .body(body) .send()?; - #[cfg(feature = "monitoring_prom")] timer.stop_and_record(); if !response.status().is_success() { return Err(ClientError::RequestFailure(response.status())); diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs index 3674685da..484a09d77 100644 --- a/stacks-signer/src/monitoring/mod.rs +++ b/stacks-signer/src/monitoring/mod.rs @@ -148,6 +148,19 @@ pub fn new_rpc_call_timer(full_path: &str, origin: &String) -> HistogramTimer { histogram.start_timer() } +/// NoOp timer uses for monitoring when the monitoring feature is not enabled. +pub struct NoOpTimer; +impl NoOpTimer { + /// NoOp method to stop recording when the monitoring feature is not enabled. + pub fn stop_and_record(&self) {} +} + +/// Stop and record the no-op timer. +#[cfg(not(feature = "monitoring_prom"))] +pub fn new_rpc_call_timer(_full_path: &str, _origin: &String) -> NoOpTimer { + NoOpTimer +} + /// Start serving monitoring metrics. /// This will only serve the metrics if the `monitoring_prom` feature is enabled. #[allow(unused_variables)] @@ -161,14 +174,15 @@ pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), Stri .name("signer_metrics".to_string()) .spawn(move || { if let Err(monitoring_err) = server::MonitoringServer::start(&config) { - error!( - "Monitoring: Error starting metrics server: {:?}", - monitoring_err - ); + error!("Monitoring: Error in metrics server: {:?}", monitoring_err); } }); } #[cfg(not(feature = "monitoring_prom"))] - warn!("Not starting monitoring metrics server as the monitoring_prom feature is not enabled"); + { + if config.metrics_endpoint.is_some() { + warn!("Not starting monitoring metrics server as the monitoring_prom feature is not enabled"); + } + } Ok(()) } From 07c6ac84d1bc1deddbd2da5d4508e49b76bbeef1 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 6 May 2024 12:04:41 -0400 Subject: [PATCH 45/56] chore: add unit test --- stackslib/src/net/inv/epoch2x.rs | 10 +++--- stackslib/src/net/tests/inv/epoch2x.rs | 50 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index c0dd04152..cd853f687 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -1753,7 +1753,7 @@ impl PeerNetwork { } /// Determine at which reward cycle to begin scanning inventories - fn get_block_scan_start(&self, sortdb: &SortitionDB) -> u64 { + pub(crate) fn get_block_scan_start(&self, sortdb: &SortitionDB) -> u64 { // see if the stacks tip affirmation map and heaviest affirmation map diverge. If so, then // start scaning at the reward cycle just before that. let am_rescan_rc = self @@ -1786,10 +1786,12 @@ impl PeerNetwork { let rescan_rc = cmp::min(am_rescan_rc, start_reward_cycle); test_debug!( - "begin blocks inv scan at {} = min({},{})", + "begin blocks inv scan at {} = min({},{}) stacks_tip_am={} heaviest_am={}", rescan_rc, - stacks_tip_rc, - am_rescan_rc + am_rescan_rc, + start_reward_cycle, + &self.stacks_tip_affirmation_map, + &self.heaviest_affirmation_map ); rescan_rc } diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index a2b2da5bb..bd21eee96 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -1241,6 +1241,56 @@ fn test_sync_inv_diagnose_nack() { ); } +#[test] +fn test_inv_sync_start_reward_cycle() { + let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); + peer_1_config.connection_opts.inv_reward_cycles = 0; + + let mut peer_1 = TestPeer::new(peer_1_config); + + let num_blocks = (GETPOXINV_MAX_BITLEN * 2) as u64; + for i in 0..num_blocks { + let (burn_ops, stacks_block, microblocks) = peer_1.make_default_tenure(); + peer_1.next_burnchain_block(burn_ops.clone()); + peer_1.process_stacks_epoch_at_tip(&stacks_block, µblocks); + } + + let _ = peer_1.step(); + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + assert_eq!(block_scan_start, 7); + + peer_1.network.connection_opts.inv_reward_cycles = 1; + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + assert_eq!(block_scan_start, 7); + + peer_1.network.connection_opts.inv_reward_cycles = 2; + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + assert_eq!(block_scan_start, 6); + + peer_1.network.connection_opts.inv_reward_cycles = 3; + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + assert_eq!(block_scan_start, 5); + + peer_1.network.connection_opts.inv_reward_cycles = 300; + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + assert_eq!(block_scan_start, 0); +} + #[test] #[ignore] fn test_sync_inv_2_peers_plain() { From a04c1fa6820437306a6fcb225aaf45ba6d9d8e9c Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 6 May 2024 13:16:49 -0400 Subject: [PATCH 46/56] chore: add unit test --- stackslib/src/burnchains/tests/db.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/stackslib/src/burnchains/tests/db.rs b/stackslib/src/burnchains/tests/db.rs index 9c3b5ee47..5a8d958f1 100644 --- a/stackslib/src/burnchains/tests/db.rs +++ b/stackslib/src/burnchains/tests/db.rs @@ -231,6 +231,15 @@ fn test_store_and_fetch() { } assert_eq!(&header, &non_canonical_block.header()); + // when we get a block header by its height, it's canonical + for (height, header) in headers.iter().enumerate() { + let hdr = BurnchainDB::get_burnchain_header(burnchain_db.conn(), &headers, height as u64) + .unwrap() + .unwrap(); + assert!(headers.iter().find(|h| **h == hdr).is_some()); + assert_ne!(hdr, non_canonical_block.header()); + } + let looked_up_canon = burnchain_db.get_canonical_chain_tip().unwrap(); assert_eq!(&looked_up_canon, &canonical_block.header()); From fa66891c7863d20da370918b9b006a7e72aa9009 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 6 May 2024 16:08:11 -0400 Subject: [PATCH 47/56] chore: address PR feedback and add more test coverage --- stackslib/src/burnchains/burnchain.rs | 2 +- stackslib/src/chainstate/burn/atc.rs | 578 ++++++++++-------- stackslib/src/chainstate/burn/db/sortdb.rs | 41 +- .../src/chainstate/burn/operations/mod.rs | 3 +- stackslib/src/chainstate/burn/sortition.rs | 11 +- stackslib/src/main.rs | 439 ++++++------- 6 files changed, 588 insertions(+), 486 deletions(-) diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 1f326f4f3..0247a5451 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -221,7 +221,7 @@ impl BurnchainStateTransition { .unwrap_or_else(|| { panic!( "FATAL: no epoch defined at burn height {}", - parent_snapshot.block_height + 1 + parent_snapshot.block_height - u64::from(epoch_id.mining_commitment_window()) ) }) .epoch_id; diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index 0a4912753..dadb4b07a 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -63,11 +63,16 @@ impl AtcRational { (self.0 >> 64).low_u64() } + /// Is this value overflowed? + pub fn is_overflowed(&self) -> bool { + self.0 > Self::max().0 + } + /// Checked addition pub fn add(&self, other: &AtcRational) -> Option { // NOTE: this is always safe since u128::MAX + u128::MAX < Uint256::max() let sum = AtcRational(self.0 + other.0); - if sum.0 > Self::max().0 { + if sum.is_overflowed() { return None; } Some(sum) @@ -85,7 +90,7 @@ impl AtcRational { pub fn mul(&self, other: &AtcRational) -> Option { // NOTE: this is always safe since u128::MAX * u128::MAX < Uint256::max() let prod = AtcRational((self.0 * other.0) >> 64); - if prod.0 > Self::max().0 { + if prod.is_overflowed() { return None; } Some(prod) @@ -105,265 +110,17 @@ impl AtcRational { self.0.to_hex_be() } + /// Inner u256 ref + pub fn inner(&self) -> &Uint256 { + &self.0 + } + /// Inner u256, for conversion to something a BurnSamplePoint can use pub fn into_inner(self) -> Uint256 { self.0 } } -#[cfg(test)] -mod test { - use stacks_common::util::hash::to_hex; - use stacks_common::util::uint::Uint256; - - use crate::chainstate::burn::atc::AtcRational; - use crate::chainstate::burn::BlockSnapshot; - - impl AtcRational { - /// Convert to f64, and panic on conversion failure - pub fn to_f64(&self) -> f64 { - let ipart = self.ipart() as f64; - let fpart = self.0.low_u64() as f64; - ipart + (fpart / (u64::MAX as f64)) - } - - /// Convert from f64 between 0 and 1, panicking on conversion failure. Scales up the f64 so that its - /// fractional parts reside in the lower 64 bits of the AtcRational. - pub fn from_f64_unit(value: f64) -> Self { - if value < 0.0 || value >= 1.0 { - panic!("only usable for values in [0.0, 1.0) range"); - } - - // NOTE: this only changes the exponent, not the mantissa. - // Moreover, u128::from(u64::MAX) + 1 has f64 representation 0x43f0000000000000, so these conversions are safe. - let scaled_value = value * ((u128::from(u64::MAX) + 1) as f64); - - // this is safe, because 0.0 <= value < 1.0, so scaled_value <= u64::MAX - let value_u64 = scaled_value as u64; - Self(Uint256::from_u64(value_u64)) - } - } - - #[test] - fn test_atc_rational() { - assert_eq!(AtcRational::frac(1, 1), AtcRational::one()); - assert_eq!( - AtcRational::frac(1, 2).0, - Uint256::from_u64(u64::MAX / 2) + Uint256::from_u64(1) - ); - assert_eq!( - AtcRational::frac(1, 4).0, - Uint256::from_u64(u64::MAX / 4) + Uint256::from_u64(1) - ); - assert_eq!( - AtcRational::frac(1, 8).0, - Uint256::from_u64(u64::MAX / 8) + Uint256::from_u64(1) - ); - assert_eq!( - AtcRational::frac(1, 16).0, - Uint256::from_u64(u64::MAX / 16) + Uint256::from_u64(1) - ); - assert_eq!( - AtcRational::frac(1, 32).0, - Uint256::from_u64(u64::MAX / 32) + Uint256::from_u64(1) - ); - - assert_eq!( - AtcRational::frac(1, 2) - .add(&AtcRational::frac(1, 2)) - .unwrap(), - AtcRational::one() - ); - assert_eq!( - AtcRational::frac(1, 4) - .add(&AtcRational::frac(1, 4)) - .unwrap(), - AtcRational::frac(1, 2) - ); - assert_eq!( - AtcRational::frac(1, 8) - .add(&AtcRational::frac(1, 8)) - .unwrap(), - AtcRational::frac(1, 4) - ); - assert_eq!( - AtcRational::frac(3, 8) - .add(&AtcRational::frac(3, 8)) - .unwrap(), - AtcRational::frac(3, 4) - ); - assert_eq!( - AtcRational::max().add(&AtcRational(Uint256::from_u64(1))), - None - ); - - assert_eq!( - AtcRational::frac(1, 2) - .sub(&AtcRational::frac(1, 2)) - .unwrap(), - AtcRational::zero() - ); - - assert_eq!( - AtcRational::one().sub(&AtcRational::frac(1, 2)).unwrap(), - AtcRational::frac(1, 2) - ); - assert_eq!( - AtcRational::one().sub(&AtcRational::frac(1, 32)).unwrap(), - AtcRational::frac(31, 32) - ); - - assert_eq!( - AtcRational::frac(1, 2) - .mul(&AtcRational::frac(1, 2)) - .unwrap(), - AtcRational::frac(1, 4) - ); - assert_eq!( - AtcRational::frac(5, 6) - .mul(&AtcRational::frac(7, 8)) - .unwrap(), - AtcRational::frac(35, 48) - ); - assert_eq!( - AtcRational::frac(100, 2) - .mul(&AtcRational::frac(200, 4)) - .unwrap(), - AtcRational::frac(20000, 8) - ); - assert_eq!( - AtcRational::frac(1, 2) - .mul(&AtcRational::frac(1024, 1)) - .unwrap(), - AtcRational::frac(512, 1) - ); - - assert_eq!( - AtcRational::frac(1, 2).min(&AtcRational::frac(15, 32)), - AtcRational::frac(15, 32) - ); - } - - #[test] - #[ignore] - fn print_functions() { - let mut grid: Vec> = vec![vec![' '; 100]; 102]; - for i in 0..100 { - let f_atc = (i as f64) / 100.0; - let atc = AtcRational::frac(i as u64, 100); - let l_atc = BlockSnapshot::null_miner_logistic(atc).to_f64(); - let p_atc = BlockSnapshot::null_miner_probability(atc).to_f64(); - - // NOTE: columns increase downwards, so flip this - let l_atc_100 = 100 - ((l_atc * 100.0) as usize); - let p_atc_100 = 100 - ((p_atc * 100.0) as usize); - let a_atc_100 = 100 - (((1.0 - f_atc) * 100.0) as usize); - grid[a_atc_100][i] = '$'; - grid[l_atc_100][i] = '#'; - grid[p_atc_100][i] = '^'; - } - for j in 0..100 { - grid[101][j] = '_'; - } - - println!(""); - for row in grid.iter() { - let grid_str: String = row.clone().into_iter().collect(); - println!("|{}", &grid_str); - } - } - - /// Calculate the logic advantage curve for the null miner. - /// This function's parameters are chosen such that: - /// * if the ATC carryover has diminished by less than 20%, the null miner has negligible - /// chances of winning. This is to avoid punishing honest miners when there are flash blocks. - /// * If the ATC carryover has diminished by between 20% and 80%, the null miner has a - /// better-than-linear probability of winning. That is, if the burnchain MEV miner pays less - /// than X% of the expected carryover (20% <= X < 80%), then their probability of winning is - /// (1) strictly less than X%, and (2) strictly less than any Pr[X% - c] for 0 < c < X. - /// * If the ATC carryover is less than 20%, the null miner has an overwhelmingly likely chance - /// of winning (>95%). - /// - /// The logistic curve fits the points (atc=0.2, null_prob=0.75) and (atc=0.8, null_prob=0.01). - fn null_miner_logistic(atc: f64) -> f64 { - // recall the inverted logistic function: - // - // L - // f(x) = --------------------- - // -k * (x0 - x) - // 1 + e - // - // It is shaped like a *backwards* "S" -- it approaches L as `x` tends towards negative - // infinity, and it approaches 0 as `x` tends towards positive infinity. This function is - // the null miner advantage function, where `x` is the ATC carryover value. - // - // We need to drive x0 and k from our two points: - // - // (x1, y1) = (0.2, 0.75) - // (x2, y2) = (0.8, 0.01) - // - // to derive L, x0, and k: - // L = 0.8 - // z = ln(L/y1 - 1) / ln(L/y2 - 1) - // x0 = (x1 - z * x2) / (1 - z) - // k = ln(L/y1 - 1) / (x1 - x0) - // - // The values for x0 and k were generated with the following GNU bc script: - // ``` - // $ cat /tmp/variables.bc - // scale=32 - // supremum=0.8 /* this is L */ - // x1=0.2 - // y1=0.75 - // x2=0.8 - // y2=0.01 - // z=l(supremum/y1 - 1)/l(supremum/y2 -1) - // x0=(x1 - z * x2)/(1 - z) - // k=l(supremum/y1 - 1)/(x1 - x0) - // print "x0 = "; x0 - // print "k = "; k - // ``` - // - // This script evaluates to: - // ``` - // $ bc -l < /tmp/variables.bc - // x0 = .42957690816204645842320195118064 - // k = 11.79583008928205260028158351938437 - // ``` - - let L: f64 = 0.8; - - // truncated f64 - let x0: f64 = 0.42957690816204647; - let k: f64 = 11.795830089282052; - - // natural logarithm constant - let e: f64 = 2.718281828459045; - - let adv = L / (1.0 + e.powf(-k * (x0 - atc))); - adv - } - - #[test] - fn make_null_miner_lookup_table() { - use crate::chainstate::burn::atc::ATC_LOOKUP; - let mut lookup_table = Vec::with_capacity(1024); - for atc in 0..1024 { - let fatc = (atc as f64) / 1024.0; - let lgst_fatc = null_miner_logistic(fatc); - let lgst_rational = AtcRational::from_f64_unit(lgst_fatc); - assert_eq!(ATC_LOOKUP[atc], lgst_rational); - lookup_table.push(lgst_rational); - } - println!("["); - for lt in lookup_table.into_iter() { - let inner = lt.into_inner(); - println!(" AtcRational(Uint256({:?})),", &inner.0); - } - println!("]"); - } -} - /// Pre-calculated 1024-member lookup table for the null miner advantage function, as AtcRational /// fixed point integers. The first item corresponds to the value of the function at 0.0, and the /// last item corresponds to the function at 1.0 - (1.0 / 1024.0). The input to a function is the @@ -1401,3 +1158,314 @@ pub(crate) const ATC_LOOKUP: [AtcRational; 1024] = [ AtcRational(Uint256([18041977930440052, 0, 0, 0])), AtcRational(Uint256([17835588001385282, 0, 0, 0])), ]; + +#[cfg(test)] +mod test { + use stacks_common::util::hash::to_hex; + use stacks_common::util::uint::Uint256; + + use crate::chainstate::burn::atc::AtcRational; + use crate::chainstate::burn::BlockSnapshot; + use crate::stacks_common::util::uint::BitArray; + + impl AtcRational { + /// Convert to f64, and panic on conversion failure + pub fn to_f64(&self) -> f64 { + let ipart = self.ipart() as f64; + let fpart = self.0.low_u64() as f64; + ipart + (fpart / (u64::MAX as f64)) + } + + /// Convert from f64 between 0 and 1, panicking on conversion failure. Scales up the f64 so that its + /// fractional parts reside in the lower 64 bits of the AtcRational. + pub fn from_f64_unit(value: f64) -> Self { + if value < 0.0 || value >= 1.0 { + panic!("only usable for values in [0.0, 1.0) range"); + } + + // NOTE: this only changes the exponent, not the mantissa. + // Moreover, u128::from(u64::MAX) + 1 has f64 representation 0x43f0000000000000, so these conversions are safe. + let scaled_value = value * ((u128::from(u64::MAX) + 1) as f64); + + // this is safe, because 0.0 <= value < 1.0, so scaled_value <= u64::MAX + let value_u64 = scaled_value as u64; + Self(Uint256::from_u64(value_u64)) + } + } + + #[test] + fn test_atc_rational() { + // zero + assert_eq!(AtcRational::zero().into_inner(), Uint256::from_u64(0)); + + // one + assert_eq!(AtcRational::one().into_inner(), Uint256::one() << 64); + + // one_sup + assert_eq!( + AtcRational::one_sup().into_inner(), + (Uint256::one() << 64) - Uint256::from_u64(1) + ); + + // max + assert_eq!( + AtcRational::max().into_inner(), + (Uint256::from_u64(u64::MAX) << 64) | Uint256::from_u64(u64::MAX) + ); + + // ipart + assert_eq!(AtcRational::one().ipart(), 1); + assert_eq!(AtcRational::frac(1, 2).ipart(), 0); + assert_eq!(AtcRational::frac(3, 2).ipart(), 1); + assert_eq!(AtcRational::frac(4, 2).ipart(), 2); + assert_eq!(AtcRational::frac(9999, 10000).ipart(), 0); + + // to_f64 + assert_eq!(AtcRational::one().to_f64(), 1.0); + assert_eq!(AtcRational::zero().to_f64(), 0.0); + assert_eq!(AtcRational::frac(1, 2).to_f64(), 0.5); + assert_eq!(AtcRational::frac(1, 32).to_f64(), 0.03125); + + // from_f64_unit + assert_eq!(AtcRational::from_f64_unit(0.0), AtcRational::zero()); + assert_eq!(AtcRational::from_f64_unit(0.5), AtcRational::frac(1, 2)); + assert_eq!( + AtcRational::from_f64_unit(0.03125), + AtcRational::frac(1, 32) + ); + + // is_overflowed + assert!(!AtcRational::max().is_overflowed()); + assert!( + AtcRational(AtcRational::max().into_inner() + Uint256::from_u64(1)).is_overflowed() + ); + assert!(AtcRational::max() + .add(&AtcRational(Uint256::from_u64(1))) + .is_none()); + + // frac constructor produces values between 0 and u64::MAX + assert_eq!(AtcRational::frac(1, 1), AtcRational::one()); + assert_eq!( + AtcRational::frac(1, 2).0, + Uint256::from_u64(u64::MAX / 2) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 4).0, + Uint256::from_u64(u64::MAX / 4) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 8).0, + Uint256::from_u64(u64::MAX / 8) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 16).0, + Uint256::from_u64(u64::MAX / 16) + Uint256::from_u64(1) + ); + assert_eq!( + AtcRational::frac(1, 32).0, + Uint256::from_u64(u64::MAX / 32) + Uint256::from_u64(1) + ); + + // fractions auto-normalize + assert_eq!(AtcRational::frac(2, 4), AtcRational::frac(1, 2)); + assert_eq!(AtcRational::frac(100, 400), AtcRational::frac(1, 4)); + assert_eq!(AtcRational::frac(5, 25), AtcRational::frac(1, 5)); + + // fractions can be added + assert_eq!( + AtcRational::frac(1, 2) + .add(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::one() + ); + assert_eq!( + AtcRational::frac(1, 4) + .add(&AtcRational::frac(1, 4)) + .unwrap(), + AtcRational::frac(1, 2) + ); + assert_eq!( + AtcRational::frac(1, 8) + .add(&AtcRational::frac(1, 8)) + .unwrap(), + AtcRational::frac(1, 4) + ); + assert_eq!( + AtcRational::frac(3, 8) + .add(&AtcRational::frac(3, 8)) + .unwrap(), + AtcRational::frac(3, 4) + ); + assert_eq!( + AtcRational::max().add(&AtcRational(Uint256::from_u64(1))), + None + ); + + // fractions can be subtracted + assert_eq!( + AtcRational::frac(1, 2) + .sub(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::zero() + ); + assert_eq!( + AtcRational::one().sub(&AtcRational::frac(1, 2)).unwrap(), + AtcRational::frac(1, 2) + ); + assert_eq!( + AtcRational::one().sub(&AtcRational::frac(1, 32)).unwrap(), + AtcRational::frac(31, 32) + ); + + // fractions can be multiplied + assert_eq!( + AtcRational::frac(1, 2) + .mul(&AtcRational::frac(1, 2)) + .unwrap(), + AtcRational::frac(1, 4) + ); + assert_eq!( + AtcRational::frac(5, 6) + .mul(&AtcRational::frac(7, 8)) + .unwrap(), + AtcRational::frac(35, 48) + ); + assert_eq!( + AtcRational::frac(100, 2) + .mul(&AtcRational::frac(200, 4)) + .unwrap(), + AtcRational::frac(20000, 8) + ); + assert_eq!( + AtcRational::frac(1, 2) + .mul(&AtcRational::frac(1024, 1)) + .unwrap(), + AtcRational::frac(512, 1) + ); + + assert_eq!( + AtcRational::frac(1, 2).min(&AtcRational::frac(15, 32)), + AtcRational::frac(15, 32) + ); + } + + #[test] + #[ignore] + fn print_functions() { + let mut grid: Vec> = vec![vec![' '; 100]; 102]; + for i in 0..100 { + let f_atc = (i as f64) / 100.0; + let atc = AtcRational::frac(i as u64, 100); + let l_atc = BlockSnapshot::null_miner_logistic(atc).to_f64(); + let p_atc = BlockSnapshot::null_miner_probability(atc).to_f64(); + + // NOTE: columns increase downwards, so flip this + let l_atc_100 = 100 - ((l_atc * 100.0) as usize); + let p_atc_100 = 100 - ((p_atc * 100.0) as usize); + let a_atc_100 = 100 - (((1.0 - f_atc) * 100.0) as usize); + grid[a_atc_100][i] = '$'; + grid[l_atc_100][i] = '#'; + grid[p_atc_100][i] = '^'; + } + for j in 0..100 { + grid[101][j] = '_'; + } + + println!(""); + for row in grid.iter() { + let grid_str: String = row.clone().into_iter().collect(); + println!("|{}", &grid_str); + } + } + + /// Calculate the logic advantage curve for the null miner. + /// This function's parameters are chosen such that: + /// * if the ATC carryover has diminished by less than 20%, the null miner has negligible + /// chances of winning. This is to avoid punishing honest miners when there are flash blocks. + /// * If the ATC carryover has diminished by between 20% and 80%, the null miner has a + /// better-than-linear probability of winning. That is, if the burnchain MEV miner pays less + /// than X% of the expected carryover (20% <= X < 80%), then their probability of winning is + /// (1) strictly less than X%, and (2) strictly less than any Pr[X% - c] for 0 < c < X. + /// * If the ATC carryover is less than 20%, the null miner has an overwhelmingly likely chance + /// of winning (>95%). + /// + /// The logistic curve fits the points (atc=0.2, null_prob=0.75) and (atc=0.8, null_prob=0.01). + fn null_miner_logistic(atc: f64) -> f64 { + // recall the inverted logistic function: + // + // L + // f(x) = --------------------- + // -k * (x0 - x) + // 1 + e + // + // It is shaped like a *backwards* "S" -- it approaches L as `x` tends towards negative + // infinity, and it approaches 0 as `x` tends towards positive infinity. This function is + // the null miner advantage function, where `x` is the ATC carryover value. + // + // We need to drive x0 and k from our two points: + // + // (x1, y1) = (0.2, 0.75) + // (x2, y2) = (0.8, 0.01) + // + // to derive L, x0, and k: + // L = 0.8 + // z = ln(L/y1 - 1) / ln(L/y2 - 1) + // x0 = (x1 - z * x2) / (1 - z) + // k = ln(L/y1 - 1) / (x1 - x0) + // + // The values for x0 and k were generated with the following GNU bc script: + // ``` + // $ cat /tmp/variables.bc + // scale=32 + // supremum=0.8 /* this is L */ + // x1=0.2 + // y1=0.75 + // x2=0.8 + // y2=0.01 + // z=l(supremum/y1 - 1)/l(supremum/y2 -1) + // x0=(x1 - z * x2)/(1 - z) + // k=l(supremum/y1 - 1)/(x1 - x0) + // print "x0 = "; x0 + // print "k = "; k + // ``` + // + // This script evaluates to: + // ``` + // $ bc -l < /tmp/variables.bc + // x0 = .42957690816204645842320195118064 + // k = 11.79583008928205260028158351938437 + // ``` + + let L: f64 = 0.8; + + // truncated f64 + let x0: f64 = 0.42957690816204647; + let k: f64 = 11.795830089282052; + + // natural logarithm constant + let e: f64 = 2.718281828459045; + + let adv = L / (1.0 + e.powf(-k * (x0 - atc))); + adv + } + + #[test] + fn make_null_miner_lookup_table() { + use crate::chainstate::burn::atc::ATC_LOOKUP; + let mut lookup_table = Vec::with_capacity(1024); + for atc in 0..1024 { + let fatc = (atc as f64) / 1024.0; + let lgst_fatc = null_miner_logistic(fatc); + let lgst_rational = AtcRational::from_f64_unit(lgst_fatc); + assert_eq!(ATC_LOOKUP[atc], lgst_rational); + assert_eq!(ATC_LOOKUP[atc].to_f64(), lgst_fatc); + lookup_table.push(lgst_rational); + } + println!("["); + for lt in lookup_table.into_iter() { + let inner = lt.into_inner(); + println!(" AtcRational(Uint256({:?})),", &inner.0); + } + println!("]"); + } +} diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 674bb5563..e3802d6ec 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -770,13 +770,30 @@ const SORTITION_DB_INDEXES: &'static [&'static str] = &[ "CREATE INDEX IF NOT EXISTS index_block_commits_by_sender ON block_commits(apparent_sender);" ]; +/// Handle to the sortition database, a MARF'ed sqlite DB on disk. +/// It stores information pertaining to cryptographic sortitions performed in each Bitcoin block -- +/// either to select the next Stacks block (in epoch 2.5 and earlier), or to choose the next Stacks +/// miner (epoch 3.0 and later). pub struct SortitionDB { + /// Whether or not write operations are permitted. Pertains to whether or not transaction + /// objects can be created or schema migrations can happen on this SortitionDB instance. pub readwrite: bool, + /// If true, then while write operations will be permitted, they will not be committed (and may + /// even be skipped). This is not used in production; it's used in the `stacks-inspect` tool + /// to simulate what could happen (e.g. to replay sortitions with different anti-MEV strategies + /// without corrupting the underlying DB). pub dryrun: bool, + /// Handle to the MARF which stores an index over each burnchain and PoX fork. pub marf: MARF, + /// First burnchain block height at which sortitions will be considered. All Stacks epochs + /// besides epoch 1.0 must start at or after this height. pub first_block_height: u64, + /// Hash of the first burnchain block at which sortitions will be considered. pub first_burn_header_hash: BurnchainHeaderHash, + /// PoX constants that pertain to this DB, for purposes of (but not limited to) evaluating PoX + /// reward cycles and evaluating block-commit validity within a PoX reward cycle pub pox_constants: PoxConstants, + /// Path on disk from which this DB was opened (caller-given; not resolved). pub path: String, } @@ -3615,18 +3632,6 @@ impl SortitionDB { .try_into() .ok() } - - /// Get the last block-commit from a given sender - pub fn get_last_block_commit_by_sender( - conn: &DBConn, - sender: &BurnchainSigner, - ) -> Result, db_error> { - let apparent_sender_str = - serde_json::to_string(sender).map_err(|e| db_error::SerializationError(e))?; - let sql = "SELECT * FROM block_commits WHERE apparent_sender = ?1 ORDER BY block_height DESC LIMIT 1"; - let args = rusqlite::params![&apparent_sender_str]; - query_row(conn, sql, args) - } } impl<'a> SortitionDBTx<'a> { @@ -6771,6 +6776,18 @@ pub mod tests { } Ok(ret) } + + /// Get the last block-commit from a given sender + pub fn get_last_block_commit_by_sender( + conn: &DBConn, + sender: &BurnchainSigner, + ) -> Result, db_error> { + let apparent_sender_str = + serde_json::to_string(sender).map_err(|e| db_error::SerializationError(e))?; + let sql = "SELECT * FROM block_commits WHERE apparent_sender = ?1 ORDER BY block_height DESC LIMIT 1"; + let args = rusqlite::params![&apparent_sender_str]; + query_row(conn, sql, args) + } } #[test] diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index ffb66d0db..5417a3a7c 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -39,7 +39,6 @@ use crate::chainstate::burn::{ConsensusHash, Opcodes}; use crate::chainstate::stacks::address::PoxAddress; use crate::util_lib::db::{DBConn, DBTx, Error as db_error}; -/// This module contains all burn-chain operations pub mod delegate_stx; pub mod leader_block_commit; pub mod leader_key_register; @@ -50,6 +49,8 @@ pub mod vote_for_aggregate_key; #[cfg(test)] mod test; +/// This module contains all burn-chain operations + #[derive(Debug)] pub enum Error { /// Failed to parse the operation from the burnchain transaction diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index fa6fda26b..2b4eea311 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -453,7 +453,7 @@ impl BlockSnapshot { // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction // of mining power the null miner has. - let null_prob_u256 = if null_prob.0 >= AtcRational::one().0 { + let null_prob_u256 = if null_prob.inner() >= AtcRational::one().inner() { // prevent left-shift overflow AtcRational::one_sup().into_inner() << 192 } else { @@ -1060,6 +1060,13 @@ mod test { ); } + /// This test runs 100 sortitions, and in each sortition, it verifies that the null miner will + /// win for the range of ATC-C values which put the sortition index into the null miner's + /// BurnSamplePoint range. The ATC-C values directly influence the null miner's + /// BurnSamplePoint range, so given a fixed sortition index, we can verify that the + /// `null_miner_wins()` function returns `true` exactly when the sortition index falls into the + /// null miner's range. The ATC-C values are sampled through linear interpolation between 0.0 + /// and 1.0 in steps of 0.01. #[test] fn test_null_miner_wins() { let first_burn_hash = BurnchainHeaderHash([0xfe; 32]); @@ -1139,7 +1146,7 @@ mod test { for j in 0..100 { let atc = AtcRational::from_f64_unit((j as f64) / 100.0); let null_prob = BlockSnapshot::null_miner_probability(atc); - let null_prob_u256 = if null_prob.0 >= AtcRational::one().0 { + let null_prob_u256 = if null_prob.inner() >= AtcRational::one().inner() { // prevent left-shift overflow AtcRational::one_sup().into_inner() << 192 } else { diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 1047ae04d..bd441cc02 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1022,221 +1022,9 @@ simulating a miner. } if argv[1] == "analyze-sortition-mev" { - if argv.len() < 7 || (argv.len() >= 7 && argv.len() % 2 != 1) { - eprintln!( - "Usage: {} /path/to/burnchain/db /path/to/sortition/db /path/to/chainstate/db start_height end_height [advantage_miner advantage_burn ..]", - &argv[0] - ); - process::exit(1); - } - - let burnchaindb_path = argv[2].clone(); - let sortdb_path = argv[3].clone(); - let chainstate_path = argv[4].clone(); - let start_height: u64 = argv[5].parse().unwrap(); - let end_height: u64 = argv[6].parse().unwrap(); - - let mut advantages = HashMap::new(); - if argv.len() >= 7 { - let mut i = 7; - while i + 2 < argv.len() { - let advantaged_miner = argv[i].clone(); - let advantage: u64 = argv[i + 1].parse().unwrap(); - advantages.insert(advantaged_miner, advantage); - i += 2; - } - } - - let mut sortdb = - SortitionDB::open(&sortdb_path, true, PoxConstants::mainnet_default()).unwrap(); - sortdb.dryrun = true; - let burnchain = Burnchain::new(&burnchaindb_path, "bitcoin", "mainnet").unwrap(); - let burnchaindb = BurnchainDB::connect(&burnchaindb_path, &burnchain, true).unwrap(); - let (mut chainstate, _) = - StacksChainState::open(true, 0x00000001, &chainstate_path, None).unwrap(); - - let mut wins_epoch2 = BTreeMap::new(); - let mut wins_epoch3 = BTreeMap::new(); - - for height in start_height..end_height { - debug!("Get ancestor snapshots for {}", height); - let (tip_sort_id, parent_ancestor_sn, ancestor_sn) = { - let mut sort_tx = sortdb.tx_begin_at_tip(); - let tip_sort_id = sort_tx.tip(); - let ancestor_sn = sort_tx - .get_block_snapshot_by_height(height) - .unwrap() - .unwrap(); - let parent_ancestor_sn = sort_tx - .get_block_snapshot_by_height(height - 1) - .unwrap() - .unwrap(); - (tip_sort_id, parent_ancestor_sn, ancestor_sn) - }; - - let mut burn_block = - BurnchainDB::get_burnchain_block(burnchaindb.conn(), &ancestor_sn.burn_header_hash) - .unwrap(); - - debug!( - "Get reward cycle info at {}", - burn_block.header.block_height - ); - let rc_info_opt = get_reward_cycle_info( - burn_block.header.block_height, - &burn_block.header.parent_block_hash, - &tip_sort_id, - &burnchain, - &burnchaindb, - &mut chainstate, - &mut sortdb, - &OnChainRewardSetProvider::new(), - false, - ) - .unwrap(); - - let mut ops = burn_block.ops.clone(); - for op in ops.iter_mut() { - if let BlockstackOperationType::LeaderBlockCommit(op) = op { - if let Some(extra_burn) = advantages.get(&op.apparent_sender.to_string()) { - debug!( - "Miner {} gets {} extra burn fee", - &op.apparent_sender.to_string(), - extra_burn - ); - op.burn_fee += *extra_burn; - } - } - } - burn_block.ops = ops; - - debug!("Re-evaluate sortition at height {}", height); - let (next_sn, state_transition) = sortdb - .evaluate_sortition( - &burn_block.header, - burn_block.ops.clone(), - &burnchain, - &tip_sort_id, - rc_info_opt, - |_| (), - ) - .unwrap(); - - assert_eq!(next_sn.block_height, ancestor_sn.block_height); - assert_eq!(next_sn.burn_header_hash, ancestor_sn.burn_header_hash); - - let mut sort_tx = sortdb.tx_begin_at_tip(); - let tip_pox_id = sort_tx.get_pox_id().unwrap(); - let next_sn_nakamoto = BlockSnapshot::make_snapshot_in_epoch( - &mut sort_tx, - &burnchain, - &ancestor_sn.sortition_id, - &tip_pox_id, - &parent_ancestor_sn, - &burn_block.header, - &state_transition, - 0, - StacksEpochId::Epoch30, - ) - .unwrap(); - - assert_eq!(next_sn.block_height, next_sn_nakamoto.block_height); - assert_eq!(next_sn.burn_header_hash, next_sn_nakamoto.burn_header_hash); - - let winner_epoch2 = get_block_commit_by_txid( - &sort_tx, - &ancestor_sn.sortition_id, - &next_sn.winning_block_txid, - ) - .unwrap() - .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) - .unwrap_or("(null)".to_string()); - - let winner_epoch3 = get_block_commit_by_txid( - &sort_tx, - &ancestor_sn.sortition_id, - &next_sn_nakamoto.winning_block_txid, - ) - .unwrap() - .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) - .unwrap_or("(null)".to_string()); - - wins_epoch2.insert( - (next_sn.block_height, next_sn.burn_header_hash), - winner_epoch2, - ); - wins_epoch3.insert( - ( - next_sn_nakamoto.block_height, - next_sn_nakamoto.burn_header_hash, - ), - winner_epoch3, - ); - } - - let mut all_wins_epoch2 = BTreeMap::new(); - let mut all_wins_epoch3 = BTreeMap::new(); - - println!("Wins epoch 2"); - println!("------------"); - println!("height,burn_header_hash,winner"); - for ((height, bhh), winner) in wins_epoch2.iter() { - println!("{},{},{}", height, bhh, winner); - if let Some(cnt) = all_wins_epoch2.get_mut(winner) { - *cnt += 1; - } else { - all_wins_epoch2.insert(winner, 1); - } - } - - println!("------------"); - println!("Wins epoch 3"); - println!("------------"); - println!("height,burn_header_hash,winner"); - for ((height, bhh), winner) in wins_epoch3.iter() { - println!("{},{},{}", height, bhh, winner); - if let Some(cnt) = all_wins_epoch3.get_mut(winner) { - *cnt += 1; - } else { - all_wins_epoch3.insert(winner, 1); - } - } - - println!("---------------"); - println!("Differences"); - println!("---------------"); - println!("height,burn_header_hash,winner_epoch2,winner_epoch3"); - for ((height, bhh), winner) in wins_epoch2.iter() { - let Some(epoch3_winner) = wins_epoch3.get(&(*height, *bhh)) else { - continue; - }; - if epoch3_winner != winner { - println!("{},{},{},{}", height, bhh, winner, epoch3_winner); - } - } - - println!("---------------"); - println!("All epoch2 wins"); - println!("---------------"); - println!("miner,count"); - for (winner, count) in all_wins_epoch2.iter() { - println!("{},{}", winner, count); - } - - println!("---------------"); - println!("All epoch3 wins"); - println!("---------------"); - println!("miner,count,degradation"); - for (winner, count) in all_wins_epoch3.into_iter() { - let degradation = (count as f64) - / (all_wins_epoch2 - .get(&winner) - .map(|cnt| *cnt as f64) - .unwrap_or(0.00000000000001f64)); - println!("{},{},{}", &winner, count, degradation); - } - - process::exit(0); + analyze_sortition_mev(argv); + // should be unreachable + process::exit(1); } if argv[1] == "replay-chainstate" { @@ -1953,3 +1741,224 @@ fn replay_block(stacks_path: &str, index_block_hash_hex: &str) { } }; } + +/// Perform an analysis of the anti-MEV algorithm in epoch 3.0, vis-a-vis the status quo. +/// Results are printed to stdout. +/// Exits with 0 on success, and 1 on failure. +fn analyze_sortition_mev(argv: Vec) { + if argv.len() < 7 || (argv.len() >= 7 && argv.len() % 2 != 1) { + eprintln!( + "Usage: {} /path/to/burnchain/db /path/to/sortition/db /path/to/chainstate/db start_height end_height [advantage_miner advantage_burn ..]", + &argv[0] + ); + process::exit(1); + } + + let burnchaindb_path = argv[2].clone(); + let sortdb_path = argv[3].clone(); + let chainstate_path = argv[4].clone(); + let start_height: u64 = argv[5].parse().unwrap(); + let end_height: u64 = argv[6].parse().unwrap(); + + let mut advantages = HashMap::new(); + if argv.len() >= 7 { + let mut i = 7; + while i + 2 < argv.len() { + let advantaged_miner = argv[i].clone(); + let advantage: u64 = argv[i + 1].parse().unwrap(); + advantages.insert(advantaged_miner, advantage); + i += 2; + } + } + + let mut sortdb = + SortitionDB::open(&sortdb_path, true, PoxConstants::mainnet_default()).unwrap(); + sortdb.dryrun = true; + let burnchain = Burnchain::new(&burnchaindb_path, "bitcoin", "mainnet").unwrap(); + let burnchaindb = BurnchainDB::connect(&burnchaindb_path, &burnchain, true).unwrap(); + let (mut chainstate, _) = + StacksChainState::open(true, 0x00000001, &chainstate_path, None).unwrap(); + + let mut wins_epoch2 = BTreeMap::new(); + let mut wins_epoch3 = BTreeMap::new(); + + for height in start_height..end_height { + debug!("Get ancestor snapshots for {}", height); + let (tip_sort_id, parent_ancestor_sn, ancestor_sn) = { + let mut sort_tx = sortdb.tx_begin_at_tip(); + let tip_sort_id = sort_tx.tip(); + let ancestor_sn = sort_tx + .get_block_snapshot_by_height(height) + .unwrap() + .unwrap(); + let parent_ancestor_sn = sort_tx + .get_block_snapshot_by_height(height - 1) + .unwrap() + .unwrap(); + (tip_sort_id, parent_ancestor_sn, ancestor_sn) + }; + + let mut burn_block = + BurnchainDB::get_burnchain_block(burnchaindb.conn(), &ancestor_sn.burn_header_hash) + .unwrap(); + + debug!( + "Get reward cycle info at {}", + burn_block.header.block_height + ); + let rc_info_opt = get_reward_cycle_info( + burn_block.header.block_height, + &burn_block.header.parent_block_hash, + &tip_sort_id, + &burnchain, + &burnchaindb, + &mut chainstate, + &mut sortdb, + &OnChainRewardSetProvider::new(), + false, + ) + .unwrap(); + + let mut ops = burn_block.ops.clone(); + for op in ops.iter_mut() { + if let BlockstackOperationType::LeaderBlockCommit(op) = op { + if let Some(extra_burn) = advantages.get(&op.apparent_sender.to_string()) { + debug!( + "Miner {} gets {} extra burn fee", + &op.apparent_sender.to_string(), + extra_burn + ); + op.burn_fee += *extra_burn; + } + } + } + burn_block.ops = ops; + + debug!("Re-evaluate sortition at height {}", height); + let (next_sn, state_transition) = sortdb + .evaluate_sortition( + &burn_block.header, + burn_block.ops.clone(), + &burnchain, + &tip_sort_id, + rc_info_opt, + |_| (), + ) + .unwrap(); + + assert_eq!(next_sn.block_height, ancestor_sn.block_height); + assert_eq!(next_sn.burn_header_hash, ancestor_sn.burn_header_hash); + + let mut sort_tx = sortdb.tx_begin_at_tip(); + let tip_pox_id = sort_tx.get_pox_id().unwrap(); + let next_sn_nakamoto = BlockSnapshot::make_snapshot_in_epoch( + &mut sort_tx, + &burnchain, + &ancestor_sn.sortition_id, + &tip_pox_id, + &parent_ancestor_sn, + &burn_block.header, + &state_transition, + 0, + StacksEpochId::Epoch30, + ) + .unwrap(); + + assert_eq!(next_sn.block_height, next_sn_nakamoto.block_height); + assert_eq!(next_sn.burn_header_hash, next_sn_nakamoto.burn_header_hash); + + let winner_epoch2 = get_block_commit_by_txid( + &sort_tx, + &ancestor_sn.sortition_id, + &next_sn.winning_block_txid, + ) + .unwrap() + .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) + .unwrap_or("(null)".to_string()); + + let winner_epoch3 = get_block_commit_by_txid( + &sort_tx, + &ancestor_sn.sortition_id, + &next_sn_nakamoto.winning_block_txid, + ) + .unwrap() + .map(|cmt| format!("{:?}", &cmt.apparent_sender.to_string())) + .unwrap_or("(null)".to_string()); + + wins_epoch2.insert( + (next_sn.block_height, next_sn.burn_header_hash), + winner_epoch2, + ); + wins_epoch3.insert( + ( + next_sn_nakamoto.block_height, + next_sn_nakamoto.burn_header_hash, + ), + winner_epoch3, + ); + } + + let mut all_wins_epoch2 = BTreeMap::new(); + let mut all_wins_epoch3 = BTreeMap::new(); + + println!("Wins epoch 2"); + println!("------------"); + println!("height,burn_header_hash,winner"); + for ((height, bhh), winner) in wins_epoch2.iter() { + println!("{},{},{}", height, bhh, winner); + if let Some(cnt) = all_wins_epoch2.get_mut(winner) { + *cnt += 1; + } else { + all_wins_epoch2.insert(winner, 1); + } + } + + println!("------------"); + println!("Wins epoch 3"); + println!("------------"); + println!("height,burn_header_hash,winner"); + for ((height, bhh), winner) in wins_epoch3.iter() { + println!("{},{},{}", height, bhh, winner); + if let Some(cnt) = all_wins_epoch3.get_mut(winner) { + *cnt += 1; + } else { + all_wins_epoch3.insert(winner, 1); + } + } + + println!("---------------"); + println!("Differences"); + println!("---------------"); + println!("height,burn_header_hash,winner_epoch2,winner_epoch3"); + for ((height, bhh), winner) in wins_epoch2.iter() { + let Some(epoch3_winner) = wins_epoch3.get(&(*height, *bhh)) else { + continue; + }; + if epoch3_winner != winner { + println!("{},{},{},{}", height, bhh, winner, epoch3_winner); + } + } + + println!("---------------"); + println!("All epoch2 wins"); + println!("---------------"); + println!("miner,count"); + for (winner, count) in all_wins_epoch2.iter() { + println!("{},{}", winner, count); + } + + println!("---------------"); + println!("All epoch3 wins"); + println!("---------------"); + println!("miner,count,degradation"); + for (winner, count) in all_wins_epoch3.into_iter() { + let degradation = (count as f64) + / (all_wins_epoch2 + .get(&winner) + .map(|cnt| *cnt as f64) + .unwrap_or(0.00000000000001f64)); + println!("{},{},{}", &winner, count, degradation); + } + + process::exit(0); +} From 95163e291ec0e8469f5053859e185f6efdbd65a1 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 6 May 2024 22:03:54 -0400 Subject: [PATCH 48/56] fix: more PR feedback -- better, more-complete AtcRational tests and better abstraction over the inner u256 --- stackslib/src/chainstate/burn/atc.rs | 58 ++++++++++++++++++++++ stackslib/src/chainstate/burn/sortition.rs | 13 ++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index dadb4b07a..098cdad53 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -119,6 +119,20 @@ impl AtcRational { pub fn into_inner(self) -> Uint256 { self.0 } + + /// Convert to a BurnSamplePoint probability for use in calculating a sortition + pub fn into_sortition_probability(self) -> Uint256 { + // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the + // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction + // of mining power the null miner has. + let prob_u256 = if self.inner() >= Self::one().inner() { + // prevent left-shift overflow + Self::one_sup().into_inner() << 192 + } else { + self.into_inner() << 192 + }; + prob_u256 + } } /// Pre-calculated 1024-member lookup table for the null miner advantage function, as AtcRational @@ -1193,6 +1207,30 @@ mod test { } } + fn check_add(num_1: u64, den_1: u64, num_2: u64, den_2: u64) { + assert!( + (AtcRational::frac(num_1, den_1) + .add(&AtcRational::frac(num_2, den_2)) + .unwrap()) + .to_f64() + .abs() + - (num_1 as f64 / den_1 as f64 + num_2 as f64 / den_2 as f64).abs() + < (1.0 / (1024.0 * 1024.0)) + ); + } + + fn check_mul(num_1: u64, den_1: u64, num_2: u64, den_2: u64) { + assert!( + (AtcRational::frac(num_1, den_1) + .mul(&AtcRational::frac(num_2, den_2)) + .unwrap()) + .to_f64() + .abs() + - ((num_1 as f64 / den_1 as f64) * (num_2 as f64 / den_2 as f64)).abs() + < (1.0 / (1024.0 * 1024.0)) + ); + } + #[test] fn test_atc_rational() { // zero @@ -1347,6 +1385,26 @@ mod test { AtcRational::frac(1, 2).min(&AtcRational::frac(15, 32)), AtcRational::frac(15, 32) ); + + // we only do stuff with an AtcRational in the range [0..1), since if the ATC-C is greater + // than 1.0, then the null miner never wins (and thus there's no need to compute the null + // miner probability). + // + // The only time an AtcRational is greater than 1.0 is when we scale it up to the lookup + // table index, which has 1024 items. We check that here as well. + for num_1 in 0..=1 { + for den_1 in 1..=1024 { + test_debug!("{}/{}", num_1, den_1); + for num_2 in 0..=1 { + for den_2 in 1..=1024 { + check_add(num_1, den_1, num_2, den_2); + check_mul(num_1, den_1, num_2, den_2); + check_mul(num_1, den_1, 1024, 1); + check_mul(num_2, den_2, 1024, 1); + } + } + } + } } #[test] diff --git a/stackslib/src/chainstate/burn/sortition.rs b/stackslib/src/chainstate/burn/sortition.rs index 2b4eea311..9f3bc5d5e 100644 --- a/stackslib/src/chainstate/burn/sortition.rs +++ b/stackslib/src/chainstate/burn/sortition.rs @@ -449,16 +449,7 @@ impl BlockSnapshot { let mut burn_sample_winner = BurnSamplePoint::zero(commit_winner.clone()); let null_prob = Self::null_miner_probability(atc); - - // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the - // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction - // of mining power the null miner has. - let null_prob_u256 = if null_prob.inner() >= AtcRational::one().inner() { - // prevent left-shift overflow - AtcRational::one_sup().into_inner() << 192 - } else { - null_prob.into_inner() << 192 - }; + let null_prob_u256 = null_prob.into_sortition_probability(); test_debug!( "atc = {}, null_prob = {}, null_prob_u256 = {}, sortition_hash: {}", @@ -1146,6 +1137,8 @@ mod test { for j in 0..100 { let atc = AtcRational::from_f64_unit((j as f64) / 100.0); let null_prob = BlockSnapshot::null_miner_probability(atc); + + // NOTE: this tests .into_sortition_probability() let null_prob_u256 = if null_prob.inner() >= AtcRational::one().inner() { // prevent left-shift overflow AtcRational::one_sup().into_inner() << 192 From a29b1628e0c3d55905ebcdb3573f08045949d8cb Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 7 May 2024 13:35:24 -0400 Subject: [PATCH 49/56] chore: fix comment typo --- stackslib/src/chainstate/burn/atc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/burn/atc.rs b/stackslib/src/chainstate/burn/atc.rs index 098cdad53..510c5d203 100644 --- a/stackslib/src/chainstate/burn/atc.rs +++ b/stackslib/src/chainstate/burn/atc.rs @@ -122,7 +122,7 @@ impl AtcRational { /// Convert to a BurnSamplePoint probability for use in calculating a sortition pub fn into_sortition_probability(self) -> Uint256 { - // AtcRational's integer part is only 64 bits, so we need to scale it up so that it occupes the + // AtcRational's fractional part is only 64 bits, so we need to scale it up so that it occupies the // upper 64 bits of the burn sample point ranges so as to accurately represent the fraction // of mining power the null miner has. let prob_u256 = if self.inner() >= Self::one().inner() { From d4f40d17b227df0502e7d00ee935eb64a96f8ef0 Mon Sep 17 00:00:00 2001 From: wileyj <2847772+wileyj@users.noreply.github.com> Date: Wed, 8 May 2024 09:16:37 -0700 Subject: [PATCH 50/56] 4746 - avoid extra ci runs --- .github/workflows/ci.yml | 78 ++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93201a153..4190311eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -## The main Github Actions workflow +i## The main Github Actions workflow name: CI on: @@ -31,9 +31,6 @@ on: # paths: # - "**.rs" # - "**.clar" - pull_request_review: - types: - - submitted defaults: run: @@ -55,15 +52,6 @@ jobs: ## - PR review comment ## - PR change is requested rustfmt: - if: | - !( - github.event_name == 'pull_request_review' && - github.event.action == 'submitted' && - ( - github.event.review.state == 'commented' || - github.event.review.state == 'changes_requested' - ) - ) name: Rust Format runs-on: ubuntu-latest steps: @@ -104,22 +92,9 @@ jobs: ## ## Runs when: ## - tag is not provided - ## and the following are not true: - ## - PR review submitted (not approved) - ## and any of: - ## - PR review comment - ## - PR change is requested docker-image: if: | - inputs.tag == '' && - !( - github.event_name == 'pull_request_review' && - github.event.action == 'submitted' && - ( - github.event.review.state == 'commented' || - github.event.review.state == 'changes_requested' - ) - ) + inputs.tag == '' name: Docker Image (Source) uses: ./.github/workflows/image-build-source.yml needs: @@ -133,7 +108,6 @@ jobs: ## or: ## - no tag provided ## and any of: - ## - PR is approved (any approval will trigger) ## - this workflow is called manually ## - PR is opened ## - commit to either (development, master) branch @@ -141,11 +115,6 @@ jobs: if: | inputs.tag != '' || ( inputs.tag == '' && ( - ( - github.event_name == 'pull_request_review' && - github.event.action == 'submitted' && - github.event.review.state == 'approved' - ) || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group' || @@ -168,16 +137,28 @@ jobs: ## ## Runs when: ## - tag is provided - ## either or of the following: - ## - tag is not provided - ## - PR is approved + ## or: + ## - no tag provided + ## and any of: + ## - this workflow is called manually + ## - PR is opened + ## - PR added to merge queue + ## - commit to either (development, next, master) branch stacks-core-tests: if: | inputs.tag != '' || ( - inputs.tag == '' || ( - github.event_name == 'pull_request_review' && - github.event.action == 'submitted' && - github.event.review.state == 'approved' + inputs.tag == '' && ( + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request' || + github.event_name == 'merge_group' || + ( + contains(' + refs/heads/master + refs/heads/develop + refs/heads/next + ', github.event.pull_request.head.ref) && + github.event_name == 'push' + ) ) ) name: Stacks Core Tests @@ -189,10 +170,18 @@ jobs: bitcoin-tests: if: | inputs.tag != '' || ( - inputs.tag == '' || ( - github.event_name == 'pull_request_review' && - github.event.action == 'submitted' && - github.event.review.state == 'approved' + inputs.tag == '' && ( + github.event_name == 'workflow_dispatch' || + github.event_name == 'pull_request' || + github.event_name == 'merge_group' || + ( + contains(' + refs/heads/master + refs/heads/develop + refs/heads/next + ', github.event.pull_request.head.ref) && + github.event_name == 'push' + ) ) ) name: Bitcoin Tests @@ -228,3 +217,4 @@ jobs: - rustfmt - create-cache uses: ./.github/workflows/slow-tests.yml + From 01bb95624af78b1dbbb2f54dd87294c890f81f5f Mon Sep 17 00:00:00 2001 From: wileyj <2847772+wileyj@users.noreply.github.com> Date: Wed, 8 May 2024 09:35:04 -0700 Subject: [PATCH 51/56] remove paths-ignore --- .github/workflows/ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4190311eb..77bce89cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,6 @@ on: - reopened - synchronize - ready_for_review - paths-ignore: - - "**.md" - - "**.yml" - ## might be better to use inclusive v exclusive paths here, ex: - # paths: - # - "**.rs" - # - "**.clar" defaults: run: From d0de1af451f941e2d49b18f19fc29d3cb6e90167 Mon Sep 17 00:00:00 2001 From: wileyj <2847772+wileyj@users.noreply.github.com> Date: Thu, 9 May 2024 07:04:19 -0700 Subject: [PATCH 52/56] remove extra character from vi typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77bce89cf..39048dc01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -i## The main Github Actions workflow +## The main Github Actions workflow name: CI on: From 8a2f4f1105b676fbc3c121cd84f36bfc75b9c348 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 9 May 2024 14:22:23 -0700 Subject: [PATCH 53/56] Remove dead CLI commands including Sign, DKG, and generate Files commands Signed-off-by: Jacinta Ferrant --- stacks-signer/src/cli.rs | 84 -------------- stacks-signer/src/main.rs | 226 +++----------------------------------- 2 files changed, 14 insertions(+), 296 deletions(-) diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs index 0d305382a..1cc51bfe6 100644 --- a/stacks-signer/src/cli.rs +++ b/stacks-signer/src/cli.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::io::{self, Read}; -use std::net::SocketAddr; use std::path::PathBuf; use blockstack_lib::chainstate::stacks::address::PoxAddress; @@ -28,8 +27,6 @@ use stacks_common::address::{ }; use stacks_common::types::chainstate::StacksPrivateKey; -use crate::config::Network; - extern crate alloc; #[derive(Parser, Debug)] @@ -52,16 +49,8 @@ pub enum Command { ListChunks(StackerDBArgs), /// Upload a chunk to the stacker-db instance PutChunk(PutChunkArgs), - /// Run DKG and sign the message through the stacker-db instance - DkgSign(SignArgs), - /// Sign the message through the stacker-db instance - Sign(SignArgs), - /// Run a DKG round through the stacker-db instance - Dkg(RunDkgArgs), /// Run the signer, waiting for events from the stacker-db instance Run(RunSignerArgs), - /// Generate necessary files for running a collection of signers - GenerateFiles(GenerateFilesArgs), /// Generate a signature for Stacking transactions GenerateStackingSignature(GenerateStackingSignatureArgs), /// Check a configuration file and output config information @@ -126,34 +115,6 @@ pub struct PutChunkArgs { pub data: alloc::vec::Vec, } -#[derive(Parser, Debug, Clone)] -/// Arguments for the dkg-sign and sign command -pub struct SignArgs { - /// Path to config file - #[arg(long, short, value_name = "FILE")] - pub config: PathBuf, - /// The reward cycle the signer is registered for and wants to sign for - /// Note: this must be the current reward cycle of the node - #[arg(long, short)] - pub reward_cycle: u64, - /// The data to sign - #[arg(required = false, value_parser = parse_data)] - // Note this weirdness is due to https://github.com/clap-rs/clap/discussions/4695 - // Need to specify the long name here due to invalid parsing in Clap which looks at the NAME rather than the TYPE which causes issues in how it handles Vec's. - pub data: alloc::vec::Vec, -} - -#[derive(Parser, Debug, Clone)] -/// Arguments for the Dkg command -pub struct RunDkgArgs { - /// Path to config file - #[arg(long, short, value_name = "FILE")] - pub config: PathBuf, - /// The reward cycle the signer is registered for and wants to peform DKG for - #[arg(long, short)] - pub reward_cycle: u64, -} - #[derive(Parser, Debug, Clone)] /// Arguments for the Run command pub struct RunSignerArgs { @@ -162,36 +123,6 @@ pub struct RunSignerArgs { pub config: PathBuf, } -#[derive(Parser, Debug, Clone)] -/// Arguments for the generate-files command -pub struct GenerateFilesArgs { - /// The Stacks node to connect to - #[arg(long)] - pub host: SocketAddr, - #[arg( - long, - required_unless_present = "private_keys", - conflicts_with = "private_keys" - )] - /// The number of signers to generate - pub num_signers: Option, - #[clap(long, value_name = "FILE")] - /// A path to a file containing a list of hexadecimal Stacks private keys of the signers - pub private_keys: Option, - #[arg(long, value_parser = parse_network)] - /// The network to use. One of "mainnet", "testnet", or "mocknet". - pub network: Network, - /// The directory to write the test data files to - #[arg(long, default_value = ".")] - pub dir: PathBuf, - /// The number of milliseconds to wait when polling for events from the stacker-db instance. - #[arg(long)] - pub timeout: Option, - #[arg(long)] - /// The authorization password to use to connect to the validate block proposal node endpoint - pub password: String, -} - #[derive(Clone, Debug)] /// Wrapper around `Pox4SignatureTopic` to implement `ValueEnum` pub struct StackingSignatureMethod(Pox4SignatureTopic); @@ -312,21 +243,6 @@ fn parse_data(data: &str) -> Result, String> { Ok(data) } -/// Parse the network. Must be one of "mainnet", "testnet", or "mocknet". -fn parse_network(network: &str) -> Result { - Ok(match network.to_lowercase().as_str() { - "mainnet" => Network::Mainnet, - "testnet" => Network::Testnet, - "mocknet" => Network::Mocknet, - _ => { - return Err(format!( - "Invalid network: {}. Must be one of \"mainnet\", \"testnet\", or \"mocknet\".", - network - )) - } - }) -} - #[cfg(test)] mod tests { use blockstack_lib::chainstate::stacks::address::{PoxAddressType20, PoxAddressType32}; diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 3573ac1d6..95bb3ce60 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -26,41 +26,33 @@ extern crate serde; extern crate serde_json; extern crate toml; -use std::fs::File; -use std::io::{self, BufRead, Write}; -use std::path::{Path, PathBuf}; +use std::io::{self, Write}; +use std::path::PathBuf; use std::sync::mpsc::{channel, Receiver, Sender}; -use std::time::Duration; use blockstack_lib::util_lib::signed_structured_data::pox4::make_pox_4_signer_key_signature; use clap::Parser; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{ - BlockProposalSigners, RunningSigner, Signer, SignerEventReceiver, SignerSession, - StackerDBSession, -}; +use libsigner::{RunningSigner, Signer, SignerEventReceiver, SignerSession, StackerDBSession}; use libstackerdb::StackerDBChunkData; -use slog::{slog_debug, slog_error, slog_info}; -use stacks_common::codec::read_next; -use stacks_common::types::chainstate::StacksPrivateKey; +use slog::{slog_debug, slog_info}; use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; -use stacks_common::{debug, error, info}; +use stacks_common::{debug, info}; use stacks_signer::cli::{ - Cli, Command, GenerateFilesArgs, GenerateStackingSignatureArgs, GetChunkArgs, - GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, RunSignerArgs, SignArgs, StackerDBArgs, + Cli, Command, GenerateStackingSignatureArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, + RunSignerArgs, StackerDBArgs, }; -use stacks_signer::config::{build_signer_config_tomls, GlobalConfig}; +use stacks_signer::config::GlobalConfig; use stacks_signer::runloop::{RunLoop, RunLoopCommand}; -use stacks_signer::signer::Command as SignerCommand; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; use wsts::state_machine::OperationResult; struct SpawnedSigner { running_signer: RunningSigner>, - cmd_send: Sender, - res_recv: Receiver>, + _cmd_send: Sender, + _res_recv: Receiver>, } /// Create a new stacker db session @@ -90,8 +82,8 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = GlobalConfig::try_from(path).unwrap(); let endpoint = config.endpoint; info!("Starting signer with config: {}", config); - let (cmd_send, cmd_recv) = channel(); - let (res_send, res_recv) = channel(); + let (_cmd_send, cmd_recv) = channel(); + let (res_send, _res_recv) = channel(); let ev = SignerEventReceiver::new(config.network.is_mainnet()); #[cfg(feature = "monitoring_prom")] { @@ -103,66 +95,8 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let running_signer = signer.spawn(endpoint).unwrap(); SpawnedSigner { running_signer, - cmd_send, - res_recv, - } -} - -// Process a DKG result -fn process_dkg_result(dkg_res: &[OperationResult]) { - assert!(dkg_res.len() == 1, "Received unexpected number of results"); - let dkg = dkg_res.first().unwrap(); - match dkg { - OperationResult::Dkg(aggregate_key) => { - println!("Received aggregate group key: {aggregate_key}"); - } - OperationResult::Sign(signature) => { - panic!( - "Received unexpected signature ({},{})", - &signature.R, &signature.z, - ); - } - OperationResult::SignTaproot(schnorr_proof) => { - panic!( - "Received unexpected schnorr proof ({},{})", - &schnorr_proof.r, &schnorr_proof.s, - ); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } - } -} - -// Process a Sign result -fn process_sign_result(sign_res: &[OperationResult]) { - assert!(sign_res.len() == 1, "Received unexpected number of results"); - let sign = sign_res.first().unwrap(); - match sign { - OperationResult::Dkg(aggregate_key) => { - panic!("Received unexpected aggregate group key: {aggregate_key}"); - } - OperationResult::Sign(signature) => { - panic!( - "Received bood signature ({},{})", - &signature.R, &signature.z, - ); - } - OperationResult::SignTaproot(schnorr_proof) => { - panic!( - "Received unexpected schnorr proof ({},{})", - &schnorr_proof.r, &schnorr_proof.s, - ); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } + _cmd_send, + _res_recv, } } @@ -198,73 +132,6 @@ fn handle_put_chunk(args: PutChunkArgs) { println!("{}", serde_json::to_string(&chunk_ack).unwrap()); } -fn handle_dkg(args: RunDkgArgs) { - debug!("Running DKG..."); - let spawned_signer = spawn_running_signer(&args.config); - let dkg_command = RunLoopCommand { - reward_cycle: args.reward_cycle, - command: SignerCommand::Dkg, - }; - spawned_signer.cmd_send.send(dkg_command).unwrap(); - let dkg_res = spawned_signer.res_recv.recv().unwrap(); - process_dkg_result(&dkg_res); - spawned_signer.running_signer.stop(); -} - -fn handle_sign(args: SignArgs) { - debug!("Signing message..."); - let spawned_signer = spawn_running_signer(&args.config); - let Some(block_proposal) = read_next::(&mut &args.data[..]).ok() - else { - error!("Unable to parse provided message as a BlockProposalSigners."); - spawned_signer.running_signer.stop(); - return; - }; - let sign_command = RunLoopCommand { - reward_cycle: args.reward_cycle, - command: SignerCommand::Sign { - block_proposal, - is_taproot: false, - merkle_root: None, - }, - }; - spawned_signer.cmd_send.send(sign_command).unwrap(); - let sign_res = spawned_signer.res_recv.recv().unwrap(); - process_sign_result(&sign_res); - spawned_signer.running_signer.stop(); -} - -fn handle_dkg_sign(args: SignArgs) { - debug!("Running DKG and signing message..."); - let spawned_signer = spawn_running_signer(&args.config); - let Some(block_proposal) = read_next::(&mut &args.data[..]).ok() - else { - error!("Unable to parse provided message as a BlockProposalSigners."); - spawned_signer.running_signer.stop(); - return; - }; - let dkg_command = RunLoopCommand { - reward_cycle: args.reward_cycle, - command: SignerCommand::Dkg, - }; - let sign_command = RunLoopCommand { - reward_cycle: args.reward_cycle, - command: SignerCommand::Sign { - block_proposal, - is_taproot: false, - merkle_root: None, - }, - }; - // First execute DKG, then sign - spawned_signer.cmd_send.send(dkg_command).unwrap(); - spawned_signer.cmd_send.send(sign_command).unwrap(); - let dkg_res = spawned_signer.res_recv.recv().unwrap(); - process_dkg_result(&dkg_res); - let sign_res = spawned_signer.res_recv.recv().unwrap(); - process_sign_result(&sign_res); - spawned_signer.running_signer.stop(); -} - fn handle_run(args: RunSignerArgs) { debug!("Running signer..."); let spawned_signer = spawn_running_signer(&args.config); @@ -273,50 +140,6 @@ fn handle_run(args: RunSignerArgs) { let _ = spawned_signer.running_signer.join(); } -fn handle_generate_files(args: GenerateFilesArgs) { - debug!("Generating files..."); - let signer_stacks_private_keys = if let Some(path) = args.private_keys { - let file = File::open(path).unwrap(); - let reader = io::BufReader::new(file); - - let private_keys: Vec = reader.lines().collect::>().unwrap(); - println!("{}", StacksPrivateKey::new().to_hex()); - let private_keys = private_keys - .iter() - .map(|key| StacksPrivateKey::from_hex(key).expect("Failed to parse private key.")) - .collect::>(); - if private_keys.is_empty() { - panic!("Private keys file is empty."); - } - private_keys - } else { - let num_signers = args.num_signers.unwrap(); - if num_signers == 0 { - panic!("--num-signers must be non-zero."); - } - (0..num_signers) - .map(|_| StacksPrivateKey::new()) - .collect::>() - }; - - let signer_config_tomls = build_signer_config_tomls( - &signer_stacks_private_keys, - &args.host.to_string(), - args.timeout.map(Duration::from_millis), - &args.network, - &args.password, - rand::random(), - 3000, - None, - None, - None, - ); - debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); - for (i, file_contents) in signer_config_tomls.iter().enumerate() { - write_file(&args.dir, &format!("signer-{}.toml", i), file_contents); - } -} - fn handle_generate_stacking_signature( args: GenerateStackingSignatureArgs, do_print: bool, @@ -370,15 +193,6 @@ fn handle_check_config(args: RunSignerArgs) { println!("Config: {}", config); } -/// Helper function for writing the given contents to filename in the given directory -fn write_file(dir: &Path, filename: &str, contents: &str) { - let file_path = dir.join(filename); - let filename = file_path.to_str().unwrap(); - let mut file = File::create(filename).unwrap(); - file.write_all(contents.as_bytes()).unwrap(); - println!("Created file: {}", filename); -} - fn main() { let cli = Cli::parse(); @@ -400,21 +214,9 @@ fn main() { Command::PutChunk(args) => { handle_put_chunk(args); } - Command::Dkg(args) => { - handle_dkg(args); - } - Command::DkgSign(args) => { - handle_dkg_sign(args); - } - Command::Sign(args) => { - handle_sign(args); - } Command::Run(args) => { handle_run(args); } - Command::GenerateFiles(args) => { - handle_generate_files(args); - } Command::GenerateStackingSignature(args) => { handle_generate_stacking_signature(args, true); } From 57b345807beca2b3cad51d89077751cf8c946c7c Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 9 May 2024 14:32:39 -0700 Subject: [PATCH 54/56] Implement this error for MonitoringError and fix clippy warnings Signed-off-by: Jacinta Ferrant --- stacks-signer/src/monitoring/mod.rs | 4 ++-- stacks-signer/src/monitoring/server.rs | 25 +++++++++++-------------- stacks-signer/src/runloop.rs | 2 +- stacks-signer/src/signer.rs | 16 ++++++++-------- stackslib/src/monitoring/mod.rs | 1 + 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs index 484a09d77..0ecc99b5f 100644 --- a/stacks-signer/src/monitoring/mod.rs +++ b/stacks-signer/src/monitoring/mod.rs @@ -142,7 +142,7 @@ pub fn update_signer_nonce(nonce: u64) { /// The `origin` parameter is the base path of the RPC call, e.g. `http://node.com`. /// The `origin` parameter is removed from `full_path` when storing in prometheus. #[cfg(feature = "monitoring_prom")] -pub fn new_rpc_call_timer(full_path: &str, origin: &String) -> HistogramTimer { +pub fn new_rpc_call_timer(full_path: &str, origin: &str) -> HistogramTimer { let path = &full_path[origin.len()..]; let histogram = prometheus::SIGNER_RPC_CALL_LATENCIES_HISTOGRAM.with_label_values(&[path]); histogram.start_timer() @@ -157,7 +157,7 @@ impl NoOpTimer { /// Stop and record the no-op timer. #[cfg(not(feature = "monitoring_prom"))] -pub fn new_rpc_call_timer(_full_path: &str, _origin: &String) -> NoOpTimer { +pub fn new_rpc_call_timer(_full_path: &str, _origin: &str) -> NoOpTimer { NoOpTimer } diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs index 4330f2f8d..e86d79c91 100644 --- a/stacks-signer/src/monitoring/server.rs +++ b/stacks-signer/src/monitoring/server.rs @@ -29,17 +29,21 @@ use crate::config::{GlobalConfig, Network}; use crate::monitoring::prometheus::gather_metrics_string; use crate::monitoring::{update_signer_nonce, update_stacks_tip_height}; -#[derive(Debug)] +#[derive(thiserror::Error, Debug)] /// Monitoring server errors pub enum MonitoringError { /// Already bound to an address + #[error("Already bound to an address")] AlreadyBound, /// Server terminated + #[error("Server terminated")] Terminated, /// No endpoint configured + #[error("Prometheus endpoint not configured.")] EndpointNotConfigured, /// Error fetching metrics from stacks node - FetchError(ClientError), + #[error("Error fetching data from stacks node: {0}")] + FetchError(#[from] ClientError), } /// Metrics and monitoring server @@ -175,8 +179,7 @@ impl MonitoringServer { debug!("{}: Updating metrics", self); let peer_info = self .stacks_client - .get_peer_info() - .map_err(|e| MonitoringError::FetchError(e))?; + .get_peer_info()?; if let Ok(height) = i64::try_from(peer_info.stacks_tip_height) { update_stacks_tip_height(height); } else { @@ -187,27 +190,21 @@ impl MonitoringServer { } let pox_info = self .stacks_client - .get_pox_data() - .map_err(|e| MonitoringError::FetchError(e))?; + .get_pox_data()?; if let Ok(reward_cycle) = i64::try_from(pox_info.reward_cycle_id) { update_reward_cycle(reward_cycle); } let signer_stx_addr = self.stacks_client.get_signer_address(); let account_entry = self .stacks_client - .get_account_entry(&signer_stx_addr) - .map_err(|e| MonitoringError::FetchError(e))?; + .get_account_entry(signer_stx_addr)?; let balance = i64::from_str_radix(&account_entry.balance[2..], 16).map_err(|e| { MonitoringError::FetchError(ClientError::MalformedClarityValue(format!( "Failed to parse balance: {} with err: {}", &account_entry.balance, e, ))) })?; - if let Ok(nonce) = u64::try_from(account_entry.nonce) { - update_signer_nonce(nonce); - } else { - warn!("Failed to parse nonce: {}", account_entry.nonce); - } + update_signer_nonce(account_entry.nonce); update_signer_stx_balance(balance); Ok(()) } @@ -226,7 +223,7 @@ impl MonitoringServer { /// Poll the Stacks node's `v2/info` endpoint to validate the connection fn heartbeat(&self) -> bool { let url = format!("{}/v2/info", self.stacks_node_origin); - let response = self.stacks_node_client.get(&url).send(); + let response = self.stacks_node_client.get(url).send(); match response { Ok(response) => { if response.status().is_success() { diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 3bb442679..905b4f307 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -95,7 +95,7 @@ impl RewardCycleInfo { let effective_height = burnchain_block_height - self.first_burnchain_block_height; let reward_index = effective_height % self.reward_cycle_length; - reward_index >= u64::from(self.reward_cycle_length - self.prepare_phase_block_length) + reward_index >= (self.reward_cycle_length - self.prepare_phase_block_length) && self.get_reward_cycle(burnchain_block_height) == self.reward_cycle } } diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 4f0b6d254..5261cc898 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -413,7 +413,7 @@ impl Signer { fn execute_command(&mut self, stacks_client: &StacksClient, command: &Command) { match command { Command::Dkg => { - crate::monitoring::increment_commands_processed(&"dkg"); + crate::monitoring::increment_commands_processed("dkg"); if self.approved_aggregate_public_key.is_some() { debug!("Reward cycle #{} Signer #{}: Already have an aggregate key. Ignoring DKG command.", self.reward_cycle, self.signer_id); return; @@ -450,7 +450,7 @@ impl Signer { is_taproot, merkle_root, } => { - crate::monitoring::increment_commands_processed(&"sign"); + crate::monitoring::increment_commands_processed("sign"); if self.approved_aggregate_public_key.is_none() { debug!("{self}: Cannot sign a block without an approved aggregate public key. Ignore it."); return; @@ -1043,25 +1043,25 @@ impl Signer { // Signers only every trigger non-taproot signing rounds over blocks. Ignore SignTaproot results match operation_result { OperationResult::Sign(signature) => { - crate::monitoring::increment_operation_results(&"sign"); + crate::monitoring::increment_operation_results("sign"); debug!("{self}: Received signature result"); self.process_signature(signature); } OperationResult::SignTaproot(_) => { - crate::monitoring::increment_operation_results(&"sign_taproot"); + crate::monitoring::increment_operation_results("sign_taproot"); debug!("{self}: Received a signature result for a taproot signature. Nothing to broadcast as we currently sign blocks with a FROST signature."); } OperationResult::Dkg(aggregate_key) => { - crate::monitoring::increment_operation_results(&"dkg"); + crate::monitoring::increment_operation_results("dkg"); self.process_dkg(stacks_client, aggregate_key); } OperationResult::SignError(e) => { - crate::monitoring::increment_operation_results(&"sign_error"); + crate::monitoring::increment_operation_results("sign_error"); warn!("{self}: Received a Sign error: {e:?}"); self.process_sign_error(e); } OperationResult::DkgError(e) => { - crate::monitoring::increment_operation_results(&"dkg_error"); + crate::monitoring::increment_operation_results("dkg_error"); warn!("{self}: Received a DKG error: {e:?}"); // TODO: process these errors and track malicious signers to report } @@ -1389,7 +1389,7 @@ impl Signer { return Ok(()); } // Check stackerdb for any missed DKG messages to catch up our state. - self.read_dkg_stackerdb_messages(&stacks_client, res, current_reward_cycle)?; + self.read_dkg_stackerdb_messages(stacks_client, res, current_reward_cycle)?; // Check if we should still queue DKG if !self.should_queue_dkg(stacks_client)? { return Ok(()); diff --git a/stackslib/src/monitoring/mod.rs b/stackslib/src/monitoring/mod.rs index fa83fe97a..60ac4bfe1 100644 --- a/stackslib/src/monitoring/mod.rs +++ b/stackslib/src/monitoring/mod.rs @@ -46,6 +46,7 @@ pub fn increment_rpc_calls_counter() { prometheus::RPC_CALL_COUNTER.inc(); } +#[allow(unused_mut)] pub fn instrument_http_request_handler( conv_http: &mut ConversationHttp, mut req: StacksHttpRequest, From a2e4a1d6ca40cc747ffb267765de8972d3b28b7a Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 9 May 2024 14:35:11 -0700 Subject: [PATCH 55/56] cargo fmt Signed-off-by: Jacinta Ferrant --- stacks-signer/src/monitoring/server.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs index e86d79c91..9cecd41ed 100644 --- a/stacks-signer/src/monitoring/server.rs +++ b/stacks-signer/src/monitoring/server.rs @@ -177,9 +177,7 @@ impl MonitoringServer { /// Update metrics by making RPC calls to the Stacks node fn update_metrics(&self) -> Result<(), MonitoringError> { debug!("{}: Updating metrics", self); - let peer_info = self - .stacks_client - .get_peer_info()?; + let peer_info = self.stacks_client.get_peer_info()?; if let Ok(height) = i64::try_from(peer_info.stacks_tip_height) { update_stacks_tip_height(height); } else { @@ -188,16 +186,12 @@ impl MonitoringServer { peer_info.stacks_tip_height ); } - let pox_info = self - .stacks_client - .get_pox_data()?; + let pox_info = self.stacks_client.get_pox_data()?; if let Ok(reward_cycle) = i64::try_from(pox_info.reward_cycle_id) { update_reward_cycle(reward_cycle); } let signer_stx_addr = self.stacks_client.get_signer_address(); - let account_entry = self - .stacks_client - .get_account_entry(signer_stx_addr)?; + let account_entry = self.stacks_client.get_account_entry(signer_stx_addr)?; let balance = i64::from_str_radix(&account_entry.balance[2..], 16).map_err(|e| { MonitoringError::FetchError(ClientError::MalformedClarityValue(format!( "Failed to parse balance: {} with err: {}", From 844d5b3aa74b030685a8292b9be210f2f276f908 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 9 May 2024 14:58:02 -0700 Subject: [PATCH 56/56] Undo var rename Signed-off-by: Jacinta Ferrant --- stacks-signer/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 95bb3ce60..9d6f9c293 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -51,8 +51,8 @@ use wsts::state_machine::OperationResult; struct SpawnedSigner { running_signer: RunningSigner>, - _cmd_send: Sender, - _res_recv: Receiver>, + cmd_send: Sender, + res_recv: Receiver>, } /// Create a new stacker db session @@ -82,8 +82,8 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = GlobalConfig::try_from(path).unwrap(); let endpoint = config.endpoint; info!("Starting signer with config: {}", config); - let (_cmd_send, cmd_recv) = channel(); - let (res_send, _res_recv) = channel(); + let (cmd_send, cmd_recv) = channel(); + let (res_send, res_recv) = channel(); let ev = SignerEventReceiver::new(config.network.is_mainnet()); #[cfg(feature = "monitoring_prom")] { @@ -95,8 +95,8 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let running_signer = signer.spawn(endpoint).unwrap(); SpawnedSigner { running_signer, - _cmd_send, - _res_recv, + cmd_send, + res_recv, } }