From 0d03b2a610dafcde79d50dc4ad94cf678169b81b Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 6 Dec 2021 21:09:49 +0000 Subject: [PATCH 001/103] separated out the update function --- src/cost_estimates/fee_scalar.rs | 64 ++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index e0414aff03..cd3f45a7b2 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -95,36 +95,42 @@ impl ScalarFeeRateEstimator { Ok(()) } + fn update_estimate_local( + &self, + new_measure: &FeeRateEstimate, + old_estimate: &FeeRateEstimate, + ) -> FeeRateEstimate { + // TODO: use a window (part 1) + // compute the exponential windowing: + // estimate = (a/b * old_estimate) + ((1 - a/b) * new_estimate) + let prior_component = old_estimate.clone() * self.decay_rate; + let next_component = new_measure.clone() * (1_f64 - self.decay_rate); + let mut next_computed = prior_component + next_component; + + // because of integer math, we can end up with some edge effects + // when the estimate is < decay_rate_fraction.1, so just saturate + // on the low end at a rate of "1" + next_computed.high = if next_computed.high >= 1f64 { + next_computed.high + } else { + 1f64 + }; + next_computed.middle = if next_computed.middle >= 1f64 { + next_computed.middle + } else { + 1f64 + }; + next_computed.low = if next_computed.low >= 1f64 { + next_computed.low + } else { + 1f64 + }; + + next_computed + } fn update_estimate(&mut self, new_measure: FeeRateEstimate) { let next_estimate = match self.get_rate_estimates() { - Ok(old_estimate) => { - // compute the exponential windowing: - // estimate = (a/b * old_estimate) + ((1 - a/b) * new_estimate) - let prior_component = old_estimate.clone() * self.decay_rate; - let next_component = new_measure.clone() * (1_f64 - self.decay_rate); - let mut next_computed = prior_component + next_component; - - // because of integer math, we can end up with some edge effects - // when the estimate is < decay_rate_fraction.1, so just saturate - // on the low end at a rate of "1" - next_computed.high = if next_computed.high >= 1f64 { - next_computed.high - } else { - 1f64 - }; - next_computed.middle = if next_computed.middle >= 1f64 { - next_computed.middle - } else { - 1f64 - }; - next_computed.low = if next_computed.low >= 1f64 { - next_computed.low - } else { - 1f64 - }; - - next_computed - } + Ok(old_estimate) => self.update_estimate_local(&new_measure, &old_estimate), Err(EstimatorError::NoEstimateAvailable) => new_measure.clone(), Err(e) => { warn!("Error in fee estimator fetching current estimates"; "err" => ?e); @@ -166,6 +172,7 @@ impl FeeEstimator for ScalarFeeRateEstimator { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { + // TODO: use the unused part of the block as being at fee rate minum (part 3) let mut all_fee_rates: Vec<_> = receipt .tx_receipts .iter() @@ -217,6 +224,7 @@ impl FeeEstimator for ScalarFeeRateEstimator { let measures_len = all_fee_rates.len(); if measures_len > 0 { + // TODO: add weights (part 2) // use 5th, 50th, and 95th percentiles from block let highest_index = measures_len - cmp::max(1, measures_len / 20); let median_index = measures_len / 2; From 9f49d2b21c6d3c2c6a4c197f9621260f867ac021 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 6 Dec 2021 21:42:34 +0000 Subject: [PATCH 002/103] factored out `fee_rate_from_receipt` --- src/cost_estimates/fee_scalar.rs | 83 +++++++++++++++++--------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index cd3f45a7b2..08e9bb4dfc 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -26,6 +26,8 @@ use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; +use cost_estimates::StacksTransactionReceipt; + const SINGLETON_ROW_ID: i64 = 1; const CREATE_TABLE: &'static str = " CREATE TABLE scalar_fee_estimator ( @@ -128,6 +130,7 @@ impl ScalarFeeRateEstimator { next_computed } + fn update_estimate(&mut self, new_measure: FeeRateEstimate) { let next_estimate = match self.get_rate_estimates() { Ok(old_estimate) => self.update_estimate_local(&new_measure, &old_estimate), @@ -164,6 +167,45 @@ impl ScalarFeeRateEstimator { tx.commit().expect("SQLite failure"); } + fn fee_rate_from_receipt( + &self, + tx_receipt: &StacksTransactionReceipt, + block_limit: &ExecutionCost, + ) -> Option { + let (payload, fee, tx_size) = match tx_receipt.transaction { + TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), + TransactionOrigin::Burn(_) => None, + }?; + let scalar_cost = match payload { + TransactionPayload::TokenTransfer(_, _, _) => { + // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. + self.metric.from_len(tx_size) + } + TransactionPayload::Coinbase(_) => { + // Coinbase txs are "free", so they don't factor into the fee market. + return None; + } + TransactionPayload::PoisonMicroblock(_, _) + | TransactionPayload::ContractCall(_) + | TransactionPayload::SmartContract(_) => { + // These transaction payload types all "work" the same: they have associated ExecutionCosts + // and contibute to the block length limit with their tx_len + self.metric + .from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) + } + }; + let fee_rate = fee as f64 + / if scalar_cost >= 1 { + scalar_cost as f64 + } else { + 1f64 + }; + if fee_rate >= 1f64 && fee_rate.is_finite() { + Some(fee_rate) + } else { + Some(1f64) + } + } } impl FeeEstimator for ScalarFeeRateEstimator { @@ -176,46 +218,7 @@ impl FeeEstimator for ScalarFeeRateEstimator { let mut all_fee_rates: Vec<_> = receipt .tx_receipts .iter() - .filter_map(|tx_receipt| { - let (payload, fee, tx_size) = match tx_receipt.transaction { - TransactionOrigin::Stacks(ref tx) => { - Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())) - } - TransactionOrigin::Burn(_) => None, - }?; - let scalar_cost = match payload { - TransactionPayload::TokenTransfer(_, _, _) => { - // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. - self.metric.from_len(tx_size) - } - TransactionPayload::Coinbase(_) => { - // Coinbase txs are "free", so they don't factor into the fee market. - return None; - } - TransactionPayload::PoisonMicroblock(_, _) - | TransactionPayload::ContractCall(_) - | TransactionPayload::SmartContract(_) => { - // These transaction payload types all "work" the same: they have associated ExecutionCosts - // and contibute to the block length limit with their tx_len - self.metric.from_cost_and_len( - &tx_receipt.execution_cost, - &block_limit, - tx_size, - ) - } - }; - let fee_rate = fee as f64 - / if scalar_cost >= 1 { - scalar_cost as f64 - } else { - 1f64 - }; - if fee_rate >= 1f64 && fee_rate.is_finite() { - Some(fee_rate) - } else { - Some(1f64) - } - }) + .filter_map(|tx_receipt| self.fee_rate_from_receipt(&tx_receipt, block_limit)) .collect(); all_fee_rates.sort_by(|a, b| { a.partial_cmp(b) From a828e1727212a9b09a5471d5db50e925efb39286 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 14:12:32 +0000 Subject: [PATCH 003/103] renamed variables --- src/cost_estimates/fee_scalar.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index 08e9bb4dfc..d87919557a 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -214,28 +214,30 @@ impl FeeEstimator for ScalarFeeRateEstimator { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { + // Step 1: Calculate a fee rate for each transaction in the block. // TODO: use the unused part of the block as being at fee rate minum (part 3) - let mut all_fee_rates: Vec<_> = receipt + let mut sorted_fee_rates: Vec<_> = receipt .tx_receipts .iter() .filter_map(|tx_receipt| self.fee_rate_from_receipt(&tx_receipt, block_limit)) .collect(); - all_fee_rates.sort_by(|a, b| { + sorted_fee_rates.sort_by(|a, b| { a.partial_cmp(b) .expect("BUG: Fee rates should be orderable: NaN and infinite values are filtered") }); - let measures_len = all_fee_rates.len(); - if measures_len > 0 { + // Step 2: If we have fee rates, update them. + let num_fee_rates = sorted_fee_rates.len(); + if num_fee_rates > 0 { // TODO: add weights (part 2) // use 5th, 50th, and 95th percentiles from block - let highest_index = measures_len - cmp::max(1, measures_len / 20); - let median_index = measures_len / 2; - let lowest_index = measures_len / 20; + let highest_index = num_fee_rates - cmp::max(1, num_fee_rates / 20); + let median_index = num_fee_rates / 2; + let lowest_index = num_fee_rates / 20; let block_estimate = FeeRateEstimate { - high: all_fee_rates[highest_index], - middle: all_fee_rates[median_index], - low: all_fee_rates[lowest_index], + high: sorted_fee_rates[highest_index], + middle: sorted_fee_rates[median_index], + low: sorted_fee_rates[lowest_index], }; self.update_estimate(block_estimate); From dca3254da2e9a87786174a01e8bbb84063c36498 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 14:15:01 +0000 Subject: [PATCH 004/103] reorganize fee rates --- src/cost_estimates/fee_scalar.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index d87919557a..670465930e 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -216,15 +216,19 @@ impl FeeEstimator for ScalarFeeRateEstimator { ) -> Result<(), EstimatorError> { // Step 1: Calculate a fee rate for each transaction in the block. // TODO: use the unused part of the block as being at fee rate minum (part 3) - let mut sorted_fee_rates: Vec<_> = receipt - .tx_receipts - .iter() - .filter_map(|tx_receipt| self.fee_rate_from_receipt(&tx_receipt, block_limit)) - .collect(); - sorted_fee_rates.sort_by(|a, b| { - a.partial_cmp(b) - .expect("BUG: Fee rates should be orderable: NaN and infinite values are filtered") - }); + let sorted_fee_rates: Vec<_> = { + let mut result = receipt + .tx_receipts + .iter() + .filter_map(|tx_receipt| self.fee_rate_from_receipt(&tx_receipt, block_limit)) + .collect(); + result.sort_by(|a, b| { + a.partial_cmp(b).expect( + "BUG: Fee rates should be orderable: NaN and infinite values are filtered", + ) + }); + result + }; // Step 2: If we have fee rates, update them. let num_fee_rates = sorted_fee_rates.len(); From 7faf443b9941efe95305a97570901a224cde8078 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 18:38:08 +0000 Subject: [PATCH 005/103] compiles --- src/cost_estimates/fee_scalar.rs | 83 ++++++++++++++++++-------------- src/cost_estimates/metrics.rs | 5 ++ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index 670465930e..bc32c6cb7a 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -44,13 +44,19 @@ CREATE TABLE scalar_fee_estimator ( /// estimates. Estimates are updated via exponential decay windowing. pub struct ScalarFeeRateEstimator { db: Connection, - /// how quickly does the current estimate decay - /// compared to the newly received block estimate - /// new_estimate := (decay_rate) * old_estimate + (1 - decay_rate) * new_measure - decay_rate: f64, + /// We only look back `window_size` fee rates when averaging past estimates. + window_size: u32, metric: M, } +/// Pair of "fee rate" and a "weight". The "weight" is a non-negative integer for a transaction +/// that gets its meaning relative to the other weights in the block. +struct FeeRateAndWeight { + pub fee_rate:f64 , + pub weight:u64, +} + + impl ScalarFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { @@ -79,7 +85,7 @@ impl ScalarFeeRateEstimator { Ok(Self { db, metric, - decay_rate: 0.5_f64, + window_size: 5, }) } @@ -105,8 +111,8 @@ impl ScalarFeeRateEstimator { // TODO: use a window (part 1) // compute the exponential windowing: // estimate = (a/b * old_estimate) + ((1 - a/b) * new_estimate) - let prior_component = old_estimate.clone() * self.decay_rate; - let next_component = new_measure.clone() * (1_f64 - self.decay_rate); + let prior_component = old_estimate.clone(); + let next_component = new_measure.clone(); let mut next_computed = prior_component + next_component; // because of integer math, we can end up with some edge effects @@ -167,11 +173,13 @@ impl ScalarFeeRateEstimator { tx.commit().expect("SQLite failure"); } - fn fee_rate_from_receipt( + + /// The fee rate is the `fee_paid/cost_metric_used` + fn fee_rate_and_weight_from_receipt( &self, tx_receipt: &StacksTransactionReceipt, block_limit: &ExecutionCost, - ) -> Option { + ) -> Option { let (payload, fee, tx_size) = match tx_receipt.transaction { TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), TransactionOrigin::Burn(_) => None, @@ -194,16 +202,33 @@ impl ScalarFeeRateEstimator { .from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) } }; - let fee_rate = fee as f64 - / if scalar_cost >= 1 { - scalar_cost as f64 - } else { - 1f64 - }; + let denominator = if scalar_cost >= 1 { + scalar_cost as f64 + } else { + 1f64 + }; + let fee_rate = fee as f64 / denominator; if fee_rate >= 1f64 && fee_rate.is_finite() { - Some(fee_rate) + Some(FeeRateAndWeight { fee_rate, weight: scalar_cost } ) } else { - Some(1f64) + Some(FeeRateAndWeight { fee_rate: 1f64, weight: scalar_cost } ) + } + } + fn compute_updates_from_fee_rates(&mut self, sorted_fee_rates: Vec) { + let num_fee_rates = sorted_fee_rates.len(); + if num_fee_rates > 0 { + // TODO: add weights (part 2) + // use 5th, 50th, and 95th percentiles from block + let highest_index = num_fee_rates - cmp::max(1, num_fee_rates / 20); + let median_index = num_fee_rates / 2; + let lowest_index = num_fee_rates / 20; + let block_estimate = FeeRateEstimate { + high: sorted_fee_rates[highest_index].fee_rate, + middle: sorted_fee_rates[median_index].fee_rate, + low: sorted_fee_rates[lowest_index].fee_rate, + }; + + self.update_estimate(block_estimate); } } } @@ -216,14 +241,14 @@ impl FeeEstimator for ScalarFeeRateEstimator { ) -> Result<(), EstimatorError> { // Step 1: Calculate a fee rate for each transaction in the block. // TODO: use the unused part of the block as being at fee rate minum (part 3) - let sorted_fee_rates: Vec<_> = { - let mut result = receipt + let sorted_fee_rates: Vec = { + let mut result: Vec = receipt .tx_receipts .iter() - .filter_map(|tx_receipt| self.fee_rate_from_receipt(&tx_receipt, block_limit)) + .filter_map(|tx_receipt| self.fee_rate_and_weight_from_receipt(&tx_receipt, block_limit)) .collect(); result.sort_by(|a, b| { - a.partial_cmp(b).expect( + a.fee_rate.partial_cmp(&b.fee_rate).expect( "BUG: Fee rates should be orderable: NaN and infinite values are filtered", ) }); @@ -231,21 +256,7 @@ impl FeeEstimator for ScalarFeeRateEstimator { }; // Step 2: If we have fee rates, update them. - let num_fee_rates = sorted_fee_rates.len(); - if num_fee_rates > 0 { - // TODO: add weights (part 2) - // use 5th, 50th, and 95th percentiles from block - let highest_index = num_fee_rates - cmp::max(1, num_fee_rates / 20); - let median_index = num_fee_rates / 2; - let lowest_index = num_fee_rates / 20; - let block_estimate = FeeRateEstimate { - high: sorted_fee_rates[highest_index], - middle: sorted_fee_rates[median_index], - low: sorted_fee_rates[lowest_index], - }; - - self.update_estimate(block_estimate); - } + self.compute_updates_from_fee_rates(sorted_fee_rates); Ok(()) } diff --git a/src/cost_estimates/metrics.rs b/src/cost_estimates/metrics.rs index 0cb0b4af16..1deb98f651 100644 --- a/src/cost_estimates/metrics.rs +++ b/src/cost_estimates/metrics.rs @@ -5,6 +5,11 @@ use crate::vm::costs::ExecutionCost; /// This trait defines metrics used to convert `ExecutionCost` and tx_len usage into single-dimensional /// metrics that can be used to compute a fee rate. pub trait CostMetric: Send { + /// Returns a single-dimensional integer representing the proportion of `block_limit` that + /// `cost` and `tx_len` up. + /// + /// TODO: Can we state more invariants about this? E.g., that the sum of all costs in a block + /// equals some constant value? fn from_cost_and_len( &self, cost: &ExecutionCost, From 7c43fe88c900c652dbe65d5e61d990092d9e02bf Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 20:34:10 +0000 Subject: [PATCH 006/103] weighte estimates in place but need to add the "minimum rate" --- src/cost_estimates/fee_scalar.rs | 73 +++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index bc32c6cb7a..9f60286efc 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -52,11 +52,10 @@ pub struct ScalarFeeRateEstimator { /// Pair of "fee rate" and a "weight". The "weight" is a non-negative integer for a transaction /// that gets its meaning relative to the other weights in the block. struct FeeRateAndWeight { - pub fee_rate:f64 , - pub weight:u64, + pub fee_rate: f64, + pub weight: u64, } - impl ScalarFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { @@ -209,26 +208,47 @@ impl ScalarFeeRateEstimator { }; let fee_rate = fee as f64 / denominator; if fee_rate >= 1f64 && fee_rate.is_finite() { - Some(FeeRateAndWeight { fee_rate, weight: scalar_cost } ) + Some(FeeRateAndWeight { + fee_rate, + weight: scalar_cost, + }) } else { - Some(FeeRateAndWeight { fee_rate: 1f64, weight: scalar_cost } ) + Some(FeeRateAndWeight { + fee_rate: 1f64, + weight: scalar_cost, + }) } } - fn compute_updates_from_fee_rates(&mut self, sorted_fee_rates: Vec) { - let num_fee_rates = sorted_fee_rates.len(); - if num_fee_rates > 0 { - // TODO: add weights (part 2) - // use 5th, 50th, and 95th percentiles from block - let highest_index = num_fee_rates - cmp::max(1, num_fee_rates / 20); - let median_index = num_fee_rates / 2; - let lowest_index = num_fee_rates / 20; - let block_estimate = FeeRateEstimate { - high: sorted_fee_rates[highest_index].fee_rate, - middle: sorted_fee_rates[median_index].fee_rate, - low: sorted_fee_rates[lowest_index].fee_rate, - }; - - self.update_estimate(block_estimate); + fn fee_rate_esimate_from_sorted_weights(&self, sorted_fee_rates: &Vec) -> FeeRateEstimate { + let mut total_weight = 0u64; + for rate_and_weight in sorted_fee_rates { + total_weight += rate_and_weight.weight; + } + let mut cumulative_weight = 0u64; + let mut percentiles = Vec::new(); + for rate_and_weight in sorted_fee_rates { + cumulative_weight += rate_and_weight.weight; + let percentile_n: f64 = (cumulative_weight as f64 + - rate_and_weight.weight as f64 / 2f64) + / total_weight as f64; + percentiles.push(percentile_n); + } + + let target_percentiles = vec![0.05, 0.5, 0.95]; + let mut fees_index = 1; // index into `sorted_fee_rates` + let mut values_at_target_percentiles = Vec::new(); + for target_percentile in target_percentiles { + while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { + fees_index += 1; + } + // TODO: use an interpolation + values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); + } + + FeeRateEstimate { + high: values_at_target_percentiles[2].fee_rate, + middle: values_at_target_percentiles[1].fee_rate, + low: values_at_target_percentiles[0].fee_rate, } } } @@ -239,13 +259,15 @@ impl FeeEstimator for ScalarFeeRateEstimator { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - // Step 1: Calculate a fee rate for each transaction in the block. + // Step 1: Calculate sorted fee rate for each transaction in the block. // TODO: use the unused part of the block as being at fee rate minum (part 3) let sorted_fee_rates: Vec = { let mut result: Vec = receipt .tx_receipts .iter() - .filter_map(|tx_receipt| self.fee_rate_and_weight_from_receipt(&tx_receipt, block_limit)) + .filter_map(|tx_receipt| { + self.fee_rate_and_weight_from_receipt(&tx_receipt, block_limit) + }) .collect(); result.sort_by(|a, b| { a.fee_rate.partial_cmp(&b.fee_rate).expect( @@ -255,8 +277,11 @@ impl FeeEstimator for ScalarFeeRateEstimator { result }; - // Step 2: If we have fee rates, update them. - self.compute_updates_from_fee_rates(sorted_fee_rates); + // Step2: Compute a FeeRateEstimate from the sorted fee rates. + let block_estimate = self.fee_rate_esimate_from_sorted_weights(&sorted_fee_rates); + + // Step 3: Update the estimate. + self.update_estimate(block_estimate); Ok(()) } From c86e7d7fcf168466d1a8624e75d77004f0f523e6 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:01:57 +0000 Subject: [PATCH 007/103] factored into methods and added "maybe minimum fee rate" --- src/cost_estimates/fee_scalar.rs | 209 ++++++++++++++++--------------- 1 file changed, 110 insertions(+), 99 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index 9f60286efc..b7d782d1f2 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -26,6 +26,7 @@ use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; +use super::metrics::PROPORTION_RESOLUTION; use cost_estimates::StacksTransactionReceipt; const SINGLETON_ROW_ID: i64 = 1; @@ -172,115 +173,32 @@ impl ScalarFeeRateEstimator { tx.commit().expect("SQLite failure"); } - - /// The fee rate is the `fee_paid/cost_metric_used` - fn fee_rate_and_weight_from_receipt( - &self, - tx_receipt: &StacksTransactionReceipt, - block_limit: &ExecutionCost, - ) -> Option { - let (payload, fee, tx_size) = match tx_receipt.transaction { - TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), - TransactionOrigin::Burn(_) => None, - }?; - let scalar_cost = match payload { - TransactionPayload::TokenTransfer(_, _, _) => { - // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. - self.metric.from_len(tx_size) - } - TransactionPayload::Coinbase(_) => { - // Coinbase txs are "free", so they don't factor into the fee market. - return None; - } - TransactionPayload::PoisonMicroblock(_, _) - | TransactionPayload::ContractCall(_) - | TransactionPayload::SmartContract(_) => { - // These transaction payload types all "work" the same: they have associated ExecutionCosts - // and contibute to the block length limit with their tx_len - self.metric - .from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) - } - }; - let denominator = if scalar_cost >= 1 { - scalar_cost as f64 - } else { - 1f64 - }; - let fee_rate = fee as f64 / denominator; - if fee_rate >= 1f64 && fee_rate.is_finite() { - Some(FeeRateAndWeight { - fee_rate, - weight: scalar_cost, - }) - } else { - Some(FeeRateAndWeight { - fee_rate: 1f64, - weight: scalar_cost, - }) - } - } - fn fee_rate_esimate_from_sorted_weights(&self, sorted_fee_rates: &Vec) -> FeeRateEstimate { - let mut total_weight = 0u64; - for rate_and_weight in sorted_fee_rates { - total_weight += rate_and_weight.weight; - } - let mut cumulative_weight = 0u64; - let mut percentiles = Vec::new(); - for rate_and_weight in sorted_fee_rates { - cumulative_weight += rate_and_weight.weight; - let percentile_n: f64 = (cumulative_weight as f64 - - rate_and_weight.weight as f64 / 2f64) - / total_weight as f64; - percentiles.push(percentile_n); - } - - let target_percentiles = vec![0.05, 0.5, 0.95]; - let mut fees_index = 1; // index into `sorted_fee_rates` - let mut values_at_target_percentiles = Vec::new(); - for target_percentile in target_percentiles { - while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { - fees_index += 1; - } - // TODO: use an interpolation - values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); - } - - FeeRateEstimate { - high: values_at_target_percentiles[2].fee_rate, - middle: values_at_target_percentiles[1].fee_rate, - low: values_at_target_percentiles[0].fee_rate, - } - } } impl FeeEstimator for ScalarFeeRateEstimator { + /// Compute a FeeRateEstimate for this block. Update the + /// running estimate using this rounds estimate. fn notify_block( &mut self, receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - // Step 1: Calculate sorted fee rate for each transaction in the block. - // TODO: use the unused part of the block as being at fee rate minum (part 3) - let sorted_fee_rates: Vec = { - let mut result: Vec = receipt - .tx_receipts - .iter() - .filter_map(|tx_receipt| { - self.fee_rate_and_weight_from_receipt(&tx_receipt, block_limit) - }) - .collect(); - result.sort_by(|a, b| { - a.fee_rate.partial_cmp(&b.fee_rate).expect( - "BUG: Fee rates should be orderable: NaN and infinite values are filtered", - ) - }); - result - }; + // Calculate sorted fee rate for each transaction in the block. + let mut working_fee_rates: Vec = receipt + .tx_receipts + .iter() + .filter_map(|tx_receipt| { + fee_rate_and_weight_from_receipt(&self.metric, &tx_receipt, block_limit) + }) + .collect(); + + // If necessary, add the "minimum" fee rate to fill the block. + maybe_add_minimum_fee_rate(&mut working_fee_rates, PROPORTION_RESOLUTION); - // Step2: Compute a FeeRateEstimate from the sorted fee rates. - let block_estimate = self.fee_rate_esimate_from_sorted_weights(&sorted_fee_rates); + // Compute a FeeRateEstimate from the sorted, adjusted fee rates. + let block_estimate = fee_rate_esimate_from_sorted_weights(&working_fee_rates); - // Step 3: Update the estimate. + // Update the running estimate using this rounds estimate. self.update_estimate(block_estimate); Ok(()) @@ -301,3 +219,96 @@ impl FeeEstimator for ScalarFeeRateEstimator { .ok_or_else(|| EstimatorError::NoEstimateAvailable) } } + +fn fee_rate_esimate_from_sorted_weights( + sorted_fee_rates: &Vec, +) -> FeeRateEstimate { + let mut total_weight = 0u64; + for rate_and_weight in sorted_fee_rates { + total_weight += rate_and_weight.weight; + } + let mut cumulative_weight = 0u64; + let mut percentiles = Vec::new(); + for rate_and_weight in sorted_fee_rates { + cumulative_weight += rate_and_weight.weight; + let percentile_n: f64 = + (cumulative_weight as f64 - rate_and_weight.weight as f64 / 2f64) / total_weight as f64; + percentiles.push(percentile_n); + } + + let target_percentiles = vec![0.05, 0.5, 0.95]; + let mut fees_index = 1; // index into `sorted_fee_rates` + let mut values_at_target_percentiles = Vec::new(); + for target_percentile in target_percentiles { + while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { + fees_index += 1; + } + // TODO: use an interpolation + values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); + } + + FeeRateEstimate { + high: values_at_target_percentiles[2].fee_rate, + middle: values_at_target_percentiles[1].fee_rate, + low: values_at_target_percentiles[0].fee_rate, + } +} +fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { + let mut total_weight = 0u64; + for rate_and_weight in working_rates.into_iter() { + total_weight += rate_and_weight.weight; + } + + if total_weight < full_block_weight { + working_rates.push(FeeRateAndWeight { + fee_rate: 1f64, + weight: total_weight, + }) + } +} + +/// The fee rate is the `fee_paid/cost_metric_used` +fn fee_rate_and_weight_from_receipt( + metric: &dyn CostMetric, + tx_receipt: &StacksTransactionReceipt, + block_limit: &ExecutionCost, +) -> Option { + let (payload, fee, tx_size) = match tx_receipt.transaction { + TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), + TransactionOrigin::Burn(_) => None, + }?; + let scalar_cost = match payload { + TransactionPayload::TokenTransfer(_, _, _) => { + // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. + metric.from_len(tx_size) + } + TransactionPayload::Coinbase(_) => { + // Coinbase txs are "free", so they don't factor into the fee market. + return None; + } + TransactionPayload::PoisonMicroblock(_, _) + | TransactionPayload::ContractCall(_) + | TransactionPayload::SmartContract(_) => { + // These transaction payload types all "work" the same: they have associated ExecutionCosts + // and contibute to the block length limit with their tx_len + metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) + } + }; + let denominator = if scalar_cost >= 1 { + scalar_cost as f64 + } else { + 1f64 + }; + let fee_rate = fee as f64 / denominator; + if fee_rate >= 1f64 && fee_rate.is_finite() { + Some(FeeRateAndWeight { + fee_rate, + weight: scalar_cost, + }) + } else { + Some(FeeRateAndWeight { + fee_rate: 1f64, + weight: scalar_cost, + }) + } +} From 8d7082fd4a79b55842819ec27f06130a0c1ea116 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:13:54 +0000 Subject: [PATCH 008/103] added rough code for the estimate updates --- src/cost_estimates/fee_scalar.rs | 140 ++++++++++++++++++------------- 1 file changed, 80 insertions(+), 60 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index b7d782d1f2..396aebd580 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -1,4 +1,5 @@ use std::cmp; +use std::cmp::Ordering; use std::convert::TryFrom; use std::{iter::FromIterator, path::Path}; @@ -31,8 +32,8 @@ use cost_estimates::StacksTransactionReceipt; const SINGLETON_ROW_ID: i64 = 1; const CREATE_TABLE: &'static str = " -CREATE TABLE scalar_fee_estimator ( - estimate_key NUMBER PRIMARY KEY, +CREATE TABLE median_fee_estimator ( + measure_key INTEGER PRIMARY KEY AUTOINCREMENT, high NUMBER NOT NULL, middle NUMBER NOT NULL, low NUMBER NOT NULL @@ -92,7 +93,7 @@ impl ScalarFeeRateEstimator { /// Check if the SQL database was already created. Necessary to avoid races if /// different threads open an estimator at the same time. fn db_already_instantiated(tx: &SqlTransaction) -> Result { - table_exists(tx, "scalar_fee_estimator") + table_exists(tx, "median_fee_estimator") } fn instantiate_db(tx: &SqlTransaction) -> Result<(), SqliteError> { @@ -103,75 +104,94 @@ impl ScalarFeeRateEstimator { Ok(()) } - fn update_estimate_local( - &self, - new_measure: &FeeRateEstimate, - old_estimate: &FeeRateEstimate, - ) -> FeeRateEstimate { - // TODO: use a window (part 1) - // compute the exponential windowing: - // estimate = (a/b * old_estimate) + ((1 - a/b) * new_estimate) - let prior_component = old_estimate.clone(); - let next_component = new_measure.clone(); - let mut next_computed = prior_component + next_component; - - // because of integer math, we can end up with some edge effects - // when the estimate is < decay_rate_fraction.1, so just saturate - // on the low end at a rate of "1" - next_computed.high = if next_computed.high >= 1f64 { - next_computed.high - } else { - 1f64 - }; - next_computed.middle = if next_computed.middle >= 1f64 { - next_computed.middle - } else { - 1f64 - }; - next_computed.low = if next_computed.low >= 1f64 { - next_computed.low - } else { - 1f64 - }; - - next_computed - } + fn get_rate_estimates_from_sql( + conn: &Connection, + window_size: u32, + ) -> Result { + let sql = + "SELECT high, middle, low FROM median_fee_estimator ORDER BY measure_key DESC LIMIT ?"; + let mut stmt = conn.prepare(sql).expect("SQLite failure"); + + // shuttle high, low, middle estimates into these lists, and then sort and find median. + let mut highs = Vec::with_capacity(window_size as usize); + let mut mids = Vec::with_capacity(window_size as usize); + let mut lows = Vec::with_capacity(window_size as usize); + let results = stmt + .query_and_then::<_, SqliteError, _, _>(&[window_size], |row| { + let high: f64 = row.get(0)?; + let middle: f64 = row.get(1)?; + let low: f64 = row.get(2)?; + Ok((low, middle, high)) + }) + .expect("SQLite failure"); - fn update_estimate(&mut self, new_measure: FeeRateEstimate) { - let next_estimate = match self.get_rate_estimates() { - Ok(old_estimate) => self.update_estimate_local(&new_measure, &old_estimate), - Err(EstimatorError::NoEstimateAvailable) => new_measure.clone(), - Err(e) => { - warn!("Error in fee estimator fetching current estimates"; "err" => ?e); - return; + for result in results { + let (low, middle, high) = result.expect("SQLite failure"); + highs.push(high); + mids.push(middle); + lows.push(low); + } + + if highs.is_empty() || mids.is_empty() || lows.is_empty() { + return Err(EstimatorError::NoEstimateAvailable); + } + + fn median(len: usize, l: Vec) -> f64 { + if len % 2 == 1 { + l[len / 2] + } else { + // note, measures_len / 2 - 1 >= 0, because + // len % 2 == 0 and emptiness is checked above + (l[len / 2] + l[len / 2 - 1]) / 2f64 } - }; + } - debug!("Updating fee rate estimate for new block"; - "new_measure_high" => new_measure.high, - "new_measure_middle" => new_measure.middle, - "new_measure_low" => new_measure.low, - "new_estimate_high" => next_estimate.high, - "new_estimate_middle" => next_estimate.middle, - "new_estimate_low" => next_estimate.low); + // sort our float arrays. for float values that do not compare easily, + // treat them as equals. + highs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + mids.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + lows.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - let sql = "INSERT OR REPLACE INTO scalar_fee_estimator - (estimate_key, high, middle, low) VALUES (?, ?, ?, ?)"; + Ok(FeeRateEstimate { + high: median(highs.len(), highs), + middle: median(mids.len(), mids), + low: median(lows.len(), lows), + }) + } + fn update_estimate(&mut self, new_measure: FeeRateEstimate) { let tx = tx_begin_immediate_sqlite(&mut self.db).expect("SQLite failure"); + let insert_sql = "INSERT INTO median_fee_estimator + (high, middle, low) VALUES (?, ?, ?)"; + + let deletion_sql = "DELETE FROM median_fee_estimator + WHERE measure_key <= ( + SELECT MAX(measure_key) - ? + FROM median_fee_estimator )"; + tx.execute( - sql, - rusqlite::params![ - SINGLETON_ROW_ID, - next_estimate.high, - next_estimate.middle, - next_estimate.low, - ], + insert_sql, + rusqlite::params![new_measure.high, new_measure.middle, new_measure.low,], ) .expect("SQLite failure"); + tx.execute(deletion_sql, rusqlite::params![self.window_size]) + .expect("SQLite failure"); + + let estimate = Self::get_rate_estimates_from_sql(&tx, self.window_size); + tx.commit().expect("SQLite failure"); + + if let Ok(next_estimate) = estimate { + debug!("Updating fee rate estimate for new block"; + "new_measure_high" => new_measure.high, + "new_measure_middle" => new_measure.middle, + "new_measure_low" => new_measure.low, + "new_estimate_high" => next_estimate.high, + "new_estimate_middle" => next_estimate.middle, + "new_estimate_low" => next_estimate.low); + } } } From 05992aa613a9aa97e0867df02265f0dcd60f2a0f Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:18:40 +0000 Subject: [PATCH 009/103] added in fee medians --- src/cost_estimates/fee_medians.rs | 334 ++++++++++++++++++++++++++++++ src/cost_estimates/mod.rs | 1 + 2 files changed, 335 insertions(+) create mode 100644 src/cost_estimates/fee_medians.rs diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs new file mode 100644 index 0000000000..396aebd580 --- /dev/null +++ b/src/cost_estimates/fee_medians.rs @@ -0,0 +1,334 @@ +use std::cmp; +use std::cmp::Ordering; +use std::convert::TryFrom; +use std::{iter::FromIterator, path::Path}; + +use rusqlite::Transaction as SqlTransaction; +use rusqlite::{ + types::{FromSql, FromSqlError}, + Connection, Error as SqliteError, OptionalExtension, ToSql, +}; +use serde_json::Value as JsonValue; + +use chainstate::stacks::TransactionPayload; +use util::db::sqlite_open; +use util::db::tx_begin_immediate_sqlite; +use util::db::u64_to_sql; + +use vm::costs::ExecutionCost; + +use chainstate::stacks::db::StacksEpochReceipt; +use chainstate::stacks::events::TransactionOrigin; + +use crate::util::db::sql_pragma; +use crate::util::db::table_exists; + +use super::metrics::CostMetric; +use super::FeeRateEstimate; +use super::{EstimatorError, FeeEstimator}; + +use super::metrics::PROPORTION_RESOLUTION; +use cost_estimates::StacksTransactionReceipt; + +const SINGLETON_ROW_ID: i64 = 1; +const CREATE_TABLE: &'static str = " +CREATE TABLE median_fee_estimator ( + measure_key INTEGER PRIMARY KEY AUTOINCREMENT, + high NUMBER NOT NULL, + middle NUMBER NOT NULL, + low NUMBER NOT NULL +)"; + +/// This struct estimates fee rates by translating a transaction's `ExecutionCost` +/// into a scalar using `ExecutionCost::proportion_dot_product` and computing +/// the subsequent fee rate using the actual paid fee. The 5th, 50th and 95th +/// percentile fee rates for each block are used as the low, middle, and high +/// estimates. Estimates are updated via exponential decay windowing. +pub struct ScalarFeeRateEstimator { + db: Connection, + /// We only look back `window_size` fee rates when averaging past estimates. + window_size: u32, + metric: M, +} + +/// Pair of "fee rate" and a "weight". The "weight" is a non-negative integer for a transaction +/// that gets its meaning relative to the other weights in the block. +struct FeeRateAndWeight { + pub fee_rate: f64, + pub weight: u64, +} + +impl ScalarFeeRateEstimator { + /// Open a fee rate estimator at the given db path. Creates if not existent. + pub fn open(p: &Path, metric: M) -> Result { + let db = + sqlite_open(p, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, false).or_else(|e| { + if let SqliteError::SqliteFailure(ref internal, _) = e { + if let rusqlite::ErrorCode::CannotOpen = internal.code { + let mut db = sqlite_open( + p, + rusqlite::OpenFlags::SQLITE_OPEN_CREATE + | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, + false, + )?; + let tx = tx_begin_immediate_sqlite(&mut db)?; + Self::instantiate_db(&tx)?; + tx.commit()?; + Ok(db) + } else { + Err(e) + } + } else { + Err(e) + } + })?; + + Ok(Self { + db, + metric, + window_size: 5, + }) + } + + /// Check if the SQL database was already created. Necessary to avoid races if + /// different threads open an estimator at the same time. + fn db_already_instantiated(tx: &SqlTransaction) -> Result { + table_exists(tx, "median_fee_estimator") + } + + fn instantiate_db(tx: &SqlTransaction) -> Result<(), SqliteError> { + if !Self::db_already_instantiated(tx)? { + tx.execute(CREATE_TABLE, rusqlite::NO_PARAMS)?; + } + + Ok(()) + } + + fn get_rate_estimates_from_sql( + conn: &Connection, + window_size: u32, + ) -> Result { + let sql = + "SELECT high, middle, low FROM median_fee_estimator ORDER BY measure_key DESC LIMIT ?"; + let mut stmt = conn.prepare(sql).expect("SQLite failure"); + + // shuttle high, low, middle estimates into these lists, and then sort and find median. + let mut highs = Vec::with_capacity(window_size as usize); + let mut mids = Vec::with_capacity(window_size as usize); + let mut lows = Vec::with_capacity(window_size as usize); + let results = stmt + .query_and_then::<_, SqliteError, _, _>(&[window_size], |row| { + let high: f64 = row.get(0)?; + let middle: f64 = row.get(1)?; + let low: f64 = row.get(2)?; + Ok((low, middle, high)) + }) + .expect("SQLite failure"); + + for result in results { + let (low, middle, high) = result.expect("SQLite failure"); + highs.push(high); + mids.push(middle); + lows.push(low); + } + + if highs.is_empty() || mids.is_empty() || lows.is_empty() { + return Err(EstimatorError::NoEstimateAvailable); + } + + fn median(len: usize, l: Vec) -> f64 { + if len % 2 == 1 { + l[len / 2] + } else { + // note, measures_len / 2 - 1 >= 0, because + // len % 2 == 0 and emptiness is checked above + (l[len / 2] + l[len / 2 - 1]) / 2f64 + } + } + + // sort our float arrays. for float values that do not compare easily, + // treat them as equals. + highs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + mids.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + lows.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + Ok(FeeRateEstimate { + high: median(highs.len(), highs), + middle: median(mids.len(), mids), + low: median(lows.len(), lows), + }) + } + + fn update_estimate(&mut self, new_measure: FeeRateEstimate) { + let tx = tx_begin_immediate_sqlite(&mut self.db).expect("SQLite failure"); + + let insert_sql = "INSERT INTO median_fee_estimator + (high, middle, low) VALUES (?, ?, ?)"; + + let deletion_sql = "DELETE FROM median_fee_estimator + WHERE measure_key <= ( + SELECT MAX(measure_key) - ? + FROM median_fee_estimator )"; + + tx.execute( + insert_sql, + rusqlite::params![new_measure.high, new_measure.middle, new_measure.low,], + ) + .expect("SQLite failure"); + + tx.execute(deletion_sql, rusqlite::params![self.window_size]) + .expect("SQLite failure"); + + let estimate = Self::get_rate_estimates_from_sql(&tx, self.window_size); + + tx.commit().expect("SQLite failure"); + + if let Ok(next_estimate) = estimate { + debug!("Updating fee rate estimate for new block"; + "new_measure_high" => new_measure.high, + "new_measure_middle" => new_measure.middle, + "new_measure_low" => new_measure.low, + "new_estimate_high" => next_estimate.high, + "new_estimate_middle" => next_estimate.middle, + "new_estimate_low" => next_estimate.low); + } + } +} + +impl FeeEstimator for ScalarFeeRateEstimator { + /// Compute a FeeRateEstimate for this block. Update the + /// running estimate using this rounds estimate. + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + // Calculate sorted fee rate for each transaction in the block. + let mut working_fee_rates: Vec = receipt + .tx_receipts + .iter() + .filter_map(|tx_receipt| { + fee_rate_and_weight_from_receipt(&self.metric, &tx_receipt, block_limit) + }) + .collect(); + + // If necessary, add the "minimum" fee rate to fill the block. + maybe_add_minimum_fee_rate(&mut working_fee_rates, PROPORTION_RESOLUTION); + + // Compute a FeeRateEstimate from the sorted, adjusted fee rates. + let block_estimate = fee_rate_esimate_from_sorted_weights(&working_fee_rates); + + // Update the running estimate using this rounds estimate. + self.update_estimate(block_estimate); + + Ok(()) + } + + fn get_rate_estimates(&self) -> Result { + let sql = "SELECT high, middle, low FROM scalar_fee_estimator WHERE estimate_key = ?"; + self.db + .query_row(sql, &[SINGLETON_ROW_ID], |row| { + let high: f64 = row.get(0)?; + let middle: f64 = row.get(1)?; + let low: f64 = row.get(2)?; + Ok((high, middle, low)) + }) + .optional() + .expect("SQLite failure") + .map(|(high, middle, low)| FeeRateEstimate { high, middle, low }) + .ok_or_else(|| EstimatorError::NoEstimateAvailable) + } +} + +fn fee_rate_esimate_from_sorted_weights( + sorted_fee_rates: &Vec, +) -> FeeRateEstimate { + let mut total_weight = 0u64; + for rate_and_weight in sorted_fee_rates { + total_weight += rate_and_weight.weight; + } + let mut cumulative_weight = 0u64; + let mut percentiles = Vec::new(); + for rate_and_weight in sorted_fee_rates { + cumulative_weight += rate_and_weight.weight; + let percentile_n: f64 = + (cumulative_weight as f64 - rate_and_weight.weight as f64 / 2f64) / total_weight as f64; + percentiles.push(percentile_n); + } + + let target_percentiles = vec![0.05, 0.5, 0.95]; + let mut fees_index = 1; // index into `sorted_fee_rates` + let mut values_at_target_percentiles = Vec::new(); + for target_percentile in target_percentiles { + while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { + fees_index += 1; + } + // TODO: use an interpolation + values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); + } + + FeeRateEstimate { + high: values_at_target_percentiles[2].fee_rate, + middle: values_at_target_percentiles[1].fee_rate, + low: values_at_target_percentiles[0].fee_rate, + } +} +fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { + let mut total_weight = 0u64; + for rate_and_weight in working_rates.into_iter() { + total_weight += rate_and_weight.weight; + } + + if total_weight < full_block_weight { + working_rates.push(FeeRateAndWeight { + fee_rate: 1f64, + weight: total_weight, + }) + } +} + +/// The fee rate is the `fee_paid/cost_metric_used` +fn fee_rate_and_weight_from_receipt( + metric: &dyn CostMetric, + tx_receipt: &StacksTransactionReceipt, + block_limit: &ExecutionCost, +) -> Option { + let (payload, fee, tx_size) = match tx_receipt.transaction { + TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), + TransactionOrigin::Burn(_) => None, + }?; + let scalar_cost = match payload { + TransactionPayload::TokenTransfer(_, _, _) => { + // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. + metric.from_len(tx_size) + } + TransactionPayload::Coinbase(_) => { + // Coinbase txs are "free", so they don't factor into the fee market. + return None; + } + TransactionPayload::PoisonMicroblock(_, _) + | TransactionPayload::ContractCall(_) + | TransactionPayload::SmartContract(_) => { + // These transaction payload types all "work" the same: they have associated ExecutionCosts + // and contibute to the block length limit with their tx_len + metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) + } + }; + let denominator = if scalar_cost >= 1 { + scalar_cost as f64 + } else { + 1f64 + }; + let fee_rate = fee as f64 / denominator; + if fee_rate >= 1f64 && fee_rate.is_finite() { + Some(FeeRateAndWeight { + fee_rate, + weight: scalar_cost, + }) + } else { + Some(FeeRateAndWeight { + fee_rate: 1f64, + weight: scalar_cost, + }) + } +} diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index fd481de13c..f298d02339 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -14,6 +14,7 @@ use burnchains::Txid; use chainstate::stacks::db::StacksEpochReceipt; pub mod fee_scalar; +pub mod fee_medians; pub mod metrics; pub mod pessimistic; From d45f44106f2314aa9ee5e645bef5a247b9cd5503 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:19:23 +0000 Subject: [PATCH 010/103] reinstate old fee_scalar --- src/cost_estimates/fee_scalar.rs | 316 ++++++++++++------------------- 1 file changed, 116 insertions(+), 200 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index 396aebd580..e0414aff03 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -1,5 +1,4 @@ use std::cmp; -use std::cmp::Ordering; use std::convert::TryFrom; use std::{iter::FromIterator, path::Path}; @@ -27,13 +26,10 @@ use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; -use super::metrics::PROPORTION_RESOLUTION; -use cost_estimates::StacksTransactionReceipt; - const SINGLETON_ROW_ID: i64 = 1; const CREATE_TABLE: &'static str = " -CREATE TABLE median_fee_estimator ( - measure_key INTEGER PRIMARY KEY AUTOINCREMENT, +CREATE TABLE scalar_fee_estimator ( + estimate_key NUMBER PRIMARY KEY, high NUMBER NOT NULL, middle NUMBER NOT NULL, low NUMBER NOT NULL @@ -46,18 +42,13 @@ CREATE TABLE median_fee_estimator ( /// estimates. Estimates are updated via exponential decay windowing. pub struct ScalarFeeRateEstimator { db: Connection, - /// We only look back `window_size` fee rates when averaging past estimates. - window_size: u32, + /// how quickly does the current estimate decay + /// compared to the newly received block estimate + /// new_estimate := (decay_rate) * old_estimate + (1 - decay_rate) * new_measure + decay_rate: f64, metric: M, } -/// Pair of "fee rate" and a "weight". The "weight" is a non-negative integer for a transaction -/// that gets its meaning relative to the other weights in the block. -struct FeeRateAndWeight { - pub fee_rate: f64, - pub weight: u64, -} - impl ScalarFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { @@ -86,14 +77,14 @@ impl ScalarFeeRateEstimator { Ok(Self { db, metric, - window_size: 5, + decay_rate: 0.5_f64, }) } /// Check if the SQL database was already created. Necessary to avoid races if /// different threads open an estimator at the same time. fn db_already_instantiated(tx: &SqlTransaction) -> Result { - table_exists(tx, "median_fee_estimator") + table_exists(tx, "scalar_fee_estimator") } fn instantiate_db(tx: &SqlTransaction) -> Result<(), SqliteError> { @@ -104,122 +95,140 @@ impl ScalarFeeRateEstimator { Ok(()) } - fn get_rate_estimates_from_sql( - conn: &Connection, - window_size: u32, - ) -> Result { - let sql = - "SELECT high, middle, low FROM median_fee_estimator ORDER BY measure_key DESC LIMIT ?"; - let mut stmt = conn.prepare(sql).expect("SQLite failure"); - - // shuttle high, low, middle estimates into these lists, and then sort and find median. - let mut highs = Vec::with_capacity(window_size as usize); - let mut mids = Vec::with_capacity(window_size as usize); - let mut lows = Vec::with_capacity(window_size as usize); - let results = stmt - .query_and_then::<_, SqliteError, _, _>(&[window_size], |row| { - let high: f64 = row.get(0)?; - let middle: f64 = row.get(1)?; - let low: f64 = row.get(2)?; - Ok((low, middle, high)) - }) - .expect("SQLite failure"); - - for result in results { - let (low, middle, high) = result.expect("SQLite failure"); - highs.push(high); - mids.push(middle); - lows.push(low); - } - - if highs.is_empty() || mids.is_empty() || lows.is_empty() { - return Err(EstimatorError::NoEstimateAvailable); - } + fn update_estimate(&mut self, new_measure: FeeRateEstimate) { + let next_estimate = match self.get_rate_estimates() { + Ok(old_estimate) => { + // compute the exponential windowing: + // estimate = (a/b * old_estimate) + ((1 - a/b) * new_estimate) + let prior_component = old_estimate.clone() * self.decay_rate; + let next_component = new_measure.clone() * (1_f64 - self.decay_rate); + let mut next_computed = prior_component + next_component; + + // because of integer math, we can end up with some edge effects + // when the estimate is < decay_rate_fraction.1, so just saturate + // on the low end at a rate of "1" + next_computed.high = if next_computed.high >= 1f64 { + next_computed.high + } else { + 1f64 + }; + next_computed.middle = if next_computed.middle >= 1f64 { + next_computed.middle + } else { + 1f64 + }; + next_computed.low = if next_computed.low >= 1f64 { + next_computed.low + } else { + 1f64 + }; - fn median(len: usize, l: Vec) -> f64 { - if len % 2 == 1 { - l[len / 2] - } else { - // note, measures_len / 2 - 1 >= 0, because - // len % 2 == 0 and emptiness is checked above - (l[len / 2] + l[len / 2 - 1]) / 2f64 + next_computed } - } + Err(EstimatorError::NoEstimateAvailable) => new_measure.clone(), + Err(e) => { + warn!("Error in fee estimator fetching current estimates"; "err" => ?e); + return; + } + }; - // sort our float arrays. for float values that do not compare easily, - // treat them as equals. - highs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - mids.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - lows.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + debug!("Updating fee rate estimate for new block"; + "new_measure_high" => new_measure.high, + "new_measure_middle" => new_measure.middle, + "new_measure_low" => new_measure.low, + "new_estimate_high" => next_estimate.high, + "new_estimate_middle" => next_estimate.middle, + "new_estimate_low" => next_estimate.low); - Ok(FeeRateEstimate { - high: median(highs.len(), highs), - middle: median(mids.len(), mids), - low: median(lows.len(), lows), - }) - } + let sql = "INSERT OR REPLACE INTO scalar_fee_estimator + (estimate_key, high, middle, low) VALUES (?, ?, ?, ?)"; - fn update_estimate(&mut self, new_measure: FeeRateEstimate) { let tx = tx_begin_immediate_sqlite(&mut self.db).expect("SQLite failure"); - let insert_sql = "INSERT INTO median_fee_estimator - (high, middle, low) VALUES (?, ?, ?)"; - - let deletion_sql = "DELETE FROM median_fee_estimator - WHERE measure_key <= ( - SELECT MAX(measure_key) - ? - FROM median_fee_estimator )"; - tx.execute( - insert_sql, - rusqlite::params![new_measure.high, new_measure.middle, new_measure.low,], + sql, + rusqlite::params![ + SINGLETON_ROW_ID, + next_estimate.high, + next_estimate.middle, + next_estimate.low, + ], ) .expect("SQLite failure"); - tx.execute(deletion_sql, rusqlite::params![self.window_size]) - .expect("SQLite failure"); - - let estimate = Self::get_rate_estimates_from_sql(&tx, self.window_size); - tx.commit().expect("SQLite failure"); - - if let Ok(next_estimate) = estimate { - debug!("Updating fee rate estimate for new block"; - "new_measure_high" => new_measure.high, - "new_measure_middle" => new_measure.middle, - "new_measure_low" => new_measure.low, - "new_estimate_high" => next_estimate.high, - "new_estimate_middle" => next_estimate.middle, - "new_estimate_low" => next_estimate.low); - } } } impl FeeEstimator for ScalarFeeRateEstimator { - /// Compute a FeeRateEstimate for this block. Update the - /// running estimate using this rounds estimate. fn notify_block( &mut self, receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - // Calculate sorted fee rate for each transaction in the block. - let mut working_fee_rates: Vec = receipt + let mut all_fee_rates: Vec<_> = receipt .tx_receipts .iter() .filter_map(|tx_receipt| { - fee_rate_and_weight_from_receipt(&self.metric, &tx_receipt, block_limit) + let (payload, fee, tx_size) = match tx_receipt.transaction { + TransactionOrigin::Stacks(ref tx) => { + Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())) + } + TransactionOrigin::Burn(_) => None, + }?; + let scalar_cost = match payload { + TransactionPayload::TokenTransfer(_, _, _) => { + // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. + self.metric.from_len(tx_size) + } + TransactionPayload::Coinbase(_) => { + // Coinbase txs are "free", so they don't factor into the fee market. + return None; + } + TransactionPayload::PoisonMicroblock(_, _) + | TransactionPayload::ContractCall(_) + | TransactionPayload::SmartContract(_) => { + // These transaction payload types all "work" the same: they have associated ExecutionCosts + // and contibute to the block length limit with their tx_len + self.metric.from_cost_and_len( + &tx_receipt.execution_cost, + &block_limit, + tx_size, + ) + } + }; + let fee_rate = fee as f64 + / if scalar_cost >= 1 { + scalar_cost as f64 + } else { + 1f64 + }; + if fee_rate >= 1f64 && fee_rate.is_finite() { + Some(fee_rate) + } else { + Some(1f64) + } }) .collect(); - - // If necessary, add the "minimum" fee rate to fill the block. - maybe_add_minimum_fee_rate(&mut working_fee_rates, PROPORTION_RESOLUTION); - - // Compute a FeeRateEstimate from the sorted, adjusted fee rates. - let block_estimate = fee_rate_esimate_from_sorted_weights(&working_fee_rates); - - // Update the running estimate using this rounds estimate. - self.update_estimate(block_estimate); + all_fee_rates.sort_by(|a, b| { + a.partial_cmp(b) + .expect("BUG: Fee rates should be orderable: NaN and infinite values are filtered") + }); + + let measures_len = all_fee_rates.len(); + if measures_len > 0 { + // use 5th, 50th, and 95th percentiles from block + let highest_index = measures_len - cmp::max(1, measures_len / 20); + let median_index = measures_len / 2; + let lowest_index = measures_len / 20; + let block_estimate = FeeRateEstimate { + high: all_fee_rates[highest_index], + middle: all_fee_rates[median_index], + low: all_fee_rates[lowest_index], + }; + + self.update_estimate(block_estimate); + } Ok(()) } @@ -239,96 +248,3 @@ impl FeeEstimator for ScalarFeeRateEstimator { .ok_or_else(|| EstimatorError::NoEstimateAvailable) } } - -fn fee_rate_esimate_from_sorted_weights( - sorted_fee_rates: &Vec, -) -> FeeRateEstimate { - let mut total_weight = 0u64; - for rate_and_weight in sorted_fee_rates { - total_weight += rate_and_weight.weight; - } - let mut cumulative_weight = 0u64; - let mut percentiles = Vec::new(); - for rate_and_weight in sorted_fee_rates { - cumulative_weight += rate_and_weight.weight; - let percentile_n: f64 = - (cumulative_weight as f64 - rate_and_weight.weight as f64 / 2f64) / total_weight as f64; - percentiles.push(percentile_n); - } - - let target_percentiles = vec![0.05, 0.5, 0.95]; - let mut fees_index = 1; // index into `sorted_fee_rates` - let mut values_at_target_percentiles = Vec::new(); - for target_percentile in target_percentiles { - while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { - fees_index += 1; - } - // TODO: use an interpolation - values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); - } - - FeeRateEstimate { - high: values_at_target_percentiles[2].fee_rate, - middle: values_at_target_percentiles[1].fee_rate, - low: values_at_target_percentiles[0].fee_rate, - } -} -fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { - let mut total_weight = 0u64; - for rate_and_weight in working_rates.into_iter() { - total_weight += rate_and_weight.weight; - } - - if total_weight < full_block_weight { - working_rates.push(FeeRateAndWeight { - fee_rate: 1f64, - weight: total_weight, - }) - } -} - -/// The fee rate is the `fee_paid/cost_metric_used` -fn fee_rate_and_weight_from_receipt( - metric: &dyn CostMetric, - tx_receipt: &StacksTransactionReceipt, - block_limit: &ExecutionCost, -) -> Option { - let (payload, fee, tx_size) = match tx_receipt.transaction { - TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), - TransactionOrigin::Burn(_) => None, - }?; - let scalar_cost = match payload { - TransactionPayload::TokenTransfer(_, _, _) => { - // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. - metric.from_len(tx_size) - } - TransactionPayload::Coinbase(_) => { - // Coinbase txs are "free", so they don't factor into the fee market. - return None; - } - TransactionPayload::PoisonMicroblock(_, _) - | TransactionPayload::ContractCall(_) - | TransactionPayload::SmartContract(_) => { - // These transaction payload types all "work" the same: they have associated ExecutionCosts - // and contibute to the block length limit with their tx_len - metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) - } - }; - let denominator = if scalar_cost >= 1 { - scalar_cost as f64 - } else { - 1f64 - }; - let fee_rate = fee as f64 / denominator; - if fee_rate >= 1f64 && fee_rate.is_finite() { - Some(FeeRateAndWeight { - fee_rate, - weight: scalar_cost, - }) - } else { - Some(FeeRateAndWeight { - fee_rate: 1f64, - weight: scalar_cost, - }) - } -} From ba8a6aae67449cb6135559fc917a39dad4fe6310 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:28:24 +0000 Subject: [PATCH 011/103] branch tests --- src/cost_estimates/fee_medians.rs | 6 +- src/cost_estimates/tests/fee_medians.rs | 332 ++++++++++++++++++++++++ src/cost_estimates/tests/mod.rs | 1 + 3 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 src/cost_estimates/tests/fee_medians.rs diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 396aebd580..79eb2362e2 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -44,7 +44,7 @@ CREATE TABLE median_fee_estimator ( /// the subsequent fee rate using the actual paid fee. The 5th, 50th and 95th /// percentile fee rates for each block are used as the low, middle, and high /// estimates. Estimates are updated via exponential decay windowing. -pub struct ScalarFeeRateEstimator { +pub struct WeightedMedianFeeRateEstimator { db: Connection, /// We only look back `window_size` fee rates when averaging past estimates. window_size: u32, @@ -58,7 +58,7 @@ struct FeeRateAndWeight { pub weight: u64, } -impl ScalarFeeRateEstimator { +impl WeightedMedianFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { let db = @@ -195,7 +195,7 @@ impl ScalarFeeRateEstimator { } } -impl FeeEstimator for ScalarFeeRateEstimator { +impl FeeEstimator for WeightedMedianFeeRateEstimator { /// Compute a FeeRateEstimate for this block. Update the /// running estimate using this rounds estimate. fn notify_block( diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs new file mode 100644 index 0000000000..7e5efa3d84 --- /dev/null +++ b/src/cost_estimates/tests/fee_medians.rs @@ -0,0 +1,332 @@ +use std::{env, path::PathBuf}; +use time::Instant; + +use rand::seq::SliceRandom; +use rand::Rng; + +use cost_estimates::metrics::CostMetric; +use cost_estimates::{EstimatorError, FeeEstimator}; +use vm::costs::ExecutionCost; + +use chainstate::burn::ConsensusHash; +use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; +use chainstate::stacks::events::StacksTransactionReceipt; +use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; +use types::proof::TrieHash; +use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; +use util::vrf::VRFProof; + +use crate::chainstate::stacks::{ + CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionSpendingCondition, TransactionVersion, +}; +use crate::core::StacksEpochId; +use crate::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator; +use crate::cost_estimates::FeeRateEstimate; +use crate::types::chainstate::StacksAddress; +use crate::vm::types::{PrincipalData, StandardPrincipalData}; +use crate::vm::Value; + +fn instantiate_test_db(m: CM) -> WeightedMedianFeeRateEstimator { + let mut path = env::temp_dir(); + let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); + path.push(&format!("fee_db_{}.sqlite", &to_hex(&random_bytes)[0..8])); + + WeightedMedianFeeRateEstimator::open(&path, m).expect("Test failure: could not open fee rate DB") +} + +/// This struct implements a simple metric used for unit testing the +/// the fee rate estimator. It always returns a cost of 1, making the +/// fee rate of a transaction always equal to the paid fee. +struct TestCostMetric; + +impl CostMetric for TestCostMetric { + fn from_cost_and_len( + &self, + _cost: &ExecutionCost, + _block_limit: &ExecutionCost, + _tx_len: u64, + ) -> u64 { + 1 + } + + fn from_len(&self, _tx_len: u64) -> u64 { + 1 + } + + fn change_per_byte(&self) -> f64 { + 0f64 + } +} + +#[test] +fn test_empty_fee_estimator() { + let metric = TestCostMetric; + let estimator = instantiate_test_db(metric); + assert_eq!( + estimator + .get_rate_estimates() + .expect_err("Empty rate estimator should error."), + EstimatorError::NoEstimateAvailable + ); +} + +fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { + StacksEpochReceipt { + header: StacksHeaderInfo { + anchored_header: StacksBlockHeader { + version: 1, + total_work: StacksWorkScore { burn: 1, work: 1 }, + proof: VRFProof::empty(), + parent_block: BlockHeaderHash([0; 32]), + parent_microblock: BlockHeaderHash([0; 32]), + parent_microblock_sequence: 0, + tx_merkle_root: Sha512Trunc256Sum([0; 32]), + state_index_root: TrieHash([0; 32]), + microblock_pubkey_hash: Hash160([0; 20]), + }, + microblock_tail: None, + block_height: 1, + index_root: TrieHash([0; 32]), + consensus_hash: ConsensusHash([2; 20]), + burn_header_hash: BurnchainHeaderHash([1; 32]), + burn_header_height: 2, + burn_header_timestamp: 2, + anchored_block_size: 1, + }, + tx_receipts, + matured_rewards: vec![], + matured_rewards_info: None, + parent_microblocks_cost: ExecutionCost::zero(), + anchored_block_cost: ExecutionCost::zero(), + parent_burn_block_hash: BurnchainHeaderHash([0; 32]), + parent_burn_block_height: 1, + parent_burn_block_timestamp: 1, + evaluated_epoch: StacksEpochId::Epoch20, + } +} + +fn make_dummy_coinbase_tx() -> StacksTransaction { + StacksTransaction::new( + TransactionVersion::Mainnet, + TransactionAuth::Standard(TransactionSpendingCondition::new_initial_sighash()), + TransactionPayload::Coinbase(CoinbasePayload([0; 32])), + ) +} + +fn make_dummy_transfer_tx(fee: u64) -> StacksTransactionReceipt { + let mut tx = StacksTransaction::new( + TransactionVersion::Mainnet, + TransactionAuth::Standard(TransactionSpendingCondition::new_initial_sighash()), + TransactionPayload::TokenTransfer( + PrincipalData::Standard(StandardPrincipalData(0, [0; 20])), + 1, + TokenTransferMemo([0; 34]), + ), + ); + tx.set_tx_fee(fee); + + StacksTransactionReceipt::from_stx_transfer( + tx, + vec![], + Value::okay(Value::Bool(true)).unwrap(), + ExecutionCost::zero(), + ) +} + +fn make_dummy_cc_tx(fee: u64) -> StacksTransactionReceipt { + let mut tx = StacksTransaction::new( + TransactionVersion::Mainnet, + TransactionAuth::Standard(TransactionSpendingCondition::new_initial_sighash()), + TransactionPayload::ContractCall(TransactionContractCall { + address: StacksAddress::new(0, Hash160([0; 20])), + contract_name: "cc-dummy".into(), + function_name: "func-name".into(), + function_args: vec![], + }), + ); + tx.set_tx_fee(fee); + StacksTransactionReceipt::from_contract_call( + tx, + vec![], + Value::okay(Value::Bool(true)).unwrap(), + 0, + ExecutionCost::zero(), + ) +} + +#[test] +fn test_fee_estimator() { + let metric = TestCostMetric; + let mut estimator = instantiate_test_db(metric); + + assert_eq!( + estimator + .get_rate_estimates() + .expect_err("Empty rate estimator should error."), + EstimatorError::NoEstimateAvailable, + "Empty rate estimator should return no estimate available" + ); + + let empty_block_receipt = make_block_receipt(vec![]); + let block_limit = ExecutionCost::max_value(); + estimator + .notify_block(&empty_block_receipt, &block_limit) + .expect("Should be able to process an empty block"); + + assert_eq!( + estimator + .get_rate_estimates() + .expect_err("Empty rate estimator should error."), + EstimatorError::NoEstimateAvailable, + "Empty block should not update the estimator" + ); + + let coinbase_only_receipt = make_block_receipt(vec![StacksTransactionReceipt::from_coinbase( + make_dummy_coinbase_tx(), + )]); + + estimator + .notify_block(&coinbase_only_receipt, &block_limit) + .expect("Should be able to process an empty block"); + + assert_eq!( + estimator + .get_rate_estimates() + .expect_err("Empty rate estimator should error."), + EstimatorError::NoEstimateAvailable, + "Coinbase-only block should not update the estimator" + ); + + let single_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(1), + ]); + + estimator + .notify_block(&single_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 1f64, + middle: 1f64, + low: 1f64 + } + ); + + let double_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(1), + make_dummy_transfer_tx(10), + ]); + + estimator + .notify_block(&double_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + + // estimate should increase for "high" and "middle": + // 10 * 1/2 + 1 * 1/2 = 5.5 + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 5.5f64, + middle: 5.5f64, + low: 1f64 + } + ); + + // estimate should increase for "high" and "middle": + // new value: 10 * 1/2 + 5.5 * 1/2 = 7.75 + estimator + .notify_block(&double_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 7.75f64, + middle: 7.75f64, + low: 1f64 + } + ); + + // estimate should increase for "high" and "middle": + // new value: 10 * 1/2 + 7.75 * 1/2 = 8.875 + estimator + .notify_block(&double_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 8.875f64, + middle: 8.875f64, + low: 1f64 + } + ); + + // estimate should increase for "high" and "middle": + // new value: 10 * 1/2 + 8.875 * 1/2 = 9.4375 + estimator + .notify_block(&double_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 9.4375f64, + middle: 9.4375f64, + low: 1f64 + } + ); + + // estimate should increase for "high" and "middle": + // new value: 10 * 1/2 + 9.4375 * 1/2 = 9 + estimator + .notify_block(&double_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 9.71875f64, + middle: 9.71875f64, + low: 1f64 + } + ); + + // make a large block receipt, and expect: + // measured high = 950, middle = 500, low = 50 + // new high: 950/2 + 9.71875/2 = 479.859375 + // new middle: 500/2 + 9.71875/2 = 254.859375 + // new low: 50/2 + 1/2 = 25.5 + + let mut receipts: Vec<_> = (0..100).map(|i| make_dummy_cc_tx(i * 10)).collect(); + let mut rng = rand::thread_rng(); + receipts.shuffle(&mut rng); + + estimator + .notify_block(&make_block_receipt(receipts), &block_limit) + .expect("Should be able to process block receipt"); + + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 479.859375f64, + middle: 254.859375f64, + low: 25.5f64 + } + ); +} diff --git a/src/cost_estimates/tests/mod.rs b/src/cost_estimates/tests/mod.rs index 28ade005c5..a5c89857c8 100644 --- a/src/cost_estimates/tests/mod.rs +++ b/src/cost_estimates/tests/mod.rs @@ -2,6 +2,7 @@ use cost_estimates::FeeRateEstimate; pub mod cost_estimators; pub mod fee_scalar; +pub mod fee_medians; pub mod metrics; #[test] From ddebc1eb379f984e4f9cf2acb8b607e343c26eea Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:34:32 +0000 Subject: [PATCH 012/103] no such column: estimate_key --- src/cost_estimates/fee_medians.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 79eb2362e2..d6e4f05731 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -225,7 +225,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { - let sql = "SELECT high, middle, low FROM scalar_fee_estimator WHERE estimate_key = ?"; + let sql = "SELECT high, middle, low FROM median_fee_estimator WHERE estimate_key = ?"; self.db .query_row(sql, &[SINGLETON_ROW_ID], |row| { let high: f64 = row.get(0)?; From 095f488ed6cc6ccc2e9452b0776bc250017e9916 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 7 Dec 2021 21:41:38 +0000 Subject: [PATCH 013/103] diffed against aaron's version --- src/cost_estimates/fee_medians.rs | 59 +++++++++++-------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index d6e4f05731..37a5799e23 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -3,6 +3,7 @@ use std::cmp::Ordering; use std::convert::TryFrom; use std::{iter::FromIterator, path::Path}; +use rusqlite::AndThenRows; use rusqlite::Transaction as SqlTransaction; use rusqlite::{ types::{FromSql, FromSqlError}, @@ -40,10 +41,13 @@ CREATE TABLE median_fee_estimator ( )"; /// This struct estimates fee rates by translating a transaction's `ExecutionCost` -/// into a scalar using `ExecutionCost::proportion_dot_product` and computing -/// the subsequent fee rate using the actual paid fee. The 5th, 50th and 95th +/// into a scalar using a `CostMetric` (type parameter `M`) and computing +/// the subsequent fee rate using the actual paid fee. The *weighted* 5th, 50th and 95th /// percentile fee rates for each block are used as the low, middle, and high -/// estimates. Estimates are updated via exponential decay windowing. +/// estimates. The fee rates are weighted by the scalar value for each transaction. +/// Blocks which do not exceed at least 1-dimension of the block limit are filled +/// with a rate = 1.0 transaction. Estimates are updated via the median value over +/// a parameterized window. pub struct WeightedMedianFeeRateEstimator { db: Connection, /// We only look back `window_size` fee rates when averaging past estimates. @@ -51,8 +55,7 @@ pub struct WeightedMedianFeeRateEstimator { metric: M, } -/// Pair of "fee rate" and a "weight". The "weight" is a non-negative integer for a transaction -/// that gets its meaning relative to the other weights in the block. +/// Convenience pair for return values. struct FeeRateAndWeight { pub fee_rate: f64, pub weight: u64, @@ -61,27 +64,18 @@ struct FeeRateAndWeight { impl WeightedMedianFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { - let db = - sqlite_open(p, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, false).or_else(|e| { - if let SqliteError::SqliteFailure(ref internal, _) = e { - if let rusqlite::ErrorCode::CannotOpen = internal.code { - let mut db = sqlite_open( - p, - rusqlite::OpenFlags::SQLITE_OPEN_CREATE - | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, - false, - )?; - let tx = tx_begin_immediate_sqlite(&mut db)?; - Self::instantiate_db(&tx)?; - tx.commit()?; - Ok(db) - } else { - Err(e) - } - } else { - Err(e) - } - })?; + let mut db = sqlite_open( + p, + rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, + false, + )?; + + // check if the db needs to be instantiated regardless of whether or not + // it was newly created: the db itself may be shared with other fee estimators, + // which would not have created the necessary table for this estimator. + let tx = tx_begin_immediate_sqlite(&mut db)?; + Self::instantiate_db(&tx)?; + tx.commit()?; Ok(Self { db, @@ -225,18 +219,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { - let sql = "SELECT high, middle, low FROM median_fee_estimator WHERE estimate_key = ?"; - self.db - .query_row(sql, &[SINGLETON_ROW_ID], |row| { - let high: f64 = row.get(0)?; - let middle: f64 = row.get(1)?; - let low: f64 = row.get(2)?; - Ok((high, middle, low)) - }) - .optional() - .expect("SQLite failure") - .map(|(high, middle, low)| FeeRateEstimate { high, middle, low }) - .ok_or_else(|| EstimatorError::NoEstimateAvailable) + Self::get_rate_estimates_from_sql(&self.db, self.window_size) } } From a2d38e124b30bd51d2249214a994874e0b51be8f Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 13:53:00 +0000 Subject: [PATCH 014/103] two working tests --- src/cost_estimates/fee_medians.rs | 4 +- src/cost_estimates/mod.rs | 2 +- src/cost_estimates/tests/fee_medians.rs | 354 +++++++++++++----------- src/cost_estimates/tests/mod.rs | 2 +- 4 files changed, 195 insertions(+), 167 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 37a5799e23..b3d09e3f98 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -63,7 +63,7 @@ struct FeeRateAndWeight { impl WeightedMedianFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. - pub fn open(p: &Path, metric: M) -> Result { + pub fn open(p: &Path, metric: M, window_size: u32) -> Result { let mut db = sqlite_open( p, rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, @@ -80,7 +80,7 @@ impl WeightedMedianFeeRateEstimator { Ok(Self { db, metric, - window_size: 5, + window_size, }) } diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index f298d02339..3dd485f513 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -13,8 +13,8 @@ use vm::costs::ExecutionCost; use burnchains::Txid; use chainstate::stacks::db::StacksEpochReceipt; -pub mod fee_scalar; pub mod fee_medians; +pub mod fee_scalar; pub mod metrics; pub mod pessimistic; diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 7e5efa3d84..3ad7570ca5 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -32,7 +32,9 @@ fn instantiate_test_db(m: CM) -> WeightedMedianFeeRateEstimator< let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); path.push(&format!("fee_db_{}.sqlite", &to_hex(&random_bytes)[0..8])); - WeightedMedianFeeRateEstimator::open(&path, m).expect("Test failure: could not open fee rate DB") + let window_size = 5; + WeightedMedianFeeRateEstimator::open(&path, m, window_size) + .expect("Test failure: could not open fee rate DB") } /// This struct implements a simple metric used for unit testing the @@ -59,18 +61,6 @@ impl CostMetric for TestCostMetric { } } -#[test] -fn test_empty_fee_estimator() { - let metric = TestCostMetric; - let estimator = instantiate_test_db(metric); - assert_eq!( - estimator - .get_rate_estimates() - .expect_err("Empty rate estimator should error."), - EstimatorError::NoEstimateAvailable - ); -} - fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { StacksEpochReceipt { header: StacksHeaderInfo { @@ -156,17 +146,24 @@ fn make_dummy_cc_tx(fee: u64) -> StacksTransactionReceipt { } #[test] -fn test_fee_estimator() { +fn test_empty_fee_estimator() { let metric = TestCostMetric; - let mut estimator = instantiate_test_db(metric); - + let estimator = instantiate_test_db(metric); assert_eq!( estimator .get_rate_estimates() .expect_err("Empty rate estimator should error."), - EstimatorError::NoEstimateAvailable, - "Empty rate estimator should return no estimate available" + EstimatorError::NoEstimateAvailable ); +} + +/// If we do not have any transactions in a block, we should fill the space +/// with a transaction with fee rate 1f. This means that, for a totally empty +/// block, the fee rate should be 1f. +#[test] +fn test_empty_block_returns_minimum() { + let metric = TestCostMetric; + let mut estimator = instantiate_test_db(metric); let empty_block_receipt = make_block_receipt(vec![]); let block_limit = ExecutionCost::max_value(); @@ -174,39 +171,6 @@ fn test_fee_estimator() { .notify_block(&empty_block_receipt, &block_limit) .expect("Should be able to process an empty block"); - assert_eq!( - estimator - .get_rate_estimates() - .expect_err("Empty rate estimator should error."), - EstimatorError::NoEstimateAvailable, - "Empty block should not update the estimator" - ); - - let coinbase_only_receipt = make_block_receipt(vec![StacksTransactionReceipt::from_coinbase( - make_dummy_coinbase_tx(), - )]); - - estimator - .notify_block(&coinbase_only_receipt, &block_limit) - .expect("Should be able to process an empty block"); - - assert_eq!( - estimator - .get_rate_estimates() - .expect_err("Empty rate estimator should error."), - EstimatorError::NoEstimateAvailable, - "Coinbase-only block should not update the estimator" - ); - - let single_tx_receipt = make_block_receipt(vec![ - StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(1), - ]); - - estimator - .notify_block(&single_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - assert_eq!( estimator .get_rate_estimates() @@ -217,116 +181,180 @@ fn test_fee_estimator() { low: 1f64 } ); - - let double_tx_receipt = make_block_receipt(vec![ - StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(1), - make_dummy_transfer_tx(10), - ]); - - estimator - .notify_block(&double_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - - // estimate should increase for "high" and "middle": - // 10 * 1/2 + 1 * 1/2 = 5.5 - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 5.5f64, - middle: 5.5f64, - low: 1f64 - } - ); - - // estimate should increase for "high" and "middle": - // new value: 10 * 1/2 + 5.5 * 1/2 = 7.75 - estimator - .notify_block(&double_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 7.75f64, - middle: 7.75f64, - low: 1f64 - } - ); - - // estimate should increase for "high" and "middle": - // new value: 10 * 1/2 + 7.75 * 1/2 = 8.875 - estimator - .notify_block(&double_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 8.875f64, - middle: 8.875f64, - low: 1f64 - } - ); - - // estimate should increase for "high" and "middle": - // new value: 10 * 1/2 + 8.875 * 1/2 = 9.4375 - estimator - .notify_block(&double_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 9.4375f64, - middle: 9.4375f64, - low: 1f64 - } - ); - - // estimate should increase for "high" and "middle": - // new value: 10 * 1/2 + 9.4375 * 1/2 = 9 - estimator - .notify_block(&double_tx_receipt, &block_limit) - .expect("Should be able to process block receipt"); - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 9.71875f64, - middle: 9.71875f64, - low: 1f64 - } - ); - - // make a large block receipt, and expect: - // measured high = 950, middle = 500, low = 50 - // new high: 950/2 + 9.71875/2 = 479.859375 - // new middle: 500/2 + 9.71875/2 = 254.859375 - // new low: 50/2 + 1/2 = 25.5 - - let mut receipts: Vec<_> = (0..100).map(|i| make_dummy_cc_tx(i * 10)).collect(); - let mut rng = rand::thread_rng(); - receipts.shuffle(&mut rng); - - estimator - .notify_block(&make_block_receipt(receipts), &block_limit) - .expect("Should be able to process block receipt"); - - assert_eq!( - estimator - .get_rate_estimates() - .expect("Should be able to create estimate now"), - FeeRateEstimate { - high: 479.859375f64, - middle: 254.859375f64, - low: 25.5f64 - } - ); } +// +//#[test] +//fn test_fee_estimator() { +// let metric = TestCostMetric; +// let mut estimator = instantiate_test_db(metric); +// +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect_err("Empty rate estimator should error."), +// EstimatorError::NoEstimateAvailable, +// "Empty rate estimator should return no estimate available" +// ); +// +// let empty_block_receipt = make_block_receipt(vec![]); +// let block_limit = ExecutionCost::max_value(); +// estimator +// .notify_block(&empty_block_receipt, &block_limit) +// .expect("Should be able to process an empty block"); +// +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect_err("Empty rate estimator should error."), +// EstimatorError::NoEstimateAvailable, +// "Empty block should not update the estimator" +// ); +// +// let coinbase_only_receipt = make_block_receipt(vec![StacksTransactionReceipt::from_coinbase( +// make_dummy_coinbase_tx(), +// )]); +// +// estimator +// .notify_block(&coinbase_only_receipt, &block_limit) +// .expect("Should be able to process an empty block"); +// +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect_err("Empty rate estimator should error."), +// EstimatorError::NoEstimateAvailable, +// "Coinbase-only block should not update the estimator" +// ); +// +// let single_tx_receipt = make_block_receipt(vec![ +// StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), +// make_dummy_cc_tx(1), +// ]); +// +// estimator +// .notify_block(&single_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 1f64, +// middle: 1f64, +// low: 1f64 +// } +// ); +// +// let double_tx_receipt = make_block_receipt(vec![ +// StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), +// make_dummy_cc_tx(1), +// make_dummy_transfer_tx(10), +// ]); +// +// estimator +// .notify_block(&double_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// +// // estimate should increase for "high" and "middle": +// // 10 * 1/2 + 1 * 1/2 = 5.5 +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 5.5f64, +// middle: 5.5f64, +// low: 1f64 +// } +// ); +// +// // estimate should increase for "high" and "middle": +// // new value: 10 * 1/2 + 5.5 * 1/2 = 7.75 +// estimator +// .notify_block(&double_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 7.75f64, +// middle: 7.75f64, +// low: 1f64 +// } +// ); +// +// // estimate should increase for "high" and "middle": +// // new value: 10 * 1/2 + 7.75 * 1/2 = 8.875 +// estimator +// .notify_block(&double_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 8.875f64, +// middle: 8.875f64, +// low: 1f64 +// } +// ); +// +// // estimate should increase for "high" and "middle": +// // new value: 10 * 1/2 + 8.875 * 1/2 = 9.4375 +// estimator +// .notify_block(&double_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 9.4375f64, +// middle: 9.4375f64, +// low: 1f64 +// } +// ); +// +// // estimate should increase for "high" and "middle": +// // new value: 10 * 1/2 + 9.4375 * 1/2 = 9 +// estimator +// .notify_block(&double_tx_receipt, &block_limit) +// .expect("Should be able to process block receipt"); +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 9.71875f64, +// middle: 9.71875f64, +// low: 1f64 +// } +// ); +// +// // make a large block receipt, and expect: +// // measured high = 950, middle = 500, low = 50 +// // new high: 950/2 + 9.71875/2 = 479.859375 +// // new middle: 500/2 + 9.71875/2 = 254.859375 +// // new low: 50/2 + 1/2 = 25.5 +// +// let mut receipts: Vec<_> = (0..100).map(|i| make_dummy_cc_tx(i * 10)).collect(); +// let mut rng = rand::thread_rng(); +// receipts.shuffle(&mut rng); +// +// estimator +// .notify_block(&make_block_receipt(receipts), &block_limit) +// .expect("Should be able to process block receipt"); +// +// assert_eq!( +// estimator +// .get_rate_estimates() +// .expect("Should be able to create estimate now"), +// FeeRateEstimate { +// high: 479.859375f64, +// middle: 254.859375f64, +// low: 25.5f64 +// } +// ); +//} diff --git a/src/cost_estimates/tests/mod.rs b/src/cost_estimates/tests/mod.rs index a5c89857c8..649a4e34f7 100644 --- a/src/cost_estimates/tests/mod.rs +++ b/src/cost_estimates/tests/mod.rs @@ -1,8 +1,8 @@ use cost_estimates::FeeRateEstimate; pub mod cost_estimators; -pub mod fee_scalar; pub mod fee_medians; +pub mod fee_scalar; pub mod metrics; #[test] From dd475ba0eeb1a7df902de9b7ee9c05586da6b2a8 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 14:48:42 +0000 Subject: [PATCH 015/103] test isn't working --- src/cost_estimates/fee_medians.rs | 6 +- src/cost_estimates/tests/fee_medians.rs | 74 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index b3d09e3f98..81231b58d5 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -56,6 +56,7 @@ pub struct WeightedMedianFeeRateEstimator { } /// Convenience pair for return values. +#[derive(Debug)] struct FeeRateAndWeight { pub fee_rate: f64, pub weight: u64, @@ -242,6 +243,8 @@ fn fee_rate_esimate_from_sorted_weights( let target_percentiles = vec![0.05, 0.5, 0.95]; let mut fees_index = 1; // index into `sorted_fee_rates` let mut values_at_target_percentiles = Vec::new(); + warn!("percentiles {:?}", &percentiles); + warn!("sorted_fee_rates {:?}", &sorted_fee_rates); for target_percentile in target_percentiles { while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { fees_index += 1; @@ -263,9 +266,10 @@ fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_bl } if total_weight < full_block_weight { + let weight_remaining = full_block_weight - total_weight; working_rates.push(FeeRateAndWeight { fee_rate: 1f64, - weight: total_weight, + weight: weight_remaining, }) } } diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 3ad7570ca5..869565a7f1 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -182,6 +182,80 @@ fn test_empty_block_returns_minimum() { } ); } + +#[test] +fn test_simple_contract_call() { + let metric = TestCostMetric; + let mut estimator = instantiate_test_db(metric); + + let single_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(10), + ]); + + let block_limit = ExecutionCost { + write_length: 100, + write_count: 100, + read_length: 100, + read_count: 100, + runtime: 100, + }; + estimator + .notify_block(&single_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + + // The higher fee is 10, because of the contract. + // The lower fee is 1 because of the minimum fee rate padding. + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 10f64, + middle: 10f64, + low: 1f64 + } + ); +} + +#[test] +fn test_five_contract_calls() { + let metric = TestCostMetric; + let mut estimator = instantiate_test_db(metric); + + for i in 1..6 { + warn!("i {}", i); + let single_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(i * 10), + ]); + + let block_limit = ExecutionCost { + write_length: 100, + write_count: 100, + read_length: 100, + read_count: 100, + runtime: 100, + }; + estimator + .notify_block(&single_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + } + + // The higher fee is 10, because of the contract. + // The lower fee is 1 because of the minimum fee rate padding. + assert_eq!( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 10f64, + middle: 10f64, + low: 1f64 + } + ); +} + // //#[test] //fn test_fee_estimator() { From 321fa8e8936e4f765e7b24802f20b92442ebe31c Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 20:34:14 +0000 Subject: [PATCH 016/103] interpolation is working --- src/cost_estimates/fee_medians.rs | 78 ++++++++++++++++++++----- src/cost_estimates/metrics.rs | 6 ++ src/cost_estimates/tests/fee_medians.rs | 76 +++++++++--------------- 3 files changed, 97 insertions(+), 63 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 81231b58d5..848db0ac14 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -52,6 +52,9 @@ pub struct WeightedMedianFeeRateEstimator { db: Connection, /// We only look back `window_size` fee rates when averaging past estimates. window_size: u32, + /// The weight of a "full block" in abstract scalar cost units. This is the weight of + /// a block that is filled on each dimension. + full_block_weight: u64, metric: M, } @@ -82,6 +85,7 @@ impl WeightedMedianFeeRateEstimator { db, metric, window_size, + full_block_weight: 6 * PROPORTION_RESOLUTION, }) } @@ -208,13 +212,20 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { .collect(); // If necessary, add the "minimum" fee rate to fill the block. - maybe_add_minimum_fee_rate(&mut working_fee_rates, PROPORTION_RESOLUTION); - - // Compute a FeeRateEstimate from the sorted, adjusted fee rates. - let block_estimate = fee_rate_esimate_from_sorted_weights(&working_fee_rates); - - // Update the running estimate using this rounds estimate. - self.update_estimate(block_estimate); + maybe_add_minimum_fee_rate(&mut working_fee_rates, self.full_block_weight); + + // If fee rates non-empty, then compute an update. + if working_fee_rates.len() > 0 { + working_fee_rates.sort_by(|a, b| { + a.fee_rate + .partial_cmp(&b.fee_rate) + .unwrap_or(Ordering::Equal) + }); + + let block_estimate = + fee_rate_estimate_from_sorted_weighted_fees(&working_fee_rates); + self.update_estimate(block_estimate); + } Ok(()) } @@ -224,9 +235,14 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } } -fn fee_rate_esimate_from_sorted_weights( +/// `sorted_fee_rates` must be non-empty. +fn fee_rate_estimate_from_sorted_weighted_fees( sorted_fee_rates: &Vec, ) -> FeeRateEstimate { + if sorted_fee_rates.is_empty() { + panic!("`sorted_fee_rates` cannot be empty."); + } + let mut total_weight = 0u64; for rate_and_weight in sorted_fee_rates { total_weight += rate_and_weight.weight; @@ -241,30 +257,50 @@ fn fee_rate_esimate_from_sorted_weights( } let target_percentiles = vec![0.05, 0.5, 0.95]; - let mut fees_index = 1; // index into `sorted_fee_rates` + let mut fees_index = 0; // index into `sorted_fee_rates` let mut values_at_target_percentiles = Vec::new(); warn!("percentiles {:?}", &percentiles); warn!("sorted_fee_rates {:?}", &sorted_fee_rates); + warn!("percentiles {:?}", &percentiles); for target_percentile in target_percentiles { while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { fees_index += 1; } - // TODO: use an interpolation - values_at_target_percentiles.push(&sorted_fee_rates[fees_index - 1]); + let v = if fees_index == 0 { + warn!("fees_index == 0"); + sorted_fee_rates[0].fee_rate + } else if fees_index == percentiles.len() { + warn!("fees_index == percentiles.len()"); + sorted_fee_rates.last().unwrap().fee_rate + } else { + warn!("fees_index < percentiles.len()"); + // Notation mimics https://en.wikipedia.org/wiki/Percentile#Weighted_percentile + let vk = sorted_fee_rates[fees_index - 1].fee_rate; + let vk1 = sorted_fee_rates[fees_index].fee_rate; + let pk = percentiles[fees_index - 1]; + let pk1 = percentiles[fees_index]; + vk + (target_percentile - pk) / (pk1 - pk) * (vk1 - vk) + }; + values_at_target_percentiles.push(v); } FeeRateEstimate { - high: values_at_target_percentiles[2].fee_rate, - middle: values_at_target_percentiles[1].fee_rate, - low: values_at_target_percentiles[0].fee_rate, + high: values_at_target_percentiles[2], + middle: values_at_target_percentiles[1], + low: values_at_target_percentiles[0], } } + fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { let mut total_weight = 0u64; for rate_and_weight in working_rates.into_iter() { total_weight += rate_and_weight.weight; } + warn!( + "total_weight {} full_block_weight {}", + total_weight, full_block_weight + ); if total_weight < full_block_weight { let weight_remaining = full_block_weight - total_weight; working_rates.push(FeeRateAndWeight { @@ -281,16 +317,22 @@ fn fee_rate_and_weight_from_receipt( block_limit: &ExecutionCost, ) -> Option { let (payload, fee, tx_size) = match tx_receipt.transaction { - TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), + TransactionOrigin::Stacks(ref tx) => { + let fee = tx.get_tx_fee(); + warn!("fee_paid: {}", fee); + Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())) + } TransactionOrigin::Burn(_) => None, }?; let scalar_cost = match payload { TransactionPayload::TokenTransfer(_, _, _) => { // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. + warn!("check"); metric.from_len(tx_size) } TransactionPayload::Coinbase(_) => { // Coinbase txs are "free", so they don't factor into the fee market. + warn!("check"); return None; } TransactionPayload::PoisonMicroblock(_, _) @@ -298,15 +340,21 @@ fn fee_rate_and_weight_from_receipt( | TransactionPayload::SmartContract(_) => { // These transaction payload types all "work" the same: they have associated ExecutionCosts // and contibute to the block length limit with their tx_len + warn!("check {:?}", &tx_receipt.execution_cost); metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) } }; + warn!("scalar_cost {}", scalar_cost); let denominator = if scalar_cost >= 1 { scalar_cost as f64 } else { 1f64 }; let fee_rate = fee as f64 / denominator; + warn!("fee_rate {}", fee_rate); + let part1 = fee_rate >= 1f64; + let part2 = fee_rate.is_finite(); + warn!("part1 {} part2 {}", part1, part2); if fee_rate >= 1f64 && fee_rate.is_finite() { Some(FeeRateAndWeight { fee_rate, diff --git a/src/cost_estimates/metrics.rs b/src/cost_estimates/metrics.rs index 1deb98f651..7fed9db6ce 100644 --- a/src/cost_estimates/metrics.rs +++ b/src/cost_estimates/metrics.rs @@ -29,6 +29,7 @@ impl CostMetric for Box { block_limit: &ExecutionCost, tx_len: u64, ) -> u64 { + warn!("check"); self.as_ref().from_cost_and_len(cost, block_limit, tx_len) } @@ -81,6 +82,10 @@ impl CostMetric for ProportionalDotProduct { ) -> u64 { let exec_proportion = cost.proportion_dot_product(block_limit, PROPORTION_RESOLUTION); let len_proportion = self.calculate_len_proportion(tx_len); + warn!( + "exec_proportion {} len_proportion {}", + exec_proportion, len_proportion + ); exec_proportion + len_proportion } @@ -100,6 +105,7 @@ impl CostMetric for UnitMetric { _block_limit: &ExecutionCost, _tx_len: u64, ) -> u64 { + warn!("check"); 1 } diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 869565a7f1..c15192fa0f 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -22,6 +22,7 @@ use crate::chainstate::stacks::{ }; use crate::core::StacksEpochId; use crate::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator; +use crate::cost_estimates::metrics::ProportionalDotProduct; use crate::cost_estimates::FeeRateEstimate; use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; @@ -37,30 +38,6 @@ fn instantiate_test_db(m: CM) -> WeightedMedianFeeRateEstimator< .expect("Test failure: could not open fee rate DB") } -/// This struct implements a simple metric used for unit testing the -/// the fee rate estimator. It always returns a cost of 1, making the -/// fee rate of a transaction always equal to the paid fee. -struct TestCostMetric; - -impl CostMetric for TestCostMetric { - fn from_cost_and_len( - &self, - _cost: &ExecutionCost, - _block_limit: &ExecutionCost, - _tx_len: u64, - ) -> u64 { - 1 - } - - fn from_len(&self, _tx_len: u64) -> u64 { - 1 - } - - fn change_per_byte(&self) -> f64 { - 0f64 - } -} - fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { StacksEpochReceipt { header: StacksHeaderInfo { @@ -124,7 +101,7 @@ fn make_dummy_transfer_tx(fee: u64) -> StacksTransactionReceipt { ) } -fn make_dummy_cc_tx(fee: u64) -> StacksTransactionReceipt { +fn make_dummy_cc_tx(fee: u64, execution_cost: &ExecutionCost) -> StacksTransactionReceipt { let mut tx = StacksTransaction::new( TransactionVersion::Mainnet, TransactionAuth::Standard(TransactionSpendingCondition::new_initial_sighash()), @@ -141,13 +118,29 @@ fn make_dummy_cc_tx(fee: u64) -> StacksTransactionReceipt { vec![], Value::okay(Value::Bool(true)).unwrap(), 0, - ExecutionCost::zero(), + execution_cost.clone(), ) } +const block_limit: ExecutionCost = ExecutionCost { + write_length: 100, + write_count: 100, + read_length: 100, + read_count: 100, + runtime: 100, +}; + +const operation_cost: ExecutionCost = ExecutionCost { + write_length: 10, + write_count: 10, + read_length: 10, + read_count: 10, + runtime: 10, +}; + #[test] fn test_empty_fee_estimator() { - let metric = TestCostMetric; + let metric = ProportionalDotProduct::new(10_000); let estimator = instantiate_test_db(metric); assert_eq!( estimator @@ -162,11 +155,10 @@ fn test_empty_fee_estimator() { /// block, the fee rate should be 1f. #[test] fn test_empty_block_returns_minimum() { - let metric = TestCostMetric; + let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); let empty_block_receipt = make_block_receipt(vec![]); - let block_limit = ExecutionCost::max_value(); estimator .notify_block(&empty_block_receipt, &block_limit) .expect("Should be able to process an empty block"); @@ -185,21 +177,16 @@ fn test_empty_block_returns_minimum() { #[test] fn test_simple_contract_call() { - let metric = TestCostMetric; + let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); + // The scalar cost of `make_dummy_cc_tx(_, &operation_cost)`. + let operation_cost_basis = 5160; let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(10), + make_dummy_cc_tx(10 * operation_cost_basis, &operation_cost), ]); - let block_limit = ExecutionCost { - write_length: 100, - write_count: 100, - read_length: 100, - read_count: 100, - runtime: 100, - }; estimator .notify_block(&single_tx_receipt, &block_limit) .expect("Should be able to process block receipt"); @@ -220,23 +207,16 @@ fn test_simple_contract_call() { #[test] fn test_five_contract_calls() { - let metric = TestCostMetric; + let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); for i in 1..6 { warn!("i {}", i); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(i * 10), + make_dummy_cc_tx(i * 10, &operation_cost), ]); - let block_limit = ExecutionCost { - write_length: 100, - write_count: 100, - read_length: 100, - read_count: 100, - runtime: 100, - }; estimator .notify_block(&single_tx_receipt, &block_limit) .expect("Should be able to process block receipt"); @@ -259,7 +239,7 @@ fn test_five_contract_calls() { // //#[test] //fn test_fee_estimator() { -// let metric = TestCostMetric; +// let metric = ProportionalDotProduct; // let mut estimator = instantiate_test_db(metric); // // assert_eq!( From f9e6721c35d6157119f67c176938ca59345de427 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:03:51 +0000 Subject: [PATCH 017/103] fixed test_single_contract_call --- src/cost_estimates/fee_medians.rs | 3 +-- src/cost_estimates/tests/fee_medians.rs | 35 ++++++++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 848db0ac14..321fa76cb5 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -222,8 +222,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { .unwrap_or(Ordering::Equal) }); - let block_estimate = - fee_rate_estimate_from_sorted_weighted_fees(&working_fee_rates); + let block_estimate = fee_rate_estimate_from_sorted_weighted_fees(&working_fee_rates); self.update_estimate(block_estimate); } diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index c15192fa0f..c0e71043c8 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -28,6 +28,21 @@ use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; +const error_epsilon: f64 = 0.1; + +/// Returns `true` iff each value in `left` is within `error_epsilon` of the +/// corresponding value in `right`. +fn is_close(left: FeeRateEstimate, right: FeeRateEstimate) -> bool { + warn!("Checking ExecutionCost's. {:?} vs {:?}", left, right); + let is_ok = (left.high - right.high).abs() < error_epsilon + && (left.middle - right.middle).abs() < error_epsilon + && (left.low - right.low).abs() < error_epsilon; + if !is_ok { + warn!("ExecutionCost's are not close. {:?} vs {:?}", left, right); + } + is_ok +} + fn instantiate_test_db(m: CM) -> WeightedMedianFeeRateEstimator { let mut path = env::temp_dir(); let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); @@ -163,7 +178,7 @@ fn test_empty_block_returns_minimum() { .notify_block(&empty_block_receipt, &block_limit) .expect("Should be able to process an empty block"); - assert_eq!( + assert!(is_close( estimator .get_rate_estimates() .expect("Should be able to create estimate now"), @@ -172,11 +187,11 @@ fn test_empty_block_returns_minimum() { middle: 1f64, low: 1f64 } - ); + )); } #[test] -fn test_simple_contract_call() { +fn test_single_contract_call() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); @@ -191,18 +206,18 @@ fn test_simple_contract_call() { .notify_block(&single_tx_receipt, &block_limit) .expect("Should be able to process block receipt"); - // The higher fee is 10, because of the contract. + // The higher fee is 10, because that's what we paid. // The lower fee is 1 because of the minimum fee rate padding. - assert_eq!( + assert!(is_close( estimator .get_rate_estimates() .expect("Should be able to create estimate now"), FeeRateEstimate { - high: 10f64, - middle: 10f64, + high: 9.87f64, + middle: 1.77f64, low: 1f64 } - ); + )); } #[test] @@ -224,7 +239,7 @@ fn test_five_contract_calls() { // The higher fee is 10, because of the contract. // The lower fee is 1 because of the minimum fee rate padding. - assert_eq!( + assert!(is_close( estimator .get_rate_estimates() .expect("Should be able to create estimate now"), @@ -233,7 +248,7 @@ fn test_five_contract_calls() { middle: 10f64, low: 1f64 } - ); + )); } // From 03f87b013afa3f9d4006d4d80c94328a57158582 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:12:16 +0000 Subject: [PATCH 018/103] five calls is failing --- src/cost_estimates/tests/fee_medians.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index c0e71043c8..63ed0b9965 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -145,7 +145,7 @@ const block_limit: ExecutionCost = ExecutionCost { runtime: 100, }; -const operation_cost: ExecutionCost = ExecutionCost { +const tenth_operation_cost: ExecutionCost = ExecutionCost { write_length: 10, write_count: 10, read_length: 10, @@ -153,6 +153,9 @@ const operation_cost: ExecutionCost = ExecutionCost { runtime: 10, }; +// The scalar cost of `make_dummy_cc_tx(_, &tenth_operation_cost)`. +const contract_call_cost_basis:u64 = 5160; + #[test] fn test_empty_fee_estimator() { let metric = ProportionalDotProduct::new(10_000); @@ -195,11 +198,9 @@ fn test_single_contract_call() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); - // The scalar cost of `make_dummy_cc_tx(_, &operation_cost)`. - let operation_cost_basis = 5160; let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(10 * operation_cost_basis, &operation_cost), + make_dummy_cc_tx(10 * contract_call_cost_basis, &tenth_operation_cost), ]); estimator @@ -229,7 +230,7 @@ fn test_five_contract_calls() { warn!("i {}", i); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(i * 10, &operation_cost), + make_dummy_cc_tx(i * 10 * contract_call_cost_basis, &tenth_operation_cost), ]); estimator From a3f467760588f48fe0901f527e616bed0a2d2e34 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:23:02 +0000 Subject: [PATCH 019/103] test_one_block_mostly_filled added --- src/cost_estimates/tests/fee_medians.rs | 51 ++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 63ed0b9965..0f59ff354d 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -153,8 +153,19 @@ const tenth_operation_cost: ExecutionCost = ExecutionCost { runtime: 10, }; +const half_operation_cost: ExecutionCost = ExecutionCost { + write_length: 50, + write_count: 50, + read_length: 50, + read_count: 50, + runtime: 50, +}; + // The scalar cost of `make_dummy_cc_tx(_, &tenth_operation_cost)`. -const contract_call_cost_basis:u64 = 5160; +const tenth_operation_cost_basis:u64 = 5160; + +// The scalar cost of `make_dummy_cc_tx(_, &half_operation_cost)`. +const half_operation_cost_basis:u64 = 25160; #[test] fn test_empty_fee_estimator() { @@ -194,13 +205,13 @@ fn test_empty_block_returns_minimum() { } #[test] -fn test_single_contract_call() { +fn test_one_block_partially_filled() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(10 * contract_call_cost_basis, &tenth_operation_cost), + make_dummy_cc_tx(10 * tenth_operation_cost_basis, &tenth_operation_cost), ]); estimator @@ -222,7 +233,36 @@ fn test_single_contract_call() { } #[test] -fn test_five_contract_calls() { +fn test_one_block_mostly_filled() { + let metric = ProportionalDotProduct::new(10_000); + let mut estimator = instantiate_test_db(metric); + + let single_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(10 * half_operation_cost_basis, &half_operation_cost), + make_dummy_cc_tx(10 * half_operation_cost_basis, &half_operation_cost), + ]); + + estimator + .notify_block(&single_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + + // The higher fee is 10, because that's what we paid. + // The lower fee is 1 because of the minimum fee rate padding. + assert!(is_close( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 10.0f64, + middle: 10.0f64, + low: 1f64 + } + )); +} + +#[test] +fn test_five_blocks_mostly_filled() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); @@ -230,7 +270,8 @@ fn test_five_contract_calls() { warn!("i {}", i); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), - make_dummy_cc_tx(i * 10 * contract_call_cost_basis, &tenth_operation_cost), + make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), + make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), ]); estimator From 4958b7e0fbcb31f161042557318697e3f1976874 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:30:24 +0000 Subject: [PATCH 020/103] test_ten_blocks_mostly_filled working --- src/cost_estimates/tests/fee_medians.rs | 211 ++++-------------------- 1 file changed, 33 insertions(+), 178 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 0f59ff354d..4e2f355a8e 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -286,186 +286,41 @@ fn test_five_blocks_mostly_filled() { .get_rate_estimates() .expect("Should be able to create estimate now"), FeeRateEstimate { - high: 10f64, - middle: 10f64, + high: 30f64, + middle: 30f64, low: 1f64 } )); } -// -//#[test] -//fn test_fee_estimator() { -// let metric = ProportionalDotProduct; -// let mut estimator = instantiate_test_db(metric); -// -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect_err("Empty rate estimator should error."), -// EstimatorError::NoEstimateAvailable, -// "Empty rate estimator should return no estimate available" -// ); -// -// let empty_block_receipt = make_block_receipt(vec![]); -// let block_limit = ExecutionCost::max_value(); -// estimator -// .notify_block(&empty_block_receipt, &block_limit) -// .expect("Should be able to process an empty block"); -// -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect_err("Empty rate estimator should error."), -// EstimatorError::NoEstimateAvailable, -// "Empty block should not update the estimator" -// ); -// -// let coinbase_only_receipt = make_block_receipt(vec![StacksTransactionReceipt::from_coinbase( -// make_dummy_coinbase_tx(), -// )]); -// -// estimator -// .notify_block(&coinbase_only_receipt, &block_limit) -// .expect("Should be able to process an empty block"); -// -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect_err("Empty rate estimator should error."), -// EstimatorError::NoEstimateAvailable, -// "Coinbase-only block should not update the estimator" -// ); -// -// let single_tx_receipt = make_block_receipt(vec![ -// StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), -// make_dummy_cc_tx(1), -// ]); -// -// estimator -// .notify_block(&single_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 1f64, -// middle: 1f64, -// low: 1f64 -// } -// ); -// -// let double_tx_receipt = make_block_receipt(vec![ -// StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), -// make_dummy_cc_tx(1), -// make_dummy_transfer_tx(10), -// ]); -// -// estimator -// .notify_block(&double_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// -// // estimate should increase for "high" and "middle": -// // 10 * 1/2 + 1 * 1/2 = 5.5 -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 5.5f64, -// middle: 5.5f64, -// low: 1f64 -// } -// ); -// -// // estimate should increase for "high" and "middle": -// // new value: 10 * 1/2 + 5.5 * 1/2 = 7.75 -// estimator -// .notify_block(&double_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 7.75f64, -// middle: 7.75f64, -// low: 1f64 -// } -// ); -// -// // estimate should increase for "high" and "middle": -// // new value: 10 * 1/2 + 7.75 * 1/2 = 8.875 -// estimator -// .notify_block(&double_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 8.875f64, -// middle: 8.875f64, -// low: 1f64 -// } -// ); -// -// // estimate should increase for "high" and "middle": -// // new value: 10 * 1/2 + 8.875 * 1/2 = 9.4375 -// estimator -// .notify_block(&double_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 9.4375f64, -// middle: 9.4375f64, -// low: 1f64 -// } -// ); -// -// // estimate should increase for "high" and "middle": -// // new value: 10 * 1/2 + 9.4375 * 1/2 = 9 -// estimator -// .notify_block(&double_tx_receipt, &block_limit) -// .expect("Should be able to process block receipt"); -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 9.71875f64, -// middle: 9.71875f64, -// low: 1f64 -// } -// ); -// -// // make a large block receipt, and expect: -// // measured high = 950, middle = 500, low = 50 -// // new high: 950/2 + 9.71875/2 = 479.859375 -// // new middle: 500/2 + 9.71875/2 = 254.859375 -// // new low: 50/2 + 1/2 = 25.5 -// -// let mut receipts: Vec<_> = (0..100).map(|i| make_dummy_cc_tx(i * 10)).collect(); -// let mut rng = rand::thread_rng(); -// receipts.shuffle(&mut rng); -// -// estimator -// .notify_block(&make_block_receipt(receipts), &block_limit) -// .expect("Should be able to process block receipt"); -// -// assert_eq!( -// estimator -// .get_rate_estimates() -// .expect("Should be able to create estimate now"), -// FeeRateEstimate { -// high: 479.859375f64, -// middle: 254.859375f64, -// low: 25.5f64 -// } -// ); -//} +#[test] +fn test_ten_blocks_mostly_filled() { + let metric = ProportionalDotProduct::new(10_000); + let mut estimator = instantiate_test_db(metric); + + for i in 1..11 { + warn!("i {}", i); + let single_tx_receipt = make_block_receipt(vec![ + StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), + make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), + make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), + ]); + + estimator + .notify_block(&single_tx_receipt, &block_limit) + .expect("Should be able to process block receipt"); + } + + // The higher fee is 10, because of the contract. + // The lower fee is 1 because of the minimum fee rate padding. + assert!(is_close( + estimator + .get_rate_estimates() + .expect("Should be able to create estimate now"), + FeeRateEstimate { + high: 80f64, + middle: 80f64, + low: 1f64 + } + )); +} From 8f53b9ba62804bf49d34961d22d2b69a6ac48f38 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:47:58 +0000 Subject: [PATCH 021/103] added comments --- src/cost_estimates/fee_medians.rs | 44 +++++++++++++------------ src/cost_estimates/tests/fee_medians.rs | 40 ++++++++++------------ 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 321fa76cb5..1c5af5b9bd 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -31,7 +31,6 @@ use super::{EstimatorError, FeeEstimator}; use super::metrics::PROPORTION_RESOLUTION; use cost_estimates::StacksTransactionReceipt; -const SINGLETON_ROW_ID: i64 = 1; const CREATE_TABLE: &'static str = " CREATE TABLE median_fee_estimator ( measure_key INTEGER PRIMARY KEY AUTOINCREMENT, @@ -40,14 +39,15 @@ CREATE TABLE median_fee_estimator ( low NUMBER NOT NULL )"; -/// This struct estimates fee rates by translating a transaction's `ExecutionCost` -/// into a scalar using a `CostMetric` (type parameter `M`) and computing -/// the subsequent fee rate using the actual paid fee. The *weighted* 5th, 50th and 95th -/// percentile fee rates for each block are used as the low, middle, and high -/// estimates. The fee rates are weighted by the scalar value for each transaction. -/// Blocks which do not exceed at least 1-dimension of the block limit are filled -/// with a rate = 1.0 transaction. Estimates are updated via the median value over -/// a parameterized window. +/// FeeRateEstimator with the following properties: +/// +/// 1) We use a "weighted" percentile approach for calculating the percentile values. Described +/// below, larger transactions contribute more to the ranking than small transactions. +/// 2) Use "windowed" decay instead of exponential decay. This allows outliers to be forgotten +/// faster, and so reduces the influence of outliers. +/// 3) "Pad" the block, so that any unused spaces is considered to have an associated fee rate of +/// 1f, the minimum. Ignoring the amount of empty space leads to over-estimates because it +/// ignores the fact that there was still space in the block. pub struct WeightedMedianFeeRateEstimator { db: Connection, /// We only look back `window_size` fee rates when averaging past estimates. @@ -55,10 +55,11 @@ pub struct WeightedMedianFeeRateEstimator { /// The weight of a "full block" in abstract scalar cost units. This is the weight of /// a block that is filled on each dimension. full_block_weight: u64, + /// Use this cost metric in fee rate calculations. metric: M, } -/// Convenience pair for return values. +/// Convenience struct for passing around this pair. #[derive(Debug)] struct FeeRateAndWeight { pub fee_rate: f64, @@ -145,8 +146,8 @@ impl WeightedMedianFeeRateEstimator { } } - // sort our float arrays. for float values that do not compare easily, - // treat them as equals. + // Sort our float arrays. For float values that do not compare easily, + // treat them as equals. highs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); mids.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); lows.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); @@ -160,28 +161,22 @@ impl WeightedMedianFeeRateEstimator { fn update_estimate(&mut self, new_measure: FeeRateEstimate) { let tx = tx_begin_immediate_sqlite(&mut self.db).expect("SQLite failure"); - let insert_sql = "INSERT INTO median_fee_estimator (high, middle, low) VALUES (?, ?, ?)"; - let deletion_sql = "DELETE FROM median_fee_estimator WHERE measure_key <= ( SELECT MAX(measure_key) - ? FROM median_fee_estimator )"; - tx.execute( insert_sql, rusqlite::params![new_measure.high, new_measure.middle, new_measure.low,], ) .expect("SQLite failure"); - tx.execute(deletion_sql, rusqlite::params![self.window_size]) .expect("SQLite failure"); let estimate = Self::get_rate_estimates_from_sql(&tx, self.window_size); - tx.commit().expect("SQLite failure"); - if let Ok(next_estimate) = estimate { debug!("Updating fee rate estimate for new block"; "new_measure_high" => new_measure.high, @@ -195,8 +190,6 @@ impl WeightedMedianFeeRateEstimator { } impl FeeEstimator for WeightedMedianFeeRateEstimator { - /// Compute a FeeRateEstimate for this block. Update the - /// running estimate using this rounds estimate. fn notify_block( &mut self, receipt: &StacksEpochReceipt, @@ -216,12 +209,14 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { // If fee rates non-empty, then compute an update. if working_fee_rates.len() > 0 { + // Values must be sorted. working_fee_rates.sort_by(|a, b| { a.fee_rate .partial_cmp(&b.fee_rate) .unwrap_or(Ordering::Equal) }); + // Compute the estimate and update. let block_estimate = fee_rate_estimate_from_sorted_weighted_fees(&working_fee_rates); self.update_estimate(block_estimate); } @@ -234,6 +229,11 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } } +/// Computes a `FeeRateEstimate` based on `sorted_fee_rates` using a "weighted percentile" method +/// described in https://en.wikipedia.org/wiki/Percentile#Weighted_percentile +/// +/// The percentiles computed are [0.05, 0.5, 0.95]. +/// /// `sorted_fee_rates` must be non-empty. fn fee_rate_estimate_from_sorted_weighted_fees( sorted_fee_rates: &Vec, @@ -290,6 +290,8 @@ fn fee_rate_estimate_from_sorted_weighted_fees( } } +/// If the weights in `working_rates` do not add up to `full_block_weight`, add a new entry **in +/// place** that takes up the remaining space. fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { let mut total_weight = 0u64; for rate_and_weight in working_rates.into_iter() { @@ -309,7 +311,7 @@ fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_bl } } -/// The fee rate is the `fee_paid/cost_metric_used` +/// Depending on the type of the transaction, calculate fee rate and total cost. fn fee_rate_and_weight_from_receipt( metric: &dyn CostMetric, tx_receipt: &StacksTransactionReceipt, diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 4e2f355a8e..14d1681157 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -28,12 +28,13 @@ use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; +/// Tolerance for approximate comparison. const error_epsilon: f64 = 0.1; /// Returns `true` iff each value in `left` is within `error_epsilon` of the /// corresponding value in `right`. fn is_close(left: FeeRateEstimate, right: FeeRateEstimate) -> bool { - warn!("Checking ExecutionCost's. {:?} vs {:?}", left, right); + warn!("Checking ExecutionCost's. {:?} vs {:?}", left, right); let is_ok = (left.high - right.high).abs() < error_epsilon && (left.middle - right.middle).abs() < error_epsilon && (left.low - right.low).abs() < error_epsilon; @@ -96,26 +97,6 @@ fn make_dummy_coinbase_tx() -> StacksTransaction { ) } -fn make_dummy_transfer_tx(fee: u64) -> StacksTransactionReceipt { - let mut tx = StacksTransaction::new( - TransactionVersion::Mainnet, - TransactionAuth::Standard(TransactionSpendingCondition::new_initial_sighash()), - TransactionPayload::TokenTransfer( - PrincipalData::Standard(StandardPrincipalData(0, [0; 20])), - 1, - TokenTransferMemo([0; 34]), - ), - ); - tx.set_tx_fee(fee); - - StacksTransactionReceipt::from_stx_transfer( - tx, - vec![], - Value::okay(Value::Bool(true)).unwrap(), - ExecutionCost::zero(), - ) -} - fn make_dummy_cc_tx(fee: u64, execution_cost: &ExecutionCost) -> StacksTransactionReceipt { let mut tx = StacksTransaction::new( TransactionVersion::Mainnet, @@ -162,11 +143,12 @@ const half_operation_cost: ExecutionCost = ExecutionCost { }; // The scalar cost of `make_dummy_cc_tx(_, &tenth_operation_cost)`. -const tenth_operation_cost_basis:u64 = 5160; +const tenth_operation_cost_basis: u64 = 5160; // The scalar cost of `make_dummy_cc_tx(_, &half_operation_cost)`. -const half_operation_cost_basis:u64 = 25160; +const half_operation_cost_basis: u64 = 25160; +/// Tests that we have no estimate available until we `notify`. #[test] fn test_empty_fee_estimator() { let metric = ProportionalDotProduct::new(10_000); @@ -204,6 +186,8 @@ fn test_empty_block_returns_minimum() { )); } +/// A block that is only a very small minority filled should reflect the paid value, +/// but be dominated by the padded fee rate. #[test] fn test_one_block_partially_filled() { let metric = ProportionalDotProduct::new(10_000); @@ -232,6 +216,8 @@ fn test_one_block_partially_filled() { )); } +/// A block that is mostly filled should create an estimate dominated by the transactions paid, and +/// the padding should only affect `low`. #[test] fn test_one_block_mostly_filled() { let metric = ProportionalDotProduct::new(10_000); @@ -261,6 +247,10 @@ fn test_one_block_mostly_filled() { )); } +/// Tests the effect of adding blocks over time. We add five blocks with an easy to calculate +/// median. +/// +/// We add 5 blocks with window size 5 so none should be forgotten. #[test] fn test_five_blocks_mostly_filled() { let metric = ProportionalDotProduct::new(10_000); @@ -293,6 +283,10 @@ fn test_five_blocks_mostly_filled() { )); } +/// Tests the effect of adding blocks over time. We add five blocks with an easy to calculate +/// median. +/// +/// We add 10 blocks with window size 5 so the first 5 should be forgotten. #[test] fn test_ten_blocks_mostly_filled() { let metric = ProportionalDotProduct::new(10_000); From 785c2edc545b87da275626c36ff487396d11606d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:52:32 +0000 Subject: [PATCH 022/103] revert src/cost_estimates/metrics.rs --- src/cost_estimates/metrics.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/cost_estimates/metrics.rs b/src/cost_estimates/metrics.rs index 7fed9db6ce..0cb0b4af16 100644 --- a/src/cost_estimates/metrics.rs +++ b/src/cost_estimates/metrics.rs @@ -5,11 +5,6 @@ use crate::vm::costs::ExecutionCost; /// This trait defines metrics used to convert `ExecutionCost` and tx_len usage into single-dimensional /// metrics that can be used to compute a fee rate. pub trait CostMetric: Send { - /// Returns a single-dimensional integer representing the proportion of `block_limit` that - /// `cost` and `tx_len` up. - /// - /// TODO: Can we state more invariants about this? E.g., that the sum of all costs in a block - /// equals some constant value? fn from_cost_and_len( &self, cost: &ExecutionCost, @@ -29,7 +24,6 @@ impl CostMetric for Box { block_limit: &ExecutionCost, tx_len: u64, ) -> u64 { - warn!("check"); self.as_ref().from_cost_and_len(cost, block_limit, tx_len) } @@ -82,10 +76,6 @@ impl CostMetric for ProportionalDotProduct { ) -> u64 { let exec_proportion = cost.proportion_dot_product(block_limit, PROPORTION_RESOLUTION); let len_proportion = self.calculate_len_proportion(tx_len); - warn!( - "exec_proportion {} len_proportion {}", - exec_proportion, len_proportion - ); exec_proportion + len_proportion } @@ -105,7 +95,6 @@ impl CostMetric for UnitMetric { _block_limit: &ExecutionCost, _tx_len: u64, ) -> u64 { - warn!("check"); 1 } From b4df12c1b484b39dd0eaca5e59f0f6d108d108e3 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:56:14 +0000 Subject: [PATCH 023/103] removed warn's --- src/cost_estimates/fee_medians.rs | 20 -------------------- src/cost_estimates/tests/fee_medians.rs | 3 --- 2 files changed, 23 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 1c5af5b9bd..a388c0f26d 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -258,21 +258,15 @@ fn fee_rate_estimate_from_sorted_weighted_fees( let target_percentiles = vec![0.05, 0.5, 0.95]; let mut fees_index = 0; // index into `sorted_fee_rates` let mut values_at_target_percentiles = Vec::new(); - warn!("percentiles {:?}", &percentiles); - warn!("sorted_fee_rates {:?}", &sorted_fee_rates); - warn!("percentiles {:?}", &percentiles); for target_percentile in target_percentiles { while fees_index < percentiles.len() && percentiles[fees_index] < target_percentile { fees_index += 1; } let v = if fees_index == 0 { - warn!("fees_index == 0"); sorted_fee_rates[0].fee_rate } else if fees_index == percentiles.len() { - warn!("fees_index == percentiles.len()"); sorted_fee_rates.last().unwrap().fee_rate } else { - warn!("fees_index < percentiles.len()"); // Notation mimics https://en.wikipedia.org/wiki/Percentile#Weighted_percentile let vk = sorted_fee_rates[fees_index - 1].fee_rate; let vk1 = sorted_fee_rates[fees_index].fee_rate; @@ -298,10 +292,6 @@ fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_bl total_weight += rate_and_weight.weight; } - warn!( - "total_weight {} full_block_weight {}", - total_weight, full_block_weight - ); if total_weight < full_block_weight { let weight_remaining = full_block_weight - total_weight; working_rates.push(FeeRateAndWeight { @@ -319,8 +309,6 @@ fn fee_rate_and_weight_from_receipt( ) -> Option { let (payload, fee, tx_size) = match tx_receipt.transaction { TransactionOrigin::Stacks(ref tx) => { - let fee = tx.get_tx_fee(); - warn!("fee_paid: {}", fee); Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())) } TransactionOrigin::Burn(_) => None, @@ -328,12 +316,10 @@ fn fee_rate_and_weight_from_receipt( let scalar_cost = match payload { TransactionPayload::TokenTransfer(_, _, _) => { // TokenTransfers *only* contribute tx_len, and just have an empty ExecutionCost. - warn!("check"); metric.from_len(tx_size) } TransactionPayload::Coinbase(_) => { // Coinbase txs are "free", so they don't factor into the fee market. - warn!("check"); return None; } TransactionPayload::PoisonMicroblock(_, _) @@ -341,21 +327,15 @@ fn fee_rate_and_weight_from_receipt( | TransactionPayload::SmartContract(_) => { // These transaction payload types all "work" the same: they have associated ExecutionCosts // and contibute to the block length limit with their tx_len - warn!("check {:?}", &tx_receipt.execution_cost); metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) } }; - warn!("scalar_cost {}", scalar_cost); let denominator = if scalar_cost >= 1 { scalar_cost as f64 } else { 1f64 }; let fee_rate = fee as f64 / denominator; - warn!("fee_rate {}", fee_rate); - let part1 = fee_rate >= 1f64; - let part2 = fee_rate.is_finite(); - warn!("part1 {} part2 {}", part1, part2); if fee_rate >= 1f64 && fee_rate.is_finite() { Some(FeeRateAndWeight { fee_rate, diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 14d1681157..2c3bba728c 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -34,7 +34,6 @@ const error_epsilon: f64 = 0.1; /// Returns `true` iff each value in `left` is within `error_epsilon` of the /// corresponding value in `right`. fn is_close(left: FeeRateEstimate, right: FeeRateEstimate) -> bool { - warn!("Checking ExecutionCost's. {:?} vs {:?}", left, right); let is_ok = (left.high - right.high).abs() < error_epsilon && (left.middle - right.middle).abs() < error_epsilon && (left.low - right.low).abs() < error_epsilon; @@ -257,7 +256,6 @@ fn test_five_blocks_mostly_filled() { let mut estimator = instantiate_test_db(metric); for i in 1..6 { - warn!("i {}", i); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), @@ -293,7 +291,6 @@ fn test_ten_blocks_mostly_filled() { let mut estimator = instantiate_test_db(metric); for i in 1..11 { - warn!("i {}", i); let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), make_dummy_cc_tx(i * 10 * half_operation_cost_basis, &half_operation_cost), From a73d7b14fec0f95308c3677409128ac6ebbb62d9 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 21:59:43 +0000 Subject: [PATCH 024/103] fixed some test logic comments --- src/cost_estimates/tests/fee_medians.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 2c3bba728c..1dad28d171 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -202,6 +202,8 @@ fn test_one_block_partially_filled() { .expect("Should be able to process block receipt"); // The higher fee is 10, because that's what we paid. + // The middle fee should be near 1, because the block is mostly empty, and dominated by the + // minimum fee rate padding. // The lower fee is 1 because of the minimum fee rate padding. assert!(is_close( estimator @@ -233,6 +235,7 @@ fn test_one_block_mostly_filled() { .expect("Should be able to process block receipt"); // The higher fee is 10, because that's what we paid. + // The middle fee should be 10, because the block is mostly filled. // The lower fee is 1 because of the minimum fee rate padding. assert!(is_close( estimator @@ -267,8 +270,7 @@ fn test_five_blocks_mostly_filled() { .expect("Should be able to process block receipt"); } - // The higher fee is 10, because of the contract. - // The lower fee is 1 because of the minimum fee rate padding. + // The fee should be 30, because it's the median of [10, 20, .., 50]. assert!(is_close( estimator .get_rate_estimates() @@ -302,8 +304,7 @@ fn test_ten_blocks_mostly_filled() { .expect("Should be able to process block receipt"); } - // The higher fee is 10, because of the contract. - // The lower fee is 1 because of the minimum fee rate padding. + // The fee should be 80, because we forgot the first five estimates. assert!(is_close( estimator .get_rate_estimates() From 0c2dd1d5ea1fa5130bca680b3814d4f62bfc4695 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 8 Dec 2021 23:13:50 +0000 Subject: [PATCH 025/103] added basic structure to fuzzer --- src/cost_estimates/fee_rate_fuzzer.rs | 57 +++++++++++++++++++++++++++ src/cost_estimates/mod.rs | 1 + 2 files changed, 58 insertions(+) create mode 100644 src/cost_estimates/fee_rate_fuzzer.rs diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs new file mode 100644 index 0000000000..ecb6c8e2cb --- /dev/null +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -0,0 +1,57 @@ +use std::cmp; +use std::convert::TryFrom; +use std::{iter::FromIterator, path::Path}; + +use rusqlite::Transaction as SqlTransaction; +use rusqlite::{ + types::{FromSql, FromSqlError}, + Connection, Error as SqliteError, OptionalExtension, ToSql, +}; +use serde_json::Value as JsonValue; + +use chainstate::stacks::TransactionPayload; +use util::db::sqlite_open; +use util::db::tx_begin_immediate_sqlite; +use util::db::u64_to_sql; + +use vm::costs::ExecutionCost; + +use chainstate::stacks::db::StacksEpochReceipt; +use chainstate::stacks::events::TransactionOrigin; + +use crate::util::db::sql_pragma; +use crate::util::db::table_exists; + +use super::metrics::CostMetric; +use super::FeeRateEstimate; +use super::{EstimatorError, FeeEstimator}; + +pub struct FeeRateFuzzer { + underlying: FeeEstimator, +} + +fn fuzz_esimate(input:&FeeRateEstimate) -> FeeRateEstimate { + input.clone() +} + +impl FeeEstimator for FeeRateFuzzer { + /// Just passes the information straight to `underlying`. + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + self.underlying.notify_block(receipt, block_limit) + } + + fn get_rate_estimates(&self) -> Result { + match self.underlying.get_rate_estimates() { + Ok(underlying_estimate) => { + Ok(fuzz_esimate(&underlying_estimate)) + } + Err(e) => { + Err(e) + } + } + } +} diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index fd481de13c..a92181d3d8 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -14,6 +14,7 @@ use burnchains::Txid; use chainstate::stacks::db::StacksEpochReceipt; pub mod fee_scalar; +pub mod fee_rate_fuzzer; pub mod metrics; pub mod pessimistic; From b29221fadbf2080c025acffc9f47ec21030dd59e Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 02:36:57 +0000 Subject: [PATCH 026/103] test compiles --- src/cost_estimates/fee_rate_fuzzer.rs | 18 ++++--- src/cost_estimates/mod.rs | 2 +- src/cost_estimates/tests/fee_rate_fuzzer.rs | 57 +++++++++++++++++++++ src/cost_estimates/tests/mod.rs | 1 + 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/cost_estimates/tests/fee_rate_fuzzer.rs diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index ecb6c8e2cb..c28412b2ef 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -27,13 +27,19 @@ use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; pub struct FeeRateFuzzer { - underlying: FeeEstimator, + underlying: Box, } -fn fuzz_esimate(input:&FeeRateEstimate) -> FeeRateEstimate { +fn fuzz_esimate(input: &FeeRateEstimate) -> FeeRateEstimate { input.clone() } +impl FeeRateFuzzer { + fn new(underlying: Box) -> FeeRateFuzzer { + Self { underlying } + } +} + impl FeeEstimator for FeeRateFuzzer { /// Just passes the information straight to `underlying`. fn notify_block( @@ -46,12 +52,8 @@ impl FeeEstimator for FeeRateFuzzer { fn get_rate_estimates(&self) -> Result { match self.underlying.get_rate_estimates() { - Ok(underlying_estimate) => { - Ok(fuzz_esimate(&underlying_estimate)) - } - Err(e) => { - Err(e) - } + Ok(underlying_estimate) => Ok(fuzz_esimate(&underlying_estimate)), + Err(e) => Err(e), } } } diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index a92181d3d8..ed0824a575 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -13,8 +13,8 @@ use vm::costs::ExecutionCost; use burnchains::Txid; use chainstate::stacks::db::StacksEpochReceipt; -pub mod fee_scalar; pub mod fee_rate_fuzzer; +pub mod fee_scalar; pub mod metrics; pub mod pessimistic; diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs new file mode 100644 index 0000000000..d3d7fbfe15 --- /dev/null +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -0,0 +1,57 @@ +use std::{env, path::PathBuf}; +use time::Instant; + +use rand::seq::SliceRandom; +use rand::Rng; + +use cost_estimates::metrics::CostMetric; +use cost_estimates::{EstimatorError, FeeEstimator}; +use vm::costs::ExecutionCost; + +use chainstate::burn::ConsensusHash; +use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; +use chainstate::stacks::events::StacksTransactionReceipt; +use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; +use types::proof::TrieHash; +use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; +use util::vrf::VRFProof; + +use crate::chainstate::stacks::{ + CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionSpendingCondition, TransactionVersion, +}; +use crate::core::StacksEpochId; +use crate::cost_estimates::fee_scalar::ScalarFeeRateEstimator; +use crate::cost_estimates::FeeRateEstimate; +use crate::types::chainstate::StacksAddress; +use crate::vm::types::{PrincipalData, StandardPrincipalData}; +use crate::vm::Value; + +struct MockFeeEstimator { + pub receipts: Vec, +} + +/// 1) on `notify_block` Inputs are recorded, and not passed anywhere. +/// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. +impl FeeEstimator for MockFeeEstimator { + /// Just passes the information straight to `underlying`. + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + self.receipts.push(receipt.clone()); + Ok(()) + } + + fn get_rate_estimates(&self) -> Result { + Ok(FeeRateEstimate { + high: 95f64, + middle: 50f64, + low: 5f64, + }) + } +} + +#[test] +fn test_empty_fee_estimator() {} diff --git a/src/cost_estimates/tests/mod.rs b/src/cost_estimates/tests/mod.rs index 28ade005c5..4a22fdc3e4 100644 --- a/src/cost_estimates/tests/mod.rs +++ b/src/cost_estimates/tests/mod.rs @@ -1,6 +1,7 @@ use cost_estimates::FeeRateEstimate; pub mod cost_estimators; +pub mod fee_rate_fuzzer; pub mod fee_scalar; pub mod metrics; From 89d12554b23f9708c9bfec44b7ee4e5815afef94 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 02:48:41 +0000 Subject: [PATCH 027/103] fee estimator compils but needs random seed --- src/cost_estimates/fee_rate_fuzzer.rs | 2 +- src/cost_estimates/tests/fee_rate_fuzzer.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index c28412b2ef..066d4af81d 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -35,7 +35,7 @@ fn fuzz_esimate(input: &FeeRateEstimate) -> FeeRateEstimate { } impl FeeRateFuzzer { - fn new(underlying: Box) -> FeeRateFuzzer { + pub fn new(underlying: Box) -> FeeRateFuzzer { Self { underlying } } } diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index d3d7fbfe15..61fa75548d 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -26,6 +26,7 @@ use crate::cost_estimates::FeeRateEstimate; use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; +use cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; struct MockFeeEstimator { pub receipts: Vec, @@ -54,4 +55,9 @@ impl FeeEstimator for MockFeeEstimator { } #[test] -fn test_empty_fee_estimator() {} +fn test_empty_fee_estimator() { + let mock_estimator = MockFeeEstimator { + receipts: vec![], + }; + let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator)); +} From a0d894f877c9a62d052adbdec93cc06e0b8f8856 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 05:01:33 +0000 Subject: [PATCH 028/103] fuzzer takes an optional seed input --- src/cost_estimates/fee_rate_fuzzer.rs | 19 +++++++++++++++++-- src/cost_estimates/tests/fee_rate_fuzzer.rs | 6 ++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 066d4af81d..c428920e93 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -26,8 +26,14 @@ use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; +use rand::rngs::StdRng; +use rand::thread_rng; +use rand::RngCore; +use rand::SeedableRng; + pub struct FeeRateFuzzer { underlying: Box, + rng: Box, } fn fuzz_esimate(input: &FeeRateEstimate) -> FeeRateEstimate { @@ -35,8 +41,17 @@ fn fuzz_esimate(input: &FeeRateEstimate) -> FeeRateEstimate { } impl FeeRateFuzzer { - pub fn new(underlying: Box) -> FeeRateFuzzer { - Self { underlying } + /// To get strong random numbers, pass in None for `seed`. + /// To get predictable random numbers for test, pass in a non-empty numeric `seed`. + pub fn new(underlying: Box, seed: Option<[u8; 32]>) -> FeeRateFuzzer { + let rng: Box = match seed { + Some(seed) => { + let rng: StdRng = SeedableRng::from_seed(seed); + Box::new(rng) + } + None => Box::new(thread_rng()), + }; + Self { underlying, rng } } } diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 61fa75548d..5ab5303d43 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -56,8 +56,6 @@ impl FeeEstimator for MockFeeEstimator { #[test] fn test_empty_fee_estimator() { - let mock_estimator = MockFeeEstimator { - receipts: vec![], - }; - let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator)); + let mock_estimator = MockFeeEstimator { receipts: vec![] }; + let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator), Some([0u8; 32])); } From e6494632b7ac4ee979f31eefb78ea235f996b567 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 11:54:51 +0000 Subject: [PATCH 029/103] creator function is working --- src/cost_estimates/fee_rate_fuzzer.rs | 82 +++++++++++++-------- src/cost_estimates/tests/fee_rate_fuzzer.rs | 62 ++++++++-------- 2 files changed, 81 insertions(+), 63 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index c428920e93..057d5f317d 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -32,43 +32,61 @@ use rand::RngCore; use rand::SeedableRng; pub struct FeeRateFuzzer { + /// We will apply a random "fuzz" on top of the estimates given by this. underlying: Box, - rng: Box, -} - -fn fuzz_esimate(input: &FeeRateEstimate) -> FeeRateEstimate { - input.clone() + /// Creator function for a new random generator. For prod, use `thread_rng`. For test, + /// pass in a contrived generator. + rng_creator: Box Box>, } impl FeeRateFuzzer { - /// To get strong random numbers, pass in None for `seed`. - /// To get predictable random numbers for test, pass in a non-empty numeric `seed`. - pub fn new(underlying: Box, seed: Option<[u8; 32]>) -> FeeRateFuzzer { - let rng: Box = match seed { - Some(seed) => { - let rng: StdRng = SeedableRng::from_seed(seed); - Box::new(rng) - } - None => Box::new(thread_rng()), - }; - Self { underlying, rng } + pub fn new(underlying: Box) -> Box { + let rng_creator = Box::new(|| { + let r: Box = Box::new(thread_rng()); + r + // thread_rng() + }); + Box::new(Self { + underlying, + rng_creator, + }) } -} - -impl FeeEstimator for FeeRateFuzzer { - /// Just passes the information straight to `underlying`. - fn notify_block( - &mut self, - receipt: &StacksEpochReceipt, - block_limit: &ExecutionCost, - ) -> Result<(), EstimatorError> { - self.underlying.notify_block(receipt, block_limit) + pub fn new_custom_creator( + underlying: Box, + rng_creator: Box Box>, + ) -> Box { + Box::new(Self { + underlying, + rng_creator, + }) } - fn get_rate_estimates(&self) -> Result { - match self.underlying.get_rate_estimates() { - Ok(underlying_estimate) => Ok(fuzz_esimate(&underlying_estimate)), - Err(e) => Err(e), - } - } + //fn fuzz_esimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { + // let _rng: Box = match self.seed { + // Some(seed) => { + // let rng: StdRng = SeedableRng::from_seed(seed); + // Box::new(rng) + // } + // None => Box::new(thread_rng()), + // }; + // input.clone() + //} } +// +//impl FeeEstimator for FeeRateFuzzer { +// /// Just passes the information straight to `underlying`. +// fn notify_block( +// &mut self, +// receipt: &StacksEpochReceipt, +// block_limit: &ExecutionCost, +// ) -> Result<(), EstimatorError> { +// self.underlying.notify_block(receipt, block_limit) +// } +// +// fn get_rate_estimates(&self) -> Result { +// match self.underlying.get_rate_estimates() { +// Ok(underlying_estimate) => Ok(fuzz_esimate(&underlying_estimate)), +// Err(e) => Err(e), +// } +// } +//} diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 5ab5303d43..b9e1bd5b1b 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -28,34 +28,34 @@ use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; use cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; -struct MockFeeEstimator { - pub receipts: Vec, -} - -/// 1) on `notify_block` Inputs are recorded, and not passed anywhere. -/// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. -impl FeeEstimator for MockFeeEstimator { - /// Just passes the information straight to `underlying`. - fn notify_block( - &mut self, - receipt: &StacksEpochReceipt, - block_limit: &ExecutionCost, - ) -> Result<(), EstimatorError> { - self.receipts.push(receipt.clone()); - Ok(()) - } - - fn get_rate_estimates(&self) -> Result { - Ok(FeeRateEstimate { - high: 95f64, - middle: 50f64, - low: 5f64, - }) - } -} - -#[test] -fn test_empty_fee_estimator() { - let mock_estimator = MockFeeEstimator { receipts: vec![] }; - let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator), Some([0u8; 32])); -} +//struct MockFeeEstimator { +// pub receipts: Vec, +//} +// +///// 1) on `notify_block` Inputs are recorded, and not passed anywhere. +///// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. +//impl FeeEstimator for MockFeeEstimator { +// /// Just passes the information straight to `underlying`. +// fn notify_block( +// &mut self, +// receipt: &StacksEpochReceipt, +// block_limit: &ExecutionCost, +// ) -> Result<(), EstimatorError> { +// self.receipts.push(receipt.clone()); +// Ok(()) +// } +// +// fn get_rate_estimates(&self) -> Result { +// Ok(FeeRateEstimate { +// high: 95f64, +// middle: 50f64, +// low: 5f64, +// }) +// } +//} +// +//#[test] +//fn test_empty_fee_estimator() { +// let mock_estimator = MockFeeEstimator { receipts: vec![] }; +// let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator), Some([0u8; 32])); +//} From 65eab2bb523d9b69573927adeae3db9bad0cbed1 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 12:41:51 +0000 Subject: [PATCH 030/103] test is almost ready to work --- src/cost_estimates/fee_rate_fuzzer.rs | 72 +++++++++++--------- src/cost_estimates/tests/fee_rate_fuzzer.rs | 73 ++++++++++++--------- 2 files changed, 84 insertions(+), 61 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 057d5f317d..f25c1dc77b 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -26,67 +26,79 @@ use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; +use rand::distributions::{Distribution, Uniform}; use rand::rngs::StdRng; use rand::thread_rng; use rand::RngCore; use rand::SeedableRng; +/// The FeeRateFuzzer wraps an underlying FeeEstimator. It passes `notify_block` calls to the +/// underlying estimator. On `get_rate_estimates` calls, it adds a random fuzz to the result coming +/// back from the underlying estimator. pub struct FeeRateFuzzer { /// We will apply a random "fuzz" on top of the estimates given by this. underlying: Box, /// Creator function for a new random generator. For prod, use `thread_rng`. For test, /// pass in a contrived generator. rng_creator: Box Box>, + /// The bound used for the uniform random fuzz. + uniform_bound: f64, } impl FeeRateFuzzer { - pub fn new(underlying: Box) -> Box { + pub fn new(underlying: Box, uniform_bound: f64) -> Box { let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r - // thread_rng() }); Box::new(Self { underlying, rng_creator, + uniform_bound, }) } pub fn new_custom_creator( underlying: Box, rng_creator: Box Box>, + uniform_bound: f64, ) -> Box { Box::new(Self { underlying, rng_creator, + uniform_bound, }) } - //fn fuzz_esimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { - // let _rng: Box = match self.seed { - // Some(seed) => { - // let rng: StdRng = SeedableRng::from_seed(seed); - // Box::new(rng) - // } - // None => Box::new(thread_rng()), - // }; - // input.clone() - //} + /// Add a uniform fuzz to input. + /// + /// Note: We use "uniform" instead of "normal" distribution to avoid importing a new crate + /// just for this. + fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { + let mut rng: Box = (self.rng_creator)(); + let normal = Uniform::new(-self.uniform_bound, self.uniform_bound); + FeeRateEstimate { + high: input.high + normal.sample(&mut rng), + middle: input.middle + normal.sample(&mut rng), + low: input.low + normal.sample(&mut rng), + } + } +} + +impl FeeEstimator for FeeRateFuzzer { + /// Just passes the information straight to `underlying`. + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + self.underlying.notify_block(receipt, block_limit) + } + + /// Call underlying estimator and add some fuzz. + fn get_rate_estimates(&self) -> Result { + match self.underlying.get_rate_estimates() { + Ok(underlying_estimate) => Ok(self.fuzz_estimate(&underlying_estimate)), + Err(e) => Err(e), + } + } } -// -//impl FeeEstimator for FeeRateFuzzer { -// /// Just passes the information straight to `underlying`. -// fn notify_block( -// &mut self, -// receipt: &StacksEpochReceipt, -// block_limit: &ExecutionCost, -// ) -> Result<(), EstimatorError> { -// self.underlying.notify_block(receipt, block_limit) -// } -// -// fn get_rate_estimates(&self) -> Result { -// match self.underlying.get_rate_estimates() { -// Ok(underlying_estimate) => Ok(fuzz_esimate(&underlying_estimate)), -// Err(e) => Err(e), -// } -// } -//} diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index b9e1bd5b1b..a0f73d5c48 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -27,35 +27,46 @@ use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; use cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; +use rand::rngs::StdRng; +use rand::thread_rng; +use rand::RngCore; +use rand::SeedableRng; -//struct MockFeeEstimator { -// pub receipts: Vec, -//} -// -///// 1) on `notify_block` Inputs are recorded, and not passed anywhere. -///// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. -//impl FeeEstimator for MockFeeEstimator { -// /// Just passes the information straight to `underlying`. -// fn notify_block( -// &mut self, -// receipt: &StacksEpochReceipt, -// block_limit: &ExecutionCost, -// ) -> Result<(), EstimatorError> { -// self.receipts.push(receipt.clone()); -// Ok(()) -// } -// -// fn get_rate_estimates(&self) -> Result { -// Ok(FeeRateEstimate { -// high: 95f64, -// middle: 50f64, -// low: 5f64, -// }) -// } -//} -// -//#[test] -//fn test_empty_fee_estimator() { -// let mock_estimator = MockFeeEstimator { receipts: vec![] }; -// let _fuzzed_estimator = FeeRateFuzzer::new(Box::new(mock_estimator), Some([0u8; 32])); -//} +struct MockFeeEstimator { + pub receipts: Vec, +} + +/// 1) on `notify_block` Inputs are recorded, and not passed anywhere. +/// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. +impl FeeEstimator for MockFeeEstimator { + /// Just passes the information straight to `underlying`. + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + self.receipts.push(receipt.clone()); + Ok(()) + } + + fn get_rate_estimates(&self) -> Result { + Ok(FeeRateEstimate { + high: 95f64, + middle: 50f64, + low: 5f64, + }) + } +} + +#[test] +fn test_empty_fee_estimator() { + let mock_estimator = MockFeeEstimator { receipts: vec![] }; + let rng_creator = Box::new(|| { + let seed = [0u8; 32]; + let rng: StdRng = SeedableRng::from_seed(seed); + let r: Box = Box::new(rng); + r + }); + let _fuzzed_estimator = + FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); +} From 0d8b141ed8f4e54f8b684afff780c48a632d2669 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 12:56:40 +0000 Subject: [PATCH 031/103] fuzzing tests are working --- src/cost_estimates/tests/fee_rate_fuzzer.rs | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index a0f73d5c48..08b800ecc5 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -58,8 +58,9 @@ impl FeeEstimator for MockFeeEstimator { } } +/// Test the fuzzer using a fixed random seed. #[test] -fn test_empty_fee_estimator() { +fn test_fuzzing_seed1() { let mock_estimator = MockFeeEstimator { receipts: vec![] }; let rng_creator = Box::new(|| { let seed = [0u8; 32]; @@ -67,6 +68,42 @@ fn test_empty_fee_estimator() { let r: Box = Box::new(rng); r }); - let _fuzzed_estimator = + let fuzzed_estimator = FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + + assert_eq!( + fuzzed_estimator + .get_rate_estimates() + .expect("Estimate should exist."), + FeeRateEstimate { + high: 95.01268903765265f64, + middle: 49.93182838353776f64, + low: 4.921037454936614f64 + } + ); +} + +/// Test the fuzzer using a fixed random seed. Uses a different seed than test_fuzzing_seed1. +#[test] +fn test_fuzzing_seed2() { + let mock_estimator = MockFeeEstimator { receipts: vec![] }; + let rng_creator = Box::new(|| { + let seed = [1u8; 32]; + let rng: StdRng = SeedableRng::from_seed(seed); + let r: Box = Box::new(rng); + r + }); + let fuzzed_estimator = + FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + + assert_eq!( + fuzzed_estimator + .get_rate_estimates() + .expect("Estimate should exist."), + FeeRateEstimate { + high: 95.05348553928201f64, + middle: 50.031434211372954f64, + low: 5.043648532116769f64 + } + ); } From 669fe570b68d44f859ea39426eeb4767c7213f46 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 13:25:58 +0000 Subject: [PATCH 032/103] added a notify test --- src/cost_estimates/tests/fee_rate_fuzzer.rs | 110 ++++++++++++++++++-- 1 file changed, 100 insertions(+), 10 deletions(-) diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 08b800ecc5..73a65766ed 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -32,20 +32,15 @@ use rand::thread_rng; use rand::RngCore; use rand::SeedableRng; -struct MockFeeEstimator { - pub receipts: Vec, -} +struct ConstantFeeEstimator {} -/// 1) on `notify_block` Inputs are recorded, and not passed anywhere. -/// 2) on `get_rate_estimates`, a constant `FeeRateEstimate` is returned. -impl FeeEstimator for MockFeeEstimator { - /// Just passes the information straight to `underlying`. +/// Returns a constant fee rate estimate. +impl FeeEstimator for ConstantFeeEstimator { fn notify_block( &mut self, receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - self.receipts.push(receipt.clone()); Ok(()) } @@ -61,7 +56,7 @@ impl FeeEstimator for MockFeeEstimator { /// Test the fuzzer using a fixed random seed. #[test] fn test_fuzzing_seed1() { - let mock_estimator = MockFeeEstimator { receipts: vec![] }; + let mock_estimator = ConstantFeeEstimator {}; let rng_creator = Box::new(|| { let seed = [0u8; 32]; let rng: StdRng = SeedableRng::from_seed(seed); @@ -86,7 +81,7 @@ fn test_fuzzing_seed1() { /// Test the fuzzer using a fixed random seed. Uses a different seed than test_fuzzing_seed1. #[test] fn test_fuzzing_seed2() { - let mock_estimator = MockFeeEstimator { receipts: vec![] }; + let mock_estimator = ConstantFeeEstimator {}; let rng_creator = Box::new(|| { let seed = [1u8; 32]; let rng: StdRng = SeedableRng::from_seed(seed); @@ -107,3 +102,98 @@ fn test_fuzzing_seed2() { } ); } + +fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { + StacksEpochReceipt { + header: StacksHeaderInfo { + anchored_header: StacksBlockHeader { + version: 1, + total_work: StacksWorkScore { burn: 1, work: 1 }, + proof: VRFProof::empty(), + parent_block: BlockHeaderHash([0; 32]), + parent_microblock: BlockHeaderHash([0; 32]), + parent_microblock_sequence: 0, + tx_merkle_root: Sha512Trunc256Sum([0; 32]), + state_index_root: TrieHash([0; 32]), + microblock_pubkey_hash: Hash160([0; 20]), + }, + microblock_tail: None, + block_height: 1, + index_root: TrieHash([0; 32]), + consensus_hash: ConsensusHash([2; 20]), + burn_header_hash: BurnchainHeaderHash([1; 32]), + burn_header_height: 2, + burn_header_timestamp: 2, + anchored_block_size: 1, + }, + tx_receipts, + matured_rewards: vec![], + matured_rewards_info: None, + parent_microblocks_cost: ExecutionCost::zero(), + anchored_block_cost: ExecutionCost::zero(), + parent_burn_block_hash: BurnchainHeaderHash([0; 32]), + parent_burn_block_height: 1, + parent_burn_block_timestamp: 1, + evaluated_epoch: StacksEpochId::Epoch20, + } +} + +struct CountingFeeEstimator { + counter: u64, +} + +/// This class "counts" the number of times `notify_block` has been called, and returns this as the +/// estimate. +impl FeeEstimator for CountingFeeEstimator { + fn notify_block( + &mut self, + receipt: &StacksEpochReceipt, + block_limit: &ExecutionCost, + ) -> Result<(), EstimatorError> { + self.counter += 1; + Ok(()) + } + + fn get_rate_estimates(&self) -> Result { + Ok(FeeRateEstimate { + high: self.counter as f64, + middle: self.counter as f64, + low: self.counter as f64, + }) + } +} + +/// Tests that the receipt is passed through in `notify_block`. +#[test] +fn test_notify_pass_through() { + let mock_estimator = CountingFeeEstimator { counter: 0 }; + let rng_creator = Box::new(|| { + let seed = [1u8; 32]; + let rng: StdRng = SeedableRng::from_seed(seed); + let r: Box = Box::new(rng); + r + }); + let mut fuzzed_estimator = + FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + + let receipt = make_block_receipt(vec![]); + fuzzed_estimator + .notify_block(&receipt, &ExecutionCost::max_value()) + .expect("notify_block should succeed here."); + fuzzed_estimator + .notify_block(&receipt, &ExecutionCost::max_value()) + .expect("notify_block should succeed here."); + + // We've called `notify_block` twice, so the values returned are 2f, with some noise from the + // fuzzer. + assert_eq!( + fuzzed_estimator + .get_rate_estimates() + .expect("Estimate should exist."), + FeeRateEstimate { + high: 2.053485539282013f64, + middle: 2.0314342113729524f64, + low: 2.0436485321167686f64 + }, + ); +} From 7bf321014aa0cb2666042b46359381908375360b Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 13:33:49 +0000 Subject: [PATCH 033/103] removed some unused imports --- src/cost_estimates/fee_rate_fuzzer.rs | 23 --------------------- src/cost_estimates/tests/fee_rate_fuzzer.rs | 12 +---------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index f25c1dc77b..471155ebc4 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -1,31 +1,8 @@ -use std::cmp; -use std::convert::TryFrom; -use std::{iter::FromIterator, path::Path}; - -use rusqlite::Transaction as SqlTransaction; -use rusqlite::{ - types::{FromSql, FromSqlError}, - Connection, Error as SqliteError, OptionalExtension, ToSql, -}; -use serde_json::Value as JsonValue; - -use chainstate::stacks::TransactionPayload; -use util::db::sqlite_open; -use util::db::tx_begin_immediate_sqlite; -use util::db::u64_to_sql; - use vm::costs::ExecutionCost; use chainstate::stacks::db::StacksEpochReceipt; -use chainstate::stacks::events::TransactionOrigin; - -use crate::util::db::sql_pragma; -use crate::util::db::table_exists; - -use super::metrics::CostMetric; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; - use rand::distributions::{Distribution, Uniform}; use rand::rngs::StdRng; use rand::thread_rng; diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 73a65766ed..fb097cc8dd 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -1,31 +1,21 @@ -use std::{env, path::PathBuf}; -use time::Instant; - -use rand::seq::SliceRandom; -use rand::Rng; - use cost_estimates::metrics::CostMetric; use cost_estimates::{EstimatorError, FeeEstimator}; use vm::costs::ExecutionCost; -use chainstate::burn::ConsensusHash; use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; use chainstate::stacks::events::StacksTransactionReceipt; use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; use types::proof::TrieHash; use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; use util::vrf::VRFProof; +use chainstate::burn::ConsensusHash; use crate::chainstate::stacks::{ CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, TransactionContractCall, TransactionPayload, TransactionSpendingCondition, TransactionVersion, }; use crate::core::StacksEpochId; -use crate::cost_estimates::fee_scalar::ScalarFeeRateEstimator; use crate::cost_estimates::FeeRateEstimate; -use crate::types::chainstate::StacksAddress; -use crate::vm::types::{PrincipalData, StandardPrincipalData}; -use crate::vm::Value; use cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; use rand::rngs::StdRng; use rand::thread_rng; From 61c2429708ac183f598aaf1325980422f72546f4 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 13:34:57 +0000 Subject: [PATCH 034/103] fixed uniform noise comment --- src/cost_estimates/fee_rate_fuzzer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 471155ebc4..bf68d7c1de 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -12,6 +12,9 @@ use rand::SeedableRng; /// The FeeRateFuzzer wraps an underlying FeeEstimator. It passes `notify_block` calls to the /// underlying estimator. On `get_rate_estimates` calls, it adds a random fuzz to the result coming /// back from the underlying estimator. +/// +/// Note: We currently use "uniform" random noise instead of "normal" distributed noise to avoid +/// importing a new crate just for this. pub struct FeeRateFuzzer { /// We will apply a random "fuzz" on top of the estimates given by this. underlying: Box, @@ -47,9 +50,6 @@ impl FeeRateFuzzer { } /// Add a uniform fuzz to input. - /// - /// Note: We use "uniform" instead of "normal" distribution to avoid importing a new crate - /// just for this. fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { let mut rng: Box = (self.rng_creator)(); let normal = Uniform::new(-self.uniform_bound, self.uniform_bound); From f676a1530bb5685730fc8742d09d0a1b7df8fd72 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 13:52:10 +0000 Subject: [PATCH 035/103] refactored the test to avoid duplicate code --- src/cost_estimates/tests/common.rs | 50 +++++++++++++++++++++ src/cost_estimates/tests/fee_rate_fuzzer.rs | 37 +-------------- src/cost_estimates/tests/fee_scalar.rs | 37 +-------------- src/cost_estimates/tests/mod.rs | 1 + 4 files changed, 55 insertions(+), 70 deletions(-) create mode 100644 src/cost_estimates/tests/common.rs diff --git a/src/cost_estimates/tests/common.rs b/src/cost_estimates/tests/common.rs new file mode 100644 index 0000000000..cb400deeec --- /dev/null +++ b/src/cost_estimates/tests/common.rs @@ -0,0 +1,50 @@ +use vm::costs::ExecutionCost; +use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; +use chainstate::stacks::events::StacksTransactionReceipt; +use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; +use types::proof::TrieHash; +use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; +use util::vrf::VRFProof; +use chainstate::burn::ConsensusHash; + +use crate::chainstate::stacks::{ + CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionSpendingCondition, TransactionVersion, +}; +use crate::core::StacksEpochId; + +/// Make a block receipt from `tx_receipts` with some dummy values filled for test. +pub fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { + StacksEpochReceipt { + header: StacksHeaderInfo { + anchored_header: StacksBlockHeader { + version: 1, + total_work: StacksWorkScore { burn: 1, work: 1 }, + proof: VRFProof::empty(), + parent_block: BlockHeaderHash([0; 32]), + parent_microblock: BlockHeaderHash([0; 32]), + parent_microblock_sequence: 0, + tx_merkle_root: Sha512Trunc256Sum([0; 32]), + state_index_root: TrieHash([0; 32]), + microblock_pubkey_hash: Hash160([0; 20]), + }, + microblock_tail: None, + block_height: 1, + index_root: TrieHash([0; 32]), + consensus_hash: ConsensusHash([2; 20]), + burn_header_hash: BurnchainHeaderHash([1; 32]), + burn_header_height: 2, + burn_header_timestamp: 2, + anchored_block_size: 1, + }, + tx_receipts, + matured_rewards: vec![], + matured_rewards_info: None, + parent_microblocks_cost: ExecutionCost::zero(), + anchored_block_cost: ExecutionCost::zero(), + parent_burn_block_hash: BurnchainHeaderHash([0; 32]), + parent_burn_block_height: 1, + parent_burn_block_timestamp: 1, + evaluated_epoch: StacksEpochId::Epoch20, + } +} diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index fb097cc8dd..92ba205d56 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -22,6 +22,8 @@ use rand::thread_rng; use rand::RngCore; use rand::SeedableRng; +use cost_estimates::tests::common::make_block_receipt; + struct ConstantFeeEstimator {} /// Returns a constant fee rate estimate. @@ -93,41 +95,6 @@ fn test_fuzzing_seed2() { ); } -fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { - StacksEpochReceipt { - header: StacksHeaderInfo { - anchored_header: StacksBlockHeader { - version: 1, - total_work: StacksWorkScore { burn: 1, work: 1 }, - proof: VRFProof::empty(), - parent_block: BlockHeaderHash([0; 32]), - parent_microblock: BlockHeaderHash([0; 32]), - parent_microblock_sequence: 0, - tx_merkle_root: Sha512Trunc256Sum([0; 32]), - state_index_root: TrieHash([0; 32]), - microblock_pubkey_hash: Hash160([0; 20]), - }, - microblock_tail: None, - block_height: 1, - index_root: TrieHash([0; 32]), - consensus_hash: ConsensusHash([2; 20]), - burn_header_hash: BurnchainHeaderHash([1; 32]), - burn_header_height: 2, - burn_header_timestamp: 2, - anchored_block_size: 1, - }, - tx_receipts, - matured_rewards: vec![], - matured_rewards_info: None, - parent_microblocks_cost: ExecutionCost::zero(), - anchored_block_cost: ExecutionCost::zero(), - parent_burn_block_hash: BurnchainHeaderHash([0; 32]), - parent_burn_block_height: 1, - parent_burn_block_timestamp: 1, - evaluated_epoch: StacksEpochId::Epoch20, - } -} - struct CountingFeeEstimator { counter: u64, } diff --git a/src/cost_estimates/tests/fee_scalar.rs b/src/cost_estimates/tests/fee_scalar.rs index f440384c76..5ad4566586 100644 --- a/src/cost_estimates/tests/fee_scalar.rs +++ b/src/cost_estimates/tests/fee_scalar.rs @@ -27,6 +27,8 @@ use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; +use cost_estimates::tests::common::make_block_receipt; + fn instantiate_test_db(m: CM) -> ScalarFeeRateEstimator { let mut path = env::temp_dir(); let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); @@ -71,41 +73,6 @@ fn test_empty_fee_estimator() { ); } -fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { - StacksEpochReceipt { - header: StacksHeaderInfo { - anchored_header: StacksBlockHeader { - version: 1, - total_work: StacksWorkScore { burn: 1, work: 1 }, - proof: VRFProof::empty(), - parent_block: BlockHeaderHash([0; 32]), - parent_microblock: BlockHeaderHash([0; 32]), - parent_microblock_sequence: 0, - tx_merkle_root: Sha512Trunc256Sum([0; 32]), - state_index_root: TrieHash([0; 32]), - microblock_pubkey_hash: Hash160([0; 20]), - }, - microblock_tail: None, - block_height: 1, - index_root: TrieHash([0; 32]), - consensus_hash: ConsensusHash([2; 20]), - burn_header_hash: BurnchainHeaderHash([1; 32]), - burn_header_height: 2, - burn_header_timestamp: 2, - anchored_block_size: 1, - }, - tx_receipts, - matured_rewards: vec![], - matured_rewards_info: None, - parent_microblocks_cost: ExecutionCost::zero(), - anchored_block_cost: ExecutionCost::zero(), - parent_burn_block_hash: BurnchainHeaderHash([0; 32]), - parent_burn_block_height: 1, - parent_burn_block_timestamp: 1, - evaluated_epoch: StacksEpochId::Epoch20, - } -} - fn make_dummy_coinbase_tx() -> StacksTransaction { StacksTransaction::new( TransactionVersion::Mainnet, diff --git a/src/cost_estimates/tests/mod.rs b/src/cost_estimates/tests/mod.rs index 4a22fdc3e4..62c6608787 100644 --- a/src/cost_estimates/tests/mod.rs +++ b/src/cost_estimates/tests/mod.rs @@ -1,5 +1,6 @@ use cost_estimates::FeeRateEstimate; +pub mod common; pub mod cost_estimators; pub mod fee_rate_fuzzer; pub mod fee_scalar; From 2566f3443043112749fc5578371f73d4a2596822 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 16:33:00 +0000 Subject: [PATCH 036/103] ran format --- src/cost_estimates/fee_rate_fuzzer.rs | 2 +- src/cost_estimates/tests/common.rs | 4 ++-- src/cost_estimates/tests/fee_rate_fuzzer.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index bf68d7c1de..8ebfcd0a6f 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -1,8 +1,8 @@ use vm::costs::ExecutionCost; -use chainstate::stacks::db::StacksEpochReceipt; use super::FeeRateEstimate; use super::{EstimatorError, FeeEstimator}; +use chainstate::stacks::db::StacksEpochReceipt; use rand::distributions::{Distribution, Uniform}; use rand::rngs::StdRng; use rand::thread_rng; diff --git a/src/cost_estimates/tests/common.rs b/src/cost_estimates/tests/common.rs index cb400deeec..63d0a02d05 100644 --- a/src/cost_estimates/tests/common.rs +++ b/src/cost_estimates/tests/common.rs @@ -1,11 +1,11 @@ -use vm::costs::ExecutionCost; +use chainstate::burn::ConsensusHash; use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; use chainstate::stacks::events::StacksTransactionReceipt; use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; use types::proof::TrieHash; use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; use util::vrf::VRFProof; -use chainstate::burn::ConsensusHash; +use vm::costs::ExecutionCost; use crate::chainstate::stacks::{ CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 92ba205d56..b285d4e9c7 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -2,13 +2,13 @@ use cost_estimates::metrics::CostMetric; use cost_estimates::{EstimatorError, FeeEstimator}; use vm::costs::ExecutionCost; +use chainstate::burn::ConsensusHash; use chainstate::stacks::db::{StacksEpochReceipt, StacksHeaderInfo}; use chainstate::stacks::events::StacksTransactionReceipt; use types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, StacksBlockHeader, StacksWorkScore}; use types::proof::TrieHash; use util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; use util::vrf::VRFProof; -use chainstate::burn::ConsensusHash; use crate::chainstate::stacks::{ CoinbasePayload, StacksTransaction, TokenTransferMemo, TransactionAuth, From ec53f5041f1d7be41a4370eb4c3d8fb76c810b59 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 16:33:32 +0000 Subject: [PATCH 037/103] fix formattin --- src/cost_estimates/fee_medians.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index a388c0f26d..7a177ed6a6 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -308,9 +308,7 @@ fn fee_rate_and_weight_from_receipt( block_limit: &ExecutionCost, ) -> Option { let (payload, fee, tx_size) = match tx_receipt.transaction { - TransactionOrigin::Stacks(ref tx) => { - Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())) - } + TransactionOrigin::Stacks(ref tx) => Some((&tx.payload, tx.get_tx_fee(), tx.tx_len())), TransactionOrigin::Burn(_) => None, }?; let scalar_cost = match payload { From 7e92f39206337e96403bb188809a3f1da2510d8b Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 9 Dec 2021 16:59:52 +0000 Subject: [PATCH 038/103] added constructor comment --- src/cost_estimates/fee_rate_fuzzer.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 8ebfcd0a6f..2699d82bd1 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -26,6 +26,8 @@ pub struct FeeRateFuzzer { } impl FeeRateFuzzer { + /// Constructor for production. It uses `thread_rng()` as the random number generator, + /// to get truly pseudo-random numbers. pub fn new(underlying: Box, uniform_bound: f64) -> Box { let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); @@ -37,6 +39,9 @@ impl FeeRateFuzzer { uniform_bound, }) } + + /// Constructor meant for test. The user can pass in a contrived random number generator + /// factory function, so that the test is repeatable. pub fn new_custom_creator( underlying: Box, rng_creator: Box Box>, From c285870e663fa24e33a1934b7fceecafe3b2f57e Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 21:04:41 +0000 Subject: [PATCH 039/103] added some tests for fee_rate_estimate_from_sorted_weighted_fees --- src/cost_estimates/fee_medians.rs | 4 +- src/cost_estimates/tests/fee_medians.rs | 69 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 7a177ed6a6..b09ebdc730 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -61,7 +61,7 @@ pub struct WeightedMedianFeeRateEstimator { /// Convenience struct for passing around this pair. #[derive(Debug)] -struct FeeRateAndWeight { +pub struct FeeRateAndWeight { pub fee_rate: f64, pub weight: u64, } @@ -235,7 +235,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { /// The percentiles computed are [0.05, 0.5, 0.95]. /// /// `sorted_fee_rates` must be non-empty. -fn fee_rate_estimate_from_sorted_weighted_fees( +pub fn fee_rate_estimate_from_sorted_weighted_fees( sorted_fee_rates: &Vec, ) -> FeeRateEstimate { if sorted_fee_rates.is_empty() { diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 1dad28d171..4585419f34 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -27,6 +27,8 @@ use crate::cost_estimates::FeeRateEstimate; use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; +use cost_estimates::fee_medians::fee_rate_estimate_from_sorted_weighted_fees; +use cost_estimates::fee_medians::FeeRateAndWeight; /// Tolerance for approximate comparison. const error_epsilon: f64 = 0.1; @@ -316,3 +318,70 @@ fn test_ten_blocks_mostly_filled() { } )); } + +//pub fn fee_rate_estimate_from_sorted_weighted_fees( +// sorted_fee_rates: &Vec, +//) -> FeeRateEstimate { + +#[test] +fn test_fee_rate_estimate_from_sorted_weighted_fees_5() { + assert_eq!( + fee_rate_estimate_from_sorted_weighted_fees(&vec![ + FeeRateAndWeight { + fee_rate: 1f64, + weight: 5u64, + }, + FeeRateAndWeight { + fee_rate: 10f64, + weight: 95u64, + }, + ]), + FeeRateEstimate { + high: 10.0f64, + middle: 9.549999999999999f64, + low: 1.45f64 + } + ); +} + +#[test] +fn test_fee_rate_estimate_from_sorted_weighted_fees_2() { + assert_eq!( + fee_rate_estimate_from_sorted_weighted_fees(&vec![ + FeeRateAndWeight { + fee_rate: 1f64, + weight: 50u64, + }, + FeeRateAndWeight { + fee_rate: 10f64, + weight: 50u64, + }, + ]), + FeeRateEstimate { + high: 10.0f64, + middle: 5.5f64, + low: 1.0f64 + } + ); +} + +#[test] +fn test_fee_rate_estimate_from_sorted_weighted_fees_3() { + assert_eq!( + fee_rate_estimate_from_sorted_weighted_fees(&vec![ + FeeRateAndWeight { + fee_rate: 1f64, + weight: 95u64, + }, + FeeRateAndWeight { + fee_rate: 10f64, + weight: 5u64, + }, + ]), + FeeRateEstimate { + high: 9.549999999999999f64, + middle: 1.4500000000000004f64, + low: 1.0f64 + } + ); +} From 6aa6b70f98914537612af9ec6d4a209870e7b907 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 22:19:25 +0000 Subject: [PATCH 040/103] new fee rate estimate cases --- src/cost_estimates/tests/fee_medians.rs | 50 +++++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 4585419f34..73add1d44f 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -319,12 +319,8 @@ fn test_ten_blocks_mostly_filled() { )); } -//pub fn fee_rate_estimate_from_sorted_weighted_fees( -// sorted_fee_rates: &Vec, -//) -> FeeRateEstimate { - #[test] -fn test_fee_rate_estimate_from_sorted_weighted_fees_5() { +fn test_fee_rate_estimate_5_vs_95() { assert_eq!( fee_rate_estimate_from_sorted_weighted_fees(&vec![ FeeRateAndWeight { @@ -345,7 +341,7 @@ fn test_fee_rate_estimate_from_sorted_weighted_fees_5() { } #[test] -fn test_fee_rate_estimate_from_sorted_weighted_fees_2() { +fn test_fee_rate_estimate_50_vs_50() { assert_eq!( fee_rate_estimate_from_sorted_weighted_fees(&vec![ FeeRateAndWeight { @@ -366,7 +362,7 @@ fn test_fee_rate_estimate_from_sorted_weighted_fees_2() { } #[test] -fn test_fee_rate_estimate_from_sorted_weighted_fees_3() { +fn test_fee_rate_estimate_95_vs_5() { assert_eq!( fee_rate_estimate_from_sorted_weighted_fees(&vec![ FeeRateAndWeight { @@ -385,3 +381,43 @@ fn test_fee_rate_estimate_from_sorted_weighted_fees_3() { } ); } + +#[test] +fn test_fee_rate_estimate_20() { + let mut pairs = vec![]; + for i in 1..21 { + pairs.push(FeeRateAndWeight { + fee_rate: 1f64 * i as f64, + weight: 1u64, + }) + } + + assert_eq!( + fee_rate_estimate_from_sorted_weighted_fees(&pairs), + FeeRateEstimate { + high: 19.5f64, + middle: 10.5f64, + low: 1.5f64 + } + ); +} + +#[test] +fn test_fee_rate_estimate_100() { + let mut pairs = vec![]; + for i in 1..101 { + pairs.push(FeeRateAndWeight { + fee_rate: 1f64 * i as f64, + weight: 1u64, + }) + } + + assert_eq!( + fee_rate_estimate_from_sorted_weighted_fees(&pairs), + FeeRateEstimate { + high: 95.5f64, + middle: 50.5f64, + low: 5.5f64 + } + ); +} From 0004684e9d291caa5a9060359d82a4520f4edec7 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 22:42:25 +0000 Subject: [PATCH 041/103] use checked add for weight normalization --- src/cost_estimates/fee_medians.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index b09ebdc730..6b976e3e53 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -289,7 +289,10 @@ pub fn fee_rate_estimate_from_sorted_weighted_fees( fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_block_weight: u64) { let mut total_weight = 0u64; for rate_and_weight in working_rates.into_iter() { - total_weight += rate_and_weight.weight; + total_weight = match total_weight.checked_add(rate_and_weight.weight) { + Some(result) => result, + None => return, + }; } if total_weight < full_block_weight { From a18b6576c7800bb3f3d5d7d8a6cb5323a4649adc Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 22:52:26 +0000 Subject: [PATCH 042/103] refactor ScalarFeeRateEstimator::open see if it breaks anything --- src/cost_estimates/fee_scalar.rs | 33 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index e0414aff03..3b46521cba 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -52,27 +52,18 @@ pub struct ScalarFeeRateEstimator { impl ScalarFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M) -> Result { - let db = - sqlite_open(p, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, false).or_else(|e| { - if let SqliteError::SqliteFailure(ref internal, _) = e { - if let rusqlite::ErrorCode::CannotOpen = internal.code { - let mut db = sqlite_open( - p, - rusqlite::OpenFlags::SQLITE_OPEN_CREATE - | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, - false, - )?; - let tx = tx_begin_immediate_sqlite(&mut db)?; - Self::instantiate_db(&tx)?; - tx.commit()?; - Ok(db) - } else { - Err(e) - } - } else { - Err(e) - } - })?; + let mut db = sqlite_open( + p, + rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, + false, + )?; + + // check if the db needs to be instantiated regardless of whether or not + // it was newly created: the db itself may be shared with other fee estimators, + // which would not have created the necessary table for this estimator. + let tx = tx_begin_immediate_sqlite(&mut db)?; + Self::instantiate_db(&tx)?; + tx.commit()?; Ok(Self { db, From 6050462de867d5176e4b6ccba910c84921ade6f0 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 23:16:27 +0000 Subject: [PATCH 043/103] use floats to avoid overflow --- src/cost_estimates/fee_medians.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 6b976e3e53..8de1570a92 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -242,14 +242,14 @@ pub fn fee_rate_estimate_from_sorted_weighted_fees( panic!("`sorted_fee_rates` cannot be empty."); } - let mut total_weight = 0u64; + let mut total_weight = 0f64; for rate_and_weight in sorted_fee_rates { - total_weight += rate_and_weight.weight; + total_weight += rate_and_weight.weight as f64; } - let mut cumulative_weight = 0u64; + let mut cumulative_weight = 0f64; let mut percentiles = Vec::new(); for rate_and_weight in sorted_fee_rates { - cumulative_weight += rate_and_weight.weight; + cumulative_weight += rate_and_weight.weight as f64; let percentile_n: f64 = (cumulative_weight as f64 - rate_and_weight.weight as f64 / 2f64) / total_weight as f64; percentiles.push(percentile_n); From f70ed276d799125b391cab653c621846b7702940 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 23:18:45 +0000 Subject: [PATCH 044/103] add assert --- src/cost_estimates/fee_medians.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 8de1570a92..579007a081 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -254,6 +254,7 @@ pub fn fee_rate_estimate_from_sorted_weighted_fees( (cumulative_weight as f64 - rate_and_weight.weight as f64 / 2f64) / total_weight as f64; percentiles.push(percentile_n); } + assert_eq!(percentiles.len(), sorted_fee_rates.len()); let target_percentiles = vec![0.05, 0.5, 0.95]; let mut fees_index = 0; // index into `sorted_fee_rates` From a539cdc825f077887d8887941748385ae978e01f Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 13 Dec 2021 23:19:53 +0000 Subject: [PATCH 045/103] ichanged block limits to see what fails --- src/cost_estimates/fee_medians.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 579007a081..9d93a9eb53 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -53,7 +53,7 @@ pub struct WeightedMedianFeeRateEstimator { /// We only look back `window_size` fee rates when averaging past estimates. window_size: u32, /// The weight of a "full block" in abstract scalar cost units. This is the weight of - /// a block that is filled on each dimension. + /// a block that is filled *one single* dimension. full_block_weight: u64, /// Use this cost metric in fee rate calculations. metric: M, @@ -86,7 +86,7 @@ impl WeightedMedianFeeRateEstimator { db, metric, window_size, - full_block_weight: 6 * PROPORTION_RESOLUTION, + full_block_weight: PROPORTION_RESOLUTION, }) } From 84ca56423ca5ff9d46672c6d07e8029246052528 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 01:01:49 +0000 Subject: [PATCH 046/103] changed test values that were failing after chaning `full_block_weight` --- src/cost_estimates/tests/fee_medians.rs | 38 +++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 73add1d44f..06ce24639a 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -128,26 +128,26 @@ const block_limit: ExecutionCost = ExecutionCost { }; const tenth_operation_cost: ExecutionCost = ExecutionCost { - write_length: 10, - write_count: 10, - read_length: 10, - read_count: 10, + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, runtime: 10, }; const half_operation_cost: ExecutionCost = ExecutionCost { - write_length: 50, - write_count: 50, - read_length: 50, - read_count: 50, + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, runtime: 50, }; // The scalar cost of `make_dummy_cc_tx(_, &tenth_operation_cost)`. -const tenth_operation_cost_basis: u64 = 5160; +const tenth_operation_cost_basis: u64 = 1164; // The scalar cost of `make_dummy_cc_tx(_, &half_operation_cost)`. -const half_operation_cost_basis: u64 = 25160; +const half_operation_cost_basis: u64 = 5164; /// Tests that we have no estimate available until we `notify`. #[test] @@ -203,7 +203,7 @@ fn test_one_block_partially_filled() { .notify_block(&single_tx_receipt, &block_limit) .expect("Should be able to process block receipt"); - // The higher fee is 10, because that's what we paid. + // The higher fee is 10, because of the operation paying 10f per cost. // The middle fee should be near 1, because the block is mostly empty, and dominated by the // minimum fee rate padding. // The lower fee is 1 because of the minimum fee rate padding. @@ -212,8 +212,8 @@ fn test_one_block_partially_filled() { .get_rate_estimates() .expect("Should be able to create estimate now"), FeeRateEstimate { - high: 9.87f64, - middle: 1.77f64, + high: 10.0f64, + middle: 2.0475999999999996f64, low: 1f64 } )); @@ -229,7 +229,9 @@ fn test_one_block_mostly_filled() { let single_tx_receipt = make_block_receipt(vec![ StacksTransactionReceipt::from_coinbase(make_dummy_coinbase_tx()), make_dummy_cc_tx(10 * half_operation_cost_basis, &half_operation_cost), - make_dummy_cc_tx(10 * half_operation_cost_basis, &half_operation_cost), + make_dummy_cc_tx(10 * tenth_operation_cost_basis, &tenth_operation_cost), + make_dummy_cc_tx(10 * tenth_operation_cost_basis, &tenth_operation_cost), + make_dummy_cc_tx(10 * tenth_operation_cost_basis, &tenth_operation_cost), ]); estimator @@ -256,7 +258,7 @@ fn test_one_block_mostly_filled() { /// /// We add 5 blocks with window size 5 so none should be forgotten. #[test] -fn test_five_blocks_mostly_filled() { +fn test_window_size_forget_nothing() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); @@ -280,7 +282,7 @@ fn test_five_blocks_mostly_filled() { FeeRateEstimate { high: 30f64, middle: 30f64, - low: 1f64 + low: 30f64 } )); } @@ -290,7 +292,7 @@ fn test_five_blocks_mostly_filled() { /// /// We add 10 blocks with window size 5 so the first 5 should be forgotten. #[test] -fn test_ten_blocks_mostly_filled() { +fn test_window_size_forget_something() { let metric = ProportionalDotProduct::new(10_000); let mut estimator = instantiate_test_db(metric); @@ -314,7 +316,7 @@ fn test_ten_blocks_mostly_filled() { FeeRateEstimate { high: 80f64, middle: 80f64, - low: 1f64 + low: 80f64 } )); } From a42fbab3cdd7fbaf4c9037e3feeef1b4e38a0f1d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 01:16:06 +0000 Subject: [PATCH 047/103] removed orig files --- src/cost_estimates/mod.rs.orig | 308 --------------------------- src/cost_estimates/tests/mod.rs.orig | 32 --- 2 files changed, 340 deletions(-) delete mode 100644 src/cost_estimates/mod.rs.orig delete mode 100644 src/cost_estimates/tests/mod.rs.orig diff --git a/src/cost_estimates/mod.rs.orig b/src/cost_estimates/mod.rs.orig deleted file mode 100644 index c791f71ee9..0000000000 --- a/src/cost_estimates/mod.rs.orig +++ /dev/null @@ -1,308 +0,0 @@ -use std::cmp; -use std::collections::HashMap; -use std::iter::FromIterator; -use std::ops::{Add, Div, Mul, Rem, Sub}; -use std::path::Path; -use std::{error::Error, fmt::Display}; - -use chainstate::stacks::events::{StacksTransactionReceipt, TransactionOrigin}; -use chainstate::stacks::{StacksBlock, TransactionPayload}; -use rusqlite::Error as SqliteError; -use vm::costs::ExecutionCost; - -use burnchains::Txid; -use chainstate::stacks::db::StacksEpochReceipt; - -<<<<<<< HEAD -pub mod fee_rate_fuzzer; -||||||| merged common ancestors -======= -pub mod fee_medians; ->>>>>>> feat/fee-rates -pub mod fee_scalar; -pub mod metrics; -pub mod pessimistic; - -#[cfg(test)] -pub mod tests; - -use crate::chainstate::stacks::StacksTransaction; -use core::StacksEpochId; - -use self::metrics::CostMetric; -pub use self::pessimistic::PessimisticEstimator; - -/// This trait is for implementation of *fee rate* estimation: estimators should -/// track the actual paid fee rate for transactions in blocks, and use that to -/// provide estimates for block inclusion. Fee rate estimators provide an estimate -/// for the amount of microstx per unit of the block limit occupied that must be -/// paid for miners to consider the transaction for inclusion in a block. -/// -/// Note: `CostEstimator` and `FeeRateEstimator` implementations do two very different -/// tasks. `CostEstimator` implementations estimate the `ExecutionCost` for a transaction -/// payload. `FeeRateEstimator` implementations estimate the network's current fee rate. -/// Clients interested in determining the fee to be paid for a transaction must used both -/// whereas miners only need to use a `CostEstimator` -pub trait FeeEstimator { - /// This method is invoked by the `stacks-node` to update the fee estimator with a new - /// block receipt. - fn notify_block( - &mut self, - receipt: &StacksEpochReceipt, - block_limit: &ExecutionCost, - ) -> Result<(), EstimatorError>; - /// Get the current estimates for fee rate - fn get_rate_estimates(&self) -> Result; -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -/// This struct is returned from fee rate estimators as the current best estimate for -/// fee rates to include a transaction in a block. -pub struct FeeRateEstimate { - pub high: f64, - pub middle: f64, - pub low: f64, -} - -fn saturating_f64_math(res: f64) -> f64 { - if res.is_finite() { - res - } else if res.is_infinite() && res.is_sign_positive() { - f64::MAX - } else if res.is_infinite() && res.is_sign_negative() { - f64::MIN - } else { - 1f64 - } -} - -impl FeeRateEstimate { - pub fn to_vec(self) -> Vec { - vec![self.low, self.middle, self.high] - } -} - -impl Mul for FeeRateEstimate { - type Output = FeeRateEstimate; - - fn mul(self, rhs: f64) -> FeeRateEstimate { - FeeRateEstimate { - high: saturating_f64_math(self.high * rhs), - middle: saturating_f64_math(self.middle * rhs), - low: saturating_f64_math(self.low * rhs), - } - } -} - -impl Add for FeeRateEstimate { - type Output = FeeRateEstimate; - - fn add(self, rhs: Self) -> FeeRateEstimate { - FeeRateEstimate { - high: saturating_f64_math(self.high + rhs.high), - middle: saturating_f64_math(self.middle + rhs.middle), - low: saturating_f64_math(self.low + rhs.low), - } - } -} - -/// Given a cost estimator and a scalar metric, estimate the fee rate for -/// the provided transaction -pub fn estimate_fee_rate( - tx: &StacksTransaction, - estimator: &CE, - metric: &CM, - block_limit: &ExecutionCost, - stacks_epoch_id: &StacksEpochId, -) -> Result { - let cost_estimate = estimator.estimate_cost(&tx.payload, stacks_epoch_id)?; - let metric_estimate = metric.from_cost_and_len(&cost_estimate, block_limit, tx.tx_len()); - Ok(tx.get_tx_fee() as f64 / metric_estimate as f64) -} - -/// This trait is for implementation of *execution cost* estimation. CostEstimators -/// provide the estimated `ExecutionCost` for a given `TransactionPayload`. -/// -/// Note: `CostEstimator` and `FeeRateEstimator` implementations do two very different -/// tasks. `CostEstimator` implementations estimate the `ExecutionCost` for a transaction -/// payload. `FeeRateEstimator` implementations estimate the network's current fee rate. -/// Clients interested in determining the fee to be paid for a transaction must used both -/// whereas miners only need to use a `CostEstimator` -pub trait CostEstimator: Send { - /// This method is invoked by the `stacks-node` to update the cost estimator with a new - /// cost measurement. The given `tx` had a measured cost of `actual_cost`. - fn notify_event( - &mut self, - tx: &TransactionPayload, - actual_cost: &ExecutionCost, - block_limit: &ExecutionCost, - evaluated_epoch: &StacksEpochId, - ) -> Result<(), EstimatorError>; - - /// This method is used by a stacks-node to obtain an estimate for a given transaction payload. - /// If the estimator cannot provide an accurate estimate for a given payload, it should return - /// `EstimatorError::NoEstimateAvailable` - fn estimate_cost( - &self, - tx: &TransactionPayload, - evaluated_epoch: &StacksEpochId, - ) -> Result; - - /// This method is invoked by the `stacks-node` to notify the estimator of all the transaction - /// receipts in a given block. - /// - /// A default implementation is provided to implementing structs that processes the transaction - /// receipts by feeding them into `CostEstimator::notify_event()` - fn notify_block( - &mut self, - receipts: &[StacksTransactionReceipt], - block_limit: &ExecutionCost, - stacks_epoch_id: &StacksEpochId, - ) { - // iterate over receipts, and for all the tx receipts, notify the event - for current_receipt in receipts.iter() { - let current_txid = match current_receipt.transaction { - TransactionOrigin::Burn(_) => continue, - TransactionOrigin::Stacks(ref tx) => tx.txid(), - }; - let tx_payload = match current_receipt.transaction { - TransactionOrigin::Burn(_) => continue, - TransactionOrigin::Stacks(ref tx) => &tx.payload, - }; - - if let Err(e) = self.notify_event( - tx_payload, - ¤t_receipt.execution_cost, - block_limit, - stacks_epoch_id, - ) { - info!("CostEstimator failed to process event"; - "txid" => %current_txid, - "error" => %e, - "execution_cost" => %current_receipt.execution_cost); - } - } - } -} - -#[derive(Debug, PartialEq)] -pub enum EstimatorError { - NoEstimateAvailable, - SqliteError(SqliteError), -} - -impl Error for EstimatorError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - EstimatorError::SqliteError(ref e) => Some(e), - _ => None, - } - } -} - -impl Display for EstimatorError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EstimatorError::NoEstimateAvailable => { - write!(f, "No estimate available for the provided payload.") - } - EstimatorError::SqliteError(e) => { - write!(f, "Sqlite error from estimator: {}", e) - } - } - } -} - -impl EstimatorError { - pub fn into_json(&self) -> serde_json::Value { - let (reason_code, reason_data) = match self { - EstimatorError::NoEstimateAvailable => ( - "NoEstimateAvailable", - Some(json!({"message": self.to_string()})), - ), - EstimatorError::SqliteError(_) => { - ("DatabaseError", Some(json!({"message": self.to_string()}))) - } - }; - let mut result = json!({ - "error": "Estimation could not be performed", - "reason": reason_code, - }); - if let Some(reason_data) = reason_data { - result - .as_object_mut() - .unwrap() - .insert("reason_data".to_string(), reason_data); - } - result - } -} - -/// Null `CostEstimator` implementation: this is useful in rust typing when supplying -/// a `None` value to the `ChainsCoordinator` estimator field. -impl CostEstimator for () { - fn notify_event( - &mut self, - _tx: &TransactionPayload, - _actual_cost: &ExecutionCost, - _block_limit: &ExecutionCost, - _evaluated_epoch: &StacksEpochId, - ) -> Result<(), EstimatorError> { - Ok(()) - } - - fn estimate_cost( - &self, - _tx: &TransactionPayload, - _evaluated_epoch: &StacksEpochId, - ) -> Result { - Err(EstimatorError::NoEstimateAvailable) - } -} - -/// Null `FeeEstimator` implementation: this is useful in rust typing when supplying -/// a `None` value to the `ChainsCoordinator` estimator field. -impl FeeEstimator for () { - fn notify_block( - &mut self, - _receipt: &StacksEpochReceipt, - _block_limit: &ExecutionCost, - ) -> Result<(), EstimatorError> { - Ok(()) - } - - fn get_rate_estimates(&self) -> Result { - Err(EstimatorError::NoEstimateAvailable) - } -} - -/// This estimator always returns a unit estimate in all dimensions. -/// This can be paired with the UnitMetric to cause block assembly to consider -/// *only* transaction fees, not performing any kind of rate estimation. -pub struct UnitEstimator; - -impl CostEstimator for UnitEstimator { - fn notify_event( - &mut self, - _tx: &TransactionPayload, - _actual_cost: &ExecutionCost, - _block_limit: &ExecutionCost, - _evaluated_epoch: &StacksEpochId, - ) -> Result<(), EstimatorError> { - Ok(()) - } - - fn estimate_cost( - &self, - _tx: &TransactionPayload, - _evaluated_epoch: &StacksEpochId, - ) -> Result { - Ok(ExecutionCost { - write_length: 1, - write_count: 1, - read_length: 1, - read_count: 1, - runtime: 1, - }) - } -} diff --git a/src/cost_estimates/tests/mod.rs.orig b/src/cost_estimates/tests/mod.rs.orig deleted file mode 100644 index 3827e9e227..0000000000 --- a/src/cost_estimates/tests/mod.rs.orig +++ /dev/null @@ -1,32 +0,0 @@ -use cost_estimates::FeeRateEstimate; - -pub mod common; -pub mod cost_estimators; -<<<<<<< HEAD -pub mod fee_rate_fuzzer; -||||||| merged common ancestors -======= -pub mod fee_medians; ->>>>>>> feat/fee-rates -pub mod fee_scalar; -pub mod metrics; - -#[test] -fn fee_rate_estimate_math_units() { - let maximal_estimate = FeeRateEstimate { - high: f64::MAX, - middle: f64::MAX, - low: f64::MAX, - }; - - assert_eq!( - maximal_estimate, - maximal_estimate.clone() * f64::MAX, - "Fee rate estimate math should saturate" - ); - assert_eq!( - maximal_estimate, - maximal_estimate.clone() + maximal_estimate.clone(), - "Fee rate estimate math should saturate" - ); -} From 243b49e59569b8b4dd13b27aab55892455b81b5d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 01:45:57 +0000 Subject: [PATCH 048/103] hook up estimator to stacks node --- src/cost_estimates/mod.rs | 2 +- src/cost_estimates/tests/mod.rs | 2 +- testnet/stacks-node/src/config.rs | 39 +++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index aff3b1a7ce..51402a7203 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -13,8 +13,8 @@ use vm::costs::ExecutionCost; use burnchains::Txid; use chainstate::stacks::db::StacksEpochReceipt; -pub mod fee_rate_fuzzer; pub mod fee_medians; +pub mod fee_rate_fuzzer; pub mod fee_scalar; pub mod metrics; pub mod pessimistic; diff --git a/src/cost_estimates/tests/mod.rs b/src/cost_estimates/tests/mod.rs index e2357b2b3d..8b5ce592b1 100644 --- a/src/cost_estimates/tests/mod.rs +++ b/src/cost_estimates/tests/mod.rs @@ -2,8 +2,8 @@ use cost_estimates::FeeRateEstimate; pub mod common; pub mod cost_estimators; -pub mod fee_rate_fuzzer; pub mod fee_medians; +pub mod fee_rate_fuzzer; pub mod fee_scalar; pub mod metrics; diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index b42adabbc2..7f7a917e17 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -14,6 +14,8 @@ use stacks::core::StacksEpoch; use stacks::core::{ CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, }; +use stacks::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator; +use stacks::cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; use stacks::cost_estimates::fee_scalar::ScalarFeeRateEstimator; use stacks::cost_estimates::metrics::CostMetric; use stacks::cost_estimates::metrics::ProportionalDotProduct; @@ -1040,6 +1042,7 @@ pub enum CostEstimatorName { #[derive(Clone, Debug)] pub enum FeeEstimatorName { ScalarFeeRate, + FuzzedWeightedMedianFeeRate, } #[derive(Clone, Debug)] @@ -1082,6 +1085,8 @@ impl FeeEstimatorName { fn panic_parse(s: String) -> FeeEstimatorName { if &s.to_lowercase() == "scalar_fee_rate" { FeeEstimatorName::ScalarFeeRate + } else if &s.to_lowercase() == "fuzzed_weighted_median_fee_rate" { + FeeEstimatorName::FuzzedWeightedMedianFeeRate } else { panic!( "Bad fee estimator name supplied in configuration file: {}", @@ -1178,10 +1183,12 @@ impl Config { pub fn make_fee_estimator(&self) -> Option> { let metric = self.make_cost_metric()?; let fee_estimator: Box = match self.estimation.fee_estimator.as_ref()? { - FeeEstimatorName::ScalarFeeRate => Box::new( - self.estimation - .make_scalar_fee_estimator(self.get_chainstate_path(), metric), - ), + FeeEstimatorName::ScalarFeeRate => self + .estimation + .make_scalar_fee_estimator(self.get_chainstate_path(), metric), + FeeEstimatorName::FuzzedWeightedMedianFeeRate => self + .estimation + .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric), }; Some(fee_estimator) @@ -1206,15 +1213,33 @@ impl FeeEstimationConfig { &self, mut chainstate_path: PathBuf, metric: CM, - ) -> ScalarFeeRateEstimator { + ) -> Box { if let Some(FeeEstimatorName::ScalarFeeRate) = self.fee_estimator.as_ref() { chainstate_path.push("fee_estimator_scalar_rate.sqlite"); - ScalarFeeRateEstimator::open(&chainstate_path, metric) - .expect("Error opening fee estimator") + Box::new( + ScalarFeeRateEstimator::open(&chainstate_path, metric) + .expect("Error opening fee estimator"), + ) } else { panic!("BUG: Expected to configure a scalar fee estimator"); } } + + pub fn make_fuzzed_weighted_median_fee_estimator( + &self, + mut chainstate_path: PathBuf, + metric: CM, + ) -> Box { + if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() { + chainstate_path.push("fee_estimator_scalar_rate.sqlite"); + let underlying_estimator = + WeightedMedianFeeRateEstimator::open(&chainstate_path, metric, 5) + .expect("Error opening fee estimator"); + FeeRateFuzzer::new(Box::new(underlying_estimator), 0.5) + } else { + panic!("BUG: Expected to configure a weighted median fee estimator"); + } + } } impl NodeConfig { From 1c13db3ed797742fd6d8054c46917d787d2127c3 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 15:40:06 +0000 Subject: [PATCH 049/103] fix build using statics --- testnet/stacks-node/src/config.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 7f7a917e17..36601808cd 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1209,7 +1209,7 @@ impl FeeEstimationConfig { } } - pub fn make_scalar_fee_estimator( + pub fn make_scalar_fee_estimator( &self, mut chainstate_path: PathBuf, metric: CM, @@ -1225,7 +1225,9 @@ impl FeeEstimationConfig { } } - pub fn make_fuzzed_weighted_median_fee_estimator( + // Creates a fuzzed WeightedMedianFeeRateEstimator with window_size 5. The fuzz + // is uniform with bounds [+/- 0.5]. + pub fn make_fuzzed_weighted_median_fee_estimator( &self, mut chainstate_path: PathBuf, metric: CM, From 0cbe379493dc58aea39e5aa0812988a0f6e4f32e Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 17:35:28 +0000 Subject: [PATCH 050/103] make the FeeRateFuzzer generic instead of using boxes --- src/cost_estimates/fee_rate_fuzzer.rs | 25 ++++++++++++--------- src/cost_estimates/tests/fee_rate_fuzzer.rs | 9 +++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 2699d82bd1..be9034072b 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -15,9 +15,9 @@ use rand::SeedableRng; /// /// Note: We currently use "uniform" random noise instead of "normal" distributed noise to avoid /// importing a new crate just for this. -pub struct FeeRateFuzzer { +pub struct FeeRateFuzzer { /// We will apply a random "fuzz" on top of the estimates given by this. - underlying: Box, + underlying: UnderlyingEstimator, /// Creator function for a new random generator. For prod, use `thread_rng`. For test, /// pass in a contrived generator. rng_creator: Box Box>, @@ -25,33 +25,36 @@ pub struct FeeRateFuzzer { uniform_bound: f64, } -impl FeeRateFuzzer { +impl FeeRateFuzzer { /// Constructor for production. It uses `thread_rng()` as the random number generator, /// to get truly pseudo-random numbers. - pub fn new(underlying: Box, uniform_bound: f64) -> Box { + pub fn new( + underlying: UnderlyingEstimator, + uniform_bound: f64, + ) -> FeeRateFuzzer { let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r }); - Box::new(Self { + Self { underlying, rng_creator, uniform_bound, - }) + } } /// Constructor meant for test. The user can pass in a contrived random number generator /// factory function, so that the test is repeatable. pub fn new_custom_creator( - underlying: Box, + underlying: UnderlyingEstimator, rng_creator: Box Box>, uniform_bound: f64, - ) -> Box { - Box::new(Self { + ) -> FeeRateFuzzer { + Self { underlying, rng_creator, uniform_bound, - }) + } } /// Add a uniform fuzz to input. @@ -66,7 +69,7 @@ impl FeeRateFuzzer { } } -impl FeeEstimator for FeeRateFuzzer { +impl FeeEstimator for FeeRateFuzzer { /// Just passes the information straight to `underlying`. fn notify_block( &mut self, diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index b285d4e9c7..4fa5e24bbf 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -55,8 +55,7 @@ fn test_fuzzing_seed1() { let r: Box = Box::new(rng); r }); - let fuzzed_estimator = - FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + let fuzzed_estimator = FeeRateFuzzer::new_custom_creator(mock_estimator, rng_creator, 0.1); assert_eq!( fuzzed_estimator @@ -80,8 +79,7 @@ fn test_fuzzing_seed2() { let r: Box = Box::new(rng); r }); - let fuzzed_estimator = - FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + let fuzzed_estimator = FeeRateFuzzer::new_custom_creator(mock_estimator, rng_creator, 0.1); assert_eq!( fuzzed_estimator @@ -130,8 +128,7 @@ fn test_notify_pass_through() { let r: Box = Box::new(rng); r }); - let mut fuzzed_estimator = - FeeRateFuzzer::new_custom_creator(Box::new(mock_estimator), rng_creator, 0.1); + let mut fuzzed_estimator = FeeRateFuzzer::new_custom_creator(mock_estimator, rng_creator, 0.1); let receipt = make_block_receipt(vec![]); fuzzed_estimator From 17a62306c04e3c6a6c2dac0069bb3754c09e58aa Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 14 Dec 2021 17:56:12 +0000 Subject: [PATCH 051/103] fix stacks-node --- testnet/stacks-node/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 36601808cd..30e595268a 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1237,7 +1237,7 @@ impl FeeEstimationConfig { let underlying_estimator = WeightedMedianFeeRateEstimator::open(&chainstate_path, metric, 5) .expect("Error opening fee estimator"); - FeeRateFuzzer::new(Box::new(underlying_estimator), 0.5) + Box::new(FeeRateFuzzer::new(underlying_estimator, 0.5)) } else { panic!("BUG: Expected to configure a weighted median fee estimator"); } From 801ddbb241b35f01437e6e73b68db9b64b967fa6 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 15 Dec 2021 16:56:49 +0000 Subject: [PATCH 052/103] test if failing with bad input, want to send link --- .../conf/mainnet-follower-conf.toml | 3 + testnet/stacks-node/src/config.rs | 1 + .../src/tests/neon_integrations.rs | 136 +++++++++++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/testnet/stacks-node/conf/mainnet-follower-conf.toml b/testnet/stacks-node/conf/mainnet-follower-conf.toml index f6b283e38e..7dd81ee89f 100644 --- a/testnet/stacks-node/conf/mainnet-follower-conf.toml +++ b/testnet/stacks-node/conf/mainnet-follower-conf.toml @@ -13,6 +13,9 @@ password = "blockstacksystem" rpc_port = 8332 peer_port = 8333 +[fee_estimation] +fee_estimator = "fuzzed_weighted_median_fee_rate" + # Used for sending events to a local stacks-blockchain-api service # [[events_observer]] # endpoint = "localhost:3700" diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 30e595268a..551d3758d7 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -44,6 +44,7 @@ pub struct ConfigFile { pub ustx_balance: Option>, pub events_observer: Option>, pub connection_options: Option, + // Note: put the name here pub fee_estimation: Option, pub miner: Option, } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index bce9046c70..fb290ea0d2 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -21,8 +21,8 @@ use stacks::core; use stacks::core::CHAIN_ID_TESTNET; use stacks::net::atlas::{AtlasConfig, AtlasDB, MAX_ATTACHMENT_INV_PAGES_PER_REQUEST}; use stacks::net::{ - AccountEntryResponse, GetAttachmentResponse, GetAttachmentsInvResponse, - PostTransactionRequestBody, RPCPeerInfoData, + AccountEntryResponse, FeeRateEstimateRequestBody, GetAttachmentResponse, + GetAttachmentsInvResponse, PostTransactionRequestBody, RPCPeerInfoData, }; use stacks::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockHeader, StacksBlockId, @@ -72,6 +72,8 @@ use super::{ SK_2, }; +use crate::config::FeeEstimatorName; + pub fn neon_integration_test_conf() -> (Config, StacksAddress) { let mut conf = super::new_test_conf(); @@ -668,6 +670,25 @@ fn get_pox_info(http_origin: &str) -> RPCPoxInfoData { .unwrap() } +fn get_fee_rate_info( + http_origin: &str, + fee_rate_input: &FeeRateEstimateRequestBody, +) -> RPCPoxInfoData { + let client = reqwest::blocking::Client::new(); + let path = format!("{}/v2/fees/transaction", http_origin); + + let body = { serde_json::to_vec(&json!(fee_rate_input)).unwrap() }; + + client + .post(&path) + .header("Content-Type", "application/json") + .body(body) + .send() + .unwrap() + .json::() + .unwrap() +} + fn get_chain_tip(http_origin: &str) -> (ConsensusHash, BlockHeaderHash) { let client = reqwest::blocking::Client::new(); let path = format!("{}/v2/info", http_origin); @@ -5847,3 +5868,114 @@ fn atlas_stress_integration_test() { test_observer::clear(); } + +#[test] +#[ignore] +fn fuzzed_median_fee_rate_estimation_test() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + // cost = { + // "write_length":350981, + // "write_count":932, + // "read_length":3711941, + // "read_count":3721, + // "runtime":4960871000 + // } + let max_contract_src = format!( + "(define-public (f) (begin {} (ok 1))) (begin (f))", + (0..310) + .map(|_| format!( + "(unwrap! (contract-call? '{} submit-proposal '{} \"cost-old\" '{} \"cost-new\") (err 1))", + boot_code_id("cost-voting", false), + boot_code_id("costs", false), + boot_code_id("costs", false), + )) + .collect::>() + .join(" ") + ); + + let spender_sk = StacksPrivateKey::new(); + let addr = to_addr(&spender_sk); + + let tx = make_contract_publish(&spender_sk, 0, 59070, "max", &max_contract_src); + + let (mut conf, miner_account) = neon_integration_test_conf(); + + conf.estimation.fee_estimator = Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate); + + conf.initial_balances.push(InitialBalance { + address: addr.clone().into(), + amount: 10000000, + }); + + conf.node.mine_microblocks = true; + conf.node.wait_time_for_microblocks = 30000; + conf.node.microblock_frequency = 1000; + + conf.miner.min_tx_fee = 1; + conf.miner.first_attempt_time_ms = i64::max_value() as u64; + conf.miner.subsequent_attempt_time_ms = i64::max_value() as u64; + + let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + btcd_controller + .start_bitcoind() + .map_err(|_e| ()) + .expect("Failed starting bitcoind"); + + let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); + let http_origin = format!("http://{}", &conf.node.rpc_bind); + + btc_regtest_controller.bootstrap_chain(201); + + eprintln!("Chain bootstrapped..."); + + let mut run_loop = neon::RunLoop::new(conf); + let blocks_processed = run_loop.get_blocks_processed_arc(); + + let channel = run_loop.get_coordinator_channel().unwrap(); + + thread::spawn(move || run_loop.start(None, 0)); + + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + + // first block wakes up the run loop + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // first block will hold our VRF registration + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // second block will be the first mined Stacks block + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + let account = get_account(&http_origin, &miner_account); + assert_eq!(account.nonce, 1); + assert_eq!(account.balance, 0); + + let account = get_account(&http_origin, &addr); + assert_eq!(account.nonce, 0); + assert_eq!(account.balance, 10000000); + + submit_tx(&http_origin, &tx); + sleep_ms(60_000); + + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + sleep_ms(60_000); + + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + let res = get_account(&http_origin, &addr); + assert_eq!(res.nonce, 1); + + let fee_rate_input = FeeRateEstimateRequestBody { + estimated_len: Some(1000u64), + transaction_payload: "1baf8".to_string(), + }; + let account2 = get_fee_rate_info(&http_origin, &fee_rate_input); + assert_eq!(account2.pox_activation_threshold_ustx, 1); + + test_observer::clear(); + channel.stop_chains_coordinator(); +} From 8e83fdf35520f95b938dc6709b7ea5fa13b4a092 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 15 Dec 2021 17:40:33 +0000 Subject: [PATCH 053/103] test is passing but vacuous --- .../src/tests/neon_integrations.rs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index fb290ea0d2..bca6679515 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -15,6 +15,7 @@ use stacks::burnchains::bitcoin::address::{BitcoinAddress, BitcoinAddressType}; use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::Txid; use stacks::chainstate::burn::operations::{BlockstackOperationType, PreStxOp, TransferStxOp}; +use stacks::chainstate::stacks::TokenTransferMemo; use stacks::clarity::vm_execute as execute; use stacks::codec::StacksMessageCodec; use stacks::core; @@ -29,7 +30,7 @@ use stacks::types::chainstate::{ StacksMicroblockHeader, }; use stacks::util::hash::Hash160; -use stacks::util::hash::{bytes_to_hex, hex_bytes}; +use stacks::util::hash::{bytes_to_hex, hex_bytes, to_hex}; use stacks::util::secp256k1::Secp256k1PublicKey; use stacks::util::{get_epoch_time_ms, get_epoch_time_secs, sleep_ms}; use stacks::vm::database::ClarityDeserializable; @@ -5969,12 +5970,33 @@ fn fuzzed_median_fee_rate_estimation_test() { let res = get_account(&http_origin, &addr); assert_eq!(res.nonce, 1); - let fee_rate_input = FeeRateEstimateRequestBody { - estimated_len: Some(1000u64), - transaction_payload: "1baf8".to_string(), - }; - let account2 = get_fee_rate_info(&http_origin, &fee_rate_input); - assert_eq!(account2.pox_activation_threshold_ustx, 1); + { + // perform some tests of the fee rate interface + let path = format!("{}/v2/fees/transaction", &http_origin); + + let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); + let tx_payload = TransactionPayload::TokenTransfer( + contract_addr.clone().into(), + 10_000_000, + TokenTransferMemo([0; 34]), + ); + + let payload_data = tx_payload.serialize_to_vec(); + let payload_hex = format!("0x{}", to_hex(&payload_data)); + + eprintln!("Test: POST {}", path); + + let body = json!({ "transaction_payload": payload_hex.clone() }); + + let client = reqwest::blocking::Client::new(); + let res = client + .post(&path) + .json(&body) + .send() + .expect("Should be able to post") + .json::() + .expect("Failed to parse result into JSON"); + } test_observer::clear(); channel.stop_chains_coordinator(); From 283342196d16d23af9cf1f473d3b571f2e090475 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 15 Dec 2021 20:15:41 +0000 Subject: [PATCH 054/103] remove sleep calls --- testnet/stacks-node/src/tests/neon_integrations.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index bca6679515..3ac758ffe4 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5955,15 +5955,9 @@ fn fuzzed_median_fee_rate_estimation_test() { assert_eq!(account.nonce, 1); assert_eq!(account.balance, 0); - let account = get_account(&http_origin, &addr); - assert_eq!(account.nonce, 0); - assert_eq!(account.balance, 10000000); - submit_tx(&http_origin, &tx); - sleep_ms(60_000); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - sleep_ms(60_000); next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); @@ -5989,13 +5983,14 @@ fn fuzzed_median_fee_rate_estimation_test() { let body = json!({ "transaction_payload": payload_hex.clone() }); let client = reqwest::blocking::Client::new(); - let res = client + let fee_rate_result = client .post(&path) .json(&body) .send() .expect("Should be able to post") .json::() .expect("Failed to parse result into JSON"); + warn!("fee_rate_result {:?}", &fee_rate_result); } test_observer::clear(); From 403e7b521639e3e8b56429fd75eeb8921ecfd12d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 15 Dec 2021 23:01:52 +0000 Subject: [PATCH 055/103] Estimation could not be performed WARN [1639609185.501746] [testnet/stacks-node/src/tests/neon_integrations.rs:6052] [tests::neon_integrations::fuzzed_median_fee_rate_estimation_test] fee_rate_result Object({"error": String("Estimation could not be performed"), "reason": String("NoEstimateAvailable"), "reason_data": Object({"message": String("No estimate available for the provided payload.")})}) --- src/cost_estimates/fee_medians.rs | 3 + src/cost_estimates/fee_rate_fuzzer.rs | 4 + .../src/tests/neon_integrations.rs | 175 ++++++++++++------ 3 files changed, 123 insertions(+), 59 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 9d93a9eb53..5da6942882 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -69,6 +69,7 @@ pub struct FeeRateAndWeight { impl WeightedMedianFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M, window_size: u32) -> Result { + warn!("WeightedMedianFeeRateEstimator::open"); let mut db = sqlite_open( p, rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, @@ -195,6 +196,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { + warn!("WeightedMedianFeeRateEstimator::nofity_block"); // Calculate sorted fee rate for each transaction in the block. let mut working_fee_rates: Vec = receipt .tx_receipts @@ -225,6 +227,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { + warn!("WeightedMedianFeeRateEstimator::get_rate_estimates"); Self::get_rate_estimates_from_sql(&self.db, self.window_size) } } diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index be9034072b..9253a09f9c 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -32,6 +32,7 @@ impl FeeRateFuzzer { underlying: UnderlyingEstimator, uniform_bound: f64, ) -> FeeRateFuzzer { + warn!("FeeRateFuzzer::new"); let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r @@ -59,6 +60,7 @@ impl FeeRateFuzzer { /// Add a uniform fuzz to input. fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { + warn!("FeeRateFuzzer::fuzz_estimate"); let mut rng: Box = (self.rng_creator)(); let normal = Uniform::new(-self.uniform_bound, self.uniform_bound); FeeRateEstimate { @@ -76,11 +78,13 @@ impl FeeEstimator for FeeRateFuzzer { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { + warn!("FeeRateFuzzer::nofity_block"); self.underlying.notify_block(receipt, block_limit) } /// Call underlying estimator and add some fuzz. fn get_rate_estimates(&self) -> Result { + warn!("FeeRateFuzzer::get_rate_estimates"); match self.underlying.get_rate_estimates() { Ok(underlying_estimate) => Ok(self.fuzz_estimate(&underlying_estimate)), Err(e) => Err(e), diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 3ac758ffe4..3cf769f1e0 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -74,6 +74,8 @@ use super::{ }; use crate::config::FeeEstimatorName; +use stacks::vm::ContractName; +use std::convert::TryFrom; pub fn neon_integration_test_conf() -> (Config, StacksAddress) { let mut conf = super::new_test_conf(); @@ -301,6 +303,7 @@ pub fn next_block_and_wait( btc_controller: &mut BitcoinRegtestController, blocks_processed: &Arc, ) -> bool { + info!("next_block_and_wait"); let current = blocks_processed.load(Ordering::SeqCst); eprintln!( "Issuing block at {}, waiting for bump ({})", @@ -3767,14 +3770,14 @@ fn near_full_block_integration_test() { ); let spender_sk = StacksPrivateKey::new(); - let addr = to_addr(&spender_sk); + let spender_addr = to_addr(&spender_sk); let tx = make_contract_publish(&spender_sk, 0, 58450, "max", &max_contract_src); let (mut conf, miner_account) = neon_integration_test_conf(); conf.initial_balances.push(InitialBalance { - address: addr.clone().into(), + address: spender_addr.clone().into(), amount: 10000000, }); @@ -3822,7 +3825,7 @@ fn near_full_block_integration_test() { assert_eq!(account.nonce, 1); assert_eq!(account.balance, 0); - let account = get_account(&http_origin, &addr); + let account = get_account(&http_origin, &spender_addr); assert_eq!(account.nonce, 0); assert_eq!(account.balance, 10000000); @@ -3834,7 +3837,7 @@ fn near_full_block_integration_test() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - let res = get_account(&http_origin, &addr); + let res = get_account(&http_origin, &spender_addr); assert_eq!(res.nonce, 1); test_observer::clear(); @@ -4118,7 +4121,7 @@ fn pox_integration_test() { // let's stack with spender 2 and spender 3... - // now let's have sender_2 and sender_3 stack to pox addr 2 in + // now let's have sender_2 and sender_3 stack to pox spender_addr 2 in // two different txs, and make sure that they sum together in the reward set. let tx = make_contract_call( @@ -5870,6 +5873,60 @@ fn atlas_stress_integration_test() { test_observer::clear(); } +/// This function will call `next_block_and_wait` until the burnchain height underlying `BitcoinRegtestController` +/// reaches *exactly* `target_height`. +/// +/// Returns `false` if `next_block_and_wait` times out. +fn run_until_burnchain_height( + btc_regtest_controller: &mut BitcoinRegtestController, + blocks_processed: &Arc, + target_height: u64, + conf: &Config, +) -> bool { + warn!("run_until_burnchain_height"); + let tip_info = get_chain_info(&conf); + let mut current_height = tip_info.burn_block_height; + + while current_height < target_height { + eprintln!( + "run_until_burnchain_height: Issuing block at {}, current_height burnchain height is ({})", + get_epoch_time_secs(), + current_height + ); + let next_result = next_block_and_wait(btc_regtest_controller, &blocks_processed); + if !next_result { + return false; + } + let tip_info = get_chain_info(&conf); + current_height = tip_info.burn_block_height; + } + + assert_eq!(current_height, target_height); + true +} + +/// Deserializes the `StacksTransaction` objects from `blocks` and returns all those that +/// match `test_fn`. +fn select_transactions_where( + blocks: &Vec, + test_fn: fn(&StacksTransaction) -> bool, +) -> Vec { + let mut result = vec![]; + for block in blocks { + let transactions = block.get("transactions").unwrap().as_array().unwrap(); + for tx in transactions.iter() { + let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if test_fn(&parsed) { + result.push(parsed); + } + } + } + + return result; +} + #[test] #[ignore] fn fuzzed_median_fee_rate_estimation_test() { @@ -5877,48 +5934,43 @@ fn fuzzed_median_fee_rate_estimation_test() { return; } - // cost = { - // "write_length":350981, - // "write_count":932, - // "read_length":3711941, - // "read_count":3721, - // "runtime":4960871000 - // } - let max_contract_src = format!( - "(define-public (f) (begin {} (ok 1))) (begin (f))", - (0..310) - .map(|_| format!( - "(unwrap! (contract-call? '{} submit-proposal '{} \"cost-old\" '{} \"cost-new\") (err 1))", - boot_code_id("cost-voting", false), - boot_code_id("costs", false), - boot_code_id("costs", false), - )) - .collect::>() - .join(" ") - ); + let max_contract_src = r#" +;; define counter variable +(define-data-var counter int 0) - let spender_sk = StacksPrivateKey::new(); - let addr = to_addr(&spender_sk); +;; increment method +(define-public (increment) + (begin + (var-set counter (+ (var-get counter) 1)) + (ok (var-get counter)))) - let tx = make_contract_publish(&spender_sk, 0, 59070, "max", &max_contract_src); + (define-public (increment-many) + (begin + (unwrap! (increment) (err u1)) + (unwrap! (increment) (err u1)) + (unwrap! (increment) (err u1)) + (unwrap! (increment) (err u1)) + (ok (var-get counter)))) + "# + .to_string(); - let (mut conf, miner_account) = neon_integration_test_conf(); + warn!("max_contract_src {:?}", &max_contract_src); + + let spender_sk = StacksPrivateKey::new(); + let spender_addr = to_addr(&spender_sk); + + // 1) a basic test `Config` + // 2) the miner's + let (mut conf, _) = neon_integration_test_conf(); + // Set this estimator as special. conf.estimation.fee_estimator = Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate); conf.initial_balances.push(InitialBalance { - address: addr.clone().into(), + address: spender_addr.clone().into(), amount: 10000000, }); - conf.node.mine_microblocks = true; - conf.node.wait_time_for_microblocks = 30000; - conf.node.microblock_frequency = 1000; - - conf.miner.min_tx_fee = 1; - conf.miner.first_attempt_time_ms = i64::max_value() as u64; - conf.miner.subsequent_attempt_time_ms = i64::max_value() as u64; - let mut btcd_controller = BitcoinCoreController::new(conf.clone()); btcd_controller .start_bitcoind() @@ -5928,11 +5980,11 @@ fn fuzzed_median_fee_rate_estimation_test() { let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None); let http_origin = format!("http://{}", &conf.node.rpc_bind); - btc_regtest_controller.bootstrap_chain(201); + btc_regtest_controller.bootstrap_chain(200); eprintln!("Chain bootstrapped..."); - let mut run_loop = neon::RunLoop::new(conf); + let mut run_loop = neon::RunLoop::new(conf.clone()); let blocks_processed = run_loop.get_blocks_processed_arc(); let channel = run_loop.get_coordinator_channel().unwrap(); @@ -5941,28 +5993,34 @@ fn fuzzed_median_fee_rate_estimation_test() { // give the run loop some time to start up! wait_for_runloop(&blocks_processed); + run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 210, &conf); - // first block wakes up the run loop - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // first block will hold our VRF registration - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // second block will be the first mined Stacks block - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - let account = get_account(&http_origin, &miner_account); - assert_eq!(account.nonce, 1); - assert_eq!(account.balance, 0); - - submit_tx(&http_origin, &tx); - - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + // Publish the contract so we can use it. + submit_tx( + &http_origin, + &make_contract_publish( + &spender_sk, + 0, + 1100000, + "increment-contract", + &max_contract_src, + ), + ); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + // Wait to make sure the contract is published. + run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); - let res = get_account(&http_origin, &addr); - assert_eq!(res.nonce, 1); + // Check that we have processed the contract successfully, by checking that the contract call + // is in the block record. + let increment_calls_alice = select_transactions_where( + &test_observer::get_blocks(), + |transaction| match &transaction.payload { + TransactionPayload::ContractCall(contract) => { + contract.contract_name == ContractName::try_from("increment-contract").unwrap() + } + _ => false, + }, + ); { // perform some tests of the fee rate interface @@ -5993,6 +6051,5 @@ fn fuzzed_median_fee_rate_estimation_test() { warn!("fee_rate_result {:?}", &fee_rate_result); } - test_observer::clear(); channel.stop_chains_coordinator(); } From cf61fde3ea8e68d2545ff0eec1b5effd9ab79221 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Thu, 16 Dec 2021 01:46:28 +0000 Subject: [PATCH 056/103] getting the estimates out now --- Cargo.lock | 1 + Cargo.toml | 1 + src/cost_estimates/fee_scalar.rs | 1 + src/cost_estimates/pessimistic.rs | 11 ++ src/net/p2p.rs | 2 + src/net/rpc.rs | 14 ++- testnet/stacks-node/src/config.rs | 20 ++-- .../src/tests/neon_integrations.rs | 104 ++++++++++++++++-- 8 files changed, 137 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 729698ee09..9c15eff76c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,7 @@ name = "blockstack-core" version = "0.0.1" dependencies = [ "assert-json-diff", + "backtrace", "chrono", "curve25519-dalek", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index de67c629d4..b69dd79dd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ name = "block_limits" harness = false [dependencies] +backtrace = "0.3.50" rand = "=0.7.2" rand_chacha = "=0.2.2" serde = "1" diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index 3b46521cba..cc0647cd9e 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -225,6 +225,7 @@ impl FeeEstimator for ScalarFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { + warn!("ScalarFeeRateEstimator::get_rate_estimates"); let sql = "SELECT high, middle, low FROM scalar_fee_estimator WHERE estimate_key = ?"; self.db .query_row(sql, &[SINGLETON_ROW_ID], |row| { diff --git a/src/cost_estimates/pessimistic.rs b/src/cost_estimates/pessimistic.rs index 3467ceef3d..f610f9866f 100644 --- a/src/cost_estimates/pessimistic.rs +++ b/src/cost_estimates/pessimistic.rs @@ -259,6 +259,7 @@ impl CostEstimator for PessimisticEstimator { block_limit: &ExecutionCost, evaluated_epoch: &StacksEpochId, ) -> Result<(), EstimatorError> { + warn!("PessimisticEstimator::notify_event"); if self.log_error { // only log the estimate error if an estimate could be constructed if let Ok(estimated_cost) = self.estimate_cost(tx, evaluated_epoch) { @@ -282,7 +283,9 @@ impl CostEstimator for PessimisticEstimator { let sql_tx = tx_begin_immediate_sqlite(&mut self.db)?; for field in CostField::ALL.iter() { + warn!("PessimisticEstimator::notify_event:field"); let key = PessimisticEstimator::get_estimate_key(tx, field, evaluated_epoch); + warn!("estimate_cost:notify_event {}", &key); let field_cost = field.select_key(actual_cost); let mut current_sample = Samples::get_sqlite(&sql_tx, &key); current_sample.update_with(field_cost); @@ -297,31 +300,39 @@ impl CostEstimator for PessimisticEstimator { tx: &TransactionPayload, evaluated_epoch: &StacksEpochId, ) -> Result { + let runtime = PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch); + warn!("estimate_cost:runtime {}", &runtime); + warn!("PessimisticEstimator::estimate_cost"); let runtime = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; + warn!("PessimisticEstimator"); let read_count = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::ReadCount, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; + warn!("PessimisticEstimator"); let read_length = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::ReadLength, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; + warn!("PessimisticEstimator"); let write_count = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::WriteCount, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; + warn!("PessimisticEstimator"); let write_length = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::WriteLength, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; + warn!("PessimisticEstimator"); Ok(ExecutionCost { runtime, diff --git a/src/net/p2p.rs b/src/net/p2p.rs index b7b7dafeec..2f990511fd 100644 --- a/src/net/p2p.rs +++ b/src/net/p2p.rs @@ -4661,6 +4661,8 @@ impl PeerNetwork { handler_args: &RPCHandlerArgs, attachment_requests: &mut HashSet, ) -> Result { + let bt= backtrace::Backtrace::new(); + warn!("PeerNetwork::run {:?}", bt); debug!(">>>>>>>>>>>>>>>>>>>>>>> Begin Network Dispatch (poll for {}) >>>>>>>>>>>>>>>>>>>>>>>>>>>>", poll_timeout); let mut poll_states = match self.network { None => { diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 3d038ae423..d304197848 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1726,6 +1726,7 @@ impl ConversationHttp { tx: &TransactionPayload, estimated_len: u64, ) -> Result<(), net_error> { + warn!("handle_post_fee_rate_estimate"); let response_metadata = HttpResponseMetadata::from(req); let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?; let stacks_epoch = SortitionDB::get_stacks_epoch(sortdb.conn(), tip.block_height)? @@ -1737,9 +1738,14 @@ impl ConversationHttp { net_error::ChainstateError("Could not load Stacks epoch for canonical burn height".into()) })?; if let Some((cost_estimator, fee_estimator, metric)) = handler_args.get_estimators_ref() { + warn!("handle_post_fee_rate_estimate"); let estimated_cost = match cost_estimator.estimate_cost(tx, &stacks_epoch.epoch_id) { - Ok(x) => x, + Ok(x) => { + warn!("handle_post_fee_rate_estimate"); + x + }, Err(e) => { + warn!("handle_post_fee_rate_estimate"); // fail here debug!( "Estimator RPC endpoint failed to estimate tx: {}", tx.name() @@ -1749,11 +1755,15 @@ impl ConversationHttp { } }; + warn!("handle_post_fee_rate_estimate"); let scalar_cost = metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len); let fee_rates = match fee_estimator.get_rate_estimates() { - Ok(x) => x, + Ok(x) => { + warn!("handle_post_fee_rate_estimate"); + x}, Err(e) => { + warn!("handle_post_fee_rate_estimate"); debug!( "Estimator RPC endpoint failed to estimate fees for tx: {}", tx.name() diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 551d3758d7..3dfc6f9473 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1183,13 +1183,19 @@ impl Config { pub fn make_fee_estimator(&self) -> Option> { let metric = self.make_cost_metric()?; + let estimator_name = self.estimation.fee_estimator.as_ref(); + warn!("estimator_name {:?}", estimator_name); let fee_estimator: Box = match self.estimation.fee_estimator.as_ref()? { - FeeEstimatorName::ScalarFeeRate => self - .estimation - .make_scalar_fee_estimator(self.get_chainstate_path(), metric), - FeeEstimatorName::FuzzedWeightedMedianFeeRate => self - .estimation - .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric), + FeeEstimatorName::ScalarFeeRate => { + warn!("ScalarFeeRate"); + self.estimation + .make_scalar_fee_estimator(self.get_chainstate_path(), metric) + } + FeeEstimatorName::FuzzedWeightedMedianFeeRate => { + warn!("FuzzedWeightedMedianFeeRate"); + self.estimation + .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric) + } }; Some(fee_estimator) @@ -1234,7 +1240,7 @@ impl FeeEstimationConfig { metric: CM, ) -> Box { if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() { - chainstate_path.push("fee_estimator_scalar_rate.sqlite"); + chainstate_path.push("fee_fuzzed_weighted_median.sqlite"); let underlying_estimator = WeightedMedianFeeRateEstimator::open(&chainstate_path, metric, 5) .expect("Error opening fee estimator"); diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 3cf769f1e0..e693f4fafa 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -47,7 +47,7 @@ use stacks::{ use stacks::{ chainstate::stacks::{ db::StacksChainState, StacksBlock, StacksPrivateKey, StacksPublicKey, StacksTransaction, - TransactionPayload, + TransactionContractCall, TransactionPayload, }, net::RPCPoxInfoData, util::db::query_row_columns, @@ -75,6 +75,7 @@ use super::{ use crate::config::FeeEstimatorName; use stacks::vm::ContractName; +use stacks::vm::ClarityName; use std::convert::TryFrom; pub fn neon_integration_test_conf() -> (Config, StacksAddress) { @@ -5912,9 +5913,12 @@ fn select_transactions_where( test_fn: fn(&StacksTransaction) -> bool, ) -> Vec { let mut result = vec![]; + warn!("blocks.len() {:?}", blocks.len()); for block in blocks { let transactions = block.get("transactions").unwrap().as_array().unwrap(); + warn!("transactions.len() {:?}", transactions.len()); for tx in transactions.iter() { + warn!("tx) {:?}", &tx); let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); @@ -5934,6 +5938,7 @@ fn fuzzed_median_fee_rate_estimation_test() { return; } + let max_contract_src = r#" ;; define counter variable (define-data-var counter int 0) @@ -5970,6 +5975,11 @@ fn fuzzed_median_fee_rate_estimation_test() { address: spender_addr.clone().into(), amount: 10000000, }); + test_observer::spawn(); + conf.events_observers.push(EventObserverConfig { + endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT), + events_keys: vec![EventKeyType::AnyEvent], + }); let mut btcd_controller = BitcoinCoreController::new(conf.clone()); btcd_controller @@ -6006,9 +6016,25 @@ fn fuzzed_median_fee_rate_estimation_test() { &max_contract_src, ), ); + run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); + + // Alice calls the contract and should succeed, because we have not lowered the block limit + // yet. + submit_tx( + &http_origin, + &make_contract_call( + &spender_sk, + 1, + 1000, + &spender_addr.into(), + "increment-contract", + "increment-many", + &[], + ), + ); // Wait to make sure the contract is published. - run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); + run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 215, &conf); // Check that we have processed the contract successfully, by checking that the contract call // is in the block record. @@ -6021,17 +6047,79 @@ fn fuzzed_median_fee_rate_estimation_test() { _ => false, }, ); + assert_eq!(increment_calls_alice.len(), 1); { // perform some tests of the fee rate interface let path = format!("{}/v2/fees/transaction", &http_origin); let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); - let tx_payload = TransactionPayload::TokenTransfer( - contract_addr.clone().into(), - 10_000_000, - TokenTransferMemo([0; 34]), - ); + let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone().into(), + contract_name: ContractName::try_from("increment-contract").unwrap(), + function_name: ClarityName::try_from("increment-many").unwrap(), + function_args: vec![], + }); + + let payload_data = tx_payload.serialize_to_vec(); + let payload_hex = format!("0x{}", to_hex(&payload_data)); + + eprintln!("Test: POST {}", path); + + let body = json!({ "transaction_payload": payload_hex.clone() }); + + let client = reqwest::blocking::Client::new(); + let fee_rate_result = client + .post(&path) + .json(&body) + .send() + .expect("Should be able to post") + .json::() + .expect("Failed to parse result into JSON"); + warn!("fee_rate_result1 {:#?}", &fee_rate_result); + } + + { + // perform some tests of the fee rate interface + let path = format!("{}/v2/fees/transaction", &http_origin); + + let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); + let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone().into(), + contract_name: ContractName::try_from("increment-contract").unwrap(), + function_name: ClarityName::try_from("increment-many").unwrap(), + function_args: vec![], + }); + + let payload_data = tx_payload.serialize_to_vec(); + let payload_hex = format!("0x{}", to_hex(&payload_data)); + + eprintln!("Test: POST {}", path); + + let body = json!({ "transaction_payload": payload_hex.clone() }); + + let client = reqwest::blocking::Client::new(); + let fee_rate_result = client + .post(&path) + .json(&body) + .send() + .expect("Should be able to post") + .json::() + .expect("Failed to parse result into JSON"); + warn!("fee_rate_result2 {:#?}", &fee_rate_result); + } + + { + // perform some tests of the fee rate interface + let path = format!("{}/v2/fees/transaction", &http_origin); + + let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); + let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone().into(), + contract_name: ContractName::try_from("increment-contract").unwrap(), + function_name: ClarityName::try_from("increment-many").unwrap(), + function_args: vec![], + }); let payload_data = tx_payload.serialize_to_vec(); let payload_hex = format!("0x{}", to_hex(&payload_data)); @@ -6048,7 +6136,7 @@ fn fuzzed_median_fee_rate_estimation_test() { .expect("Should be able to post") .json::() .expect("Failed to parse result into JSON"); - warn!("fee_rate_result {:?}", &fee_rate_result); + warn!("fee_rate_result3 {:#?}", &fee_rate_result); } channel.stop_chains_coordinator(); From ee494ec42bb09d2691605744a0adb614afa9c041 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 00:14:05 +0000 Subject: [PATCH 057/103] test is making asserts --- src/cost_estimates/pessimistic.rs | 7 +- src/net/p2p.rs | 2 - src/net/rpc.rs | 17 +- .../src/tests/neon_integrations.rs | 202 ++++++++---------- 4 files changed, 101 insertions(+), 127 deletions(-) diff --git a/src/cost_estimates/pessimistic.rs b/src/cost_estimates/pessimistic.rs index f610f9866f..8705fa21ba 100644 --- a/src/cost_estimates/pessimistic.rs +++ b/src/cost_estimates/pessimistic.rs @@ -283,9 +283,9 @@ impl CostEstimator for PessimisticEstimator { let sql_tx = tx_begin_immediate_sqlite(&mut self.db)?; for field in CostField::ALL.iter() { - warn!("PessimisticEstimator::notify_event:field"); + warn!("PessimisticEstimator::notify_event:field"); let key = PessimisticEstimator::get_estimate_key(tx, field, evaluated_epoch); - warn!("estimate_cost:notify_event {}", &key); + warn!("estimate_cost:notify_event {}", &key); let field_cost = field.select_key(actual_cost); let mut current_sample = Samples::get_sqlite(&sql_tx, &key); current_sample.update_with(field_cost); @@ -300,7 +300,8 @@ impl CostEstimator for PessimisticEstimator { tx: &TransactionPayload, evaluated_epoch: &StacksEpochId, ) -> Result { - let runtime = PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch); + let runtime = + PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch); warn!("estimate_cost:runtime {}", &runtime); warn!("PessimisticEstimator::estimate_cost"); let runtime = Samples::get_estimate_sqlite( diff --git a/src/net/p2p.rs b/src/net/p2p.rs index 2f990511fd..b7b7dafeec 100644 --- a/src/net/p2p.rs +++ b/src/net/p2p.rs @@ -4661,8 +4661,6 @@ impl PeerNetwork { handler_args: &RPCHandlerArgs, attachment_requests: &mut HashSet, ) -> Result { - let bt= backtrace::Backtrace::new(); - warn!("PeerNetwork::run {:?}", bt); debug!(">>>>>>>>>>>>>>>>>>>>>>> Begin Network Dispatch (poll for {}) >>>>>>>>>>>>>>>>>>>>>>>>>>>>", poll_timeout); let mut poll_states = match self.network { None => { diff --git a/src/net/rpc.rs b/src/net/rpc.rs index d304197848..5c404cd2e8 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1738,14 +1738,14 @@ impl ConversationHttp { net_error::ChainstateError("Could not load Stacks epoch for canonical burn height".into()) })?; if let Some((cost_estimator, fee_estimator, metric)) = handler_args.get_estimators_ref() { - warn!("handle_post_fee_rate_estimate"); + warn!("handle_post_fee_rate_estimate"); let estimated_cost = match cost_estimator.estimate_cost(tx, &stacks_epoch.epoch_id) { Ok(x) => { - warn!("handle_post_fee_rate_estimate"); + warn!("handle_post_fee_rate_estimate"); x - }, + } Err(e) => { - warn!("handle_post_fee_rate_estimate"); // fail here + warn!("handle_post_fee_rate_estimate"); // fail here debug!( "Estimator RPC endpoint failed to estimate tx: {}", tx.name() @@ -1755,15 +1755,16 @@ impl ConversationHttp { } }; - warn!("handle_post_fee_rate_estimate"); + warn!("handle_post_fee_rate_estimate"); let scalar_cost = metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len); let fee_rates = match fee_estimator.get_rate_estimates() { Ok(x) => { - warn!("handle_post_fee_rate_estimate"); - x}, + warn!("handle_post_fee_rate_estimate"); + x + } Err(e) => { - warn!("handle_post_fee_rate_estimate"); + warn!("handle_post_fee_rate_estimate"); debug!( "Estimator RPC endpoint failed to estimate fees for tx: {}", tx.name() diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index e693f4fafa..5a60aae3db 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -74,9 +74,10 @@ use super::{ }; use crate::config::FeeEstimatorName; -use stacks::vm::ContractName; use stacks::vm::ClarityName; +use stacks::vm::ContractName; use std::convert::TryFrom; +use stacks::net::RPCFeeEstimateResponse; pub fn neon_integration_test_conf() -> (Config, StacksAddress) { let mut conf = super::new_test_conf(); @@ -5916,9 +5917,9 @@ fn select_transactions_where( warn!("blocks.len() {:?}", blocks.len()); for block in blocks { let transactions = block.get("transactions").unwrap().as_array().unwrap(); - warn!("transactions.len() {:?}", transactions.len()); + warn!("transactions.len() {:?}", transactions.len()); for tx in transactions.iter() { - warn!("tx) {:?}", &tx); + // warn!("tx) {:?}", &tx); let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); @@ -5938,7 +5939,6 @@ fn fuzzed_median_fee_rate_estimation_test() { return; } - let max_contract_src = r#" ;; define counter variable (define-data-var counter int 0) @@ -5973,7 +5973,7 @@ fn fuzzed_median_fee_rate_estimation_test() { conf.initial_balances.push(InitialBalance { address: spender_addr.clone().into(), - amount: 10000000, + amount: 10000000000, }); test_observer::spawn(); conf.events_observers.push(EventObserverConfig { @@ -6011,133 +6011,107 @@ fn fuzzed_median_fee_rate_estimation_test() { &make_contract_publish( &spender_sk, 0, - 1100000, + 110000, "increment-contract", &max_contract_src, ), ); run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); - // Alice calls the contract and should succeed, because we have not lowered the block limit - // yet. - submit_tx( - &http_origin, - &make_contract_call( - &spender_sk, - 1, - 1000, - &spender_addr.into(), - "increment-contract", - "increment-many", - &[], - ), - ); - - // Wait to make sure the contract is published. - run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 215, &conf); - - // Check that we have processed the contract successfully, by checking that the contract call - // is in the block record. - let increment_calls_alice = select_transactions_where( - &test_observer::get_blocks(), - |transaction| match &transaction.payload { - TransactionPayload::ContractCall(contract) => { - contract.contract_name == ContractName::try_from("increment-contract").unwrap() - } - _ => false, - }, - ); - assert_eq!(increment_calls_alice.len(), 1); - - { - // perform some tests of the fee rate interface - let path = format!("{}/v2/fees/transaction", &http_origin); - - let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); - let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { - address: contract_addr.clone().into(), - contract_name: ContractName::try_from("increment-contract").unwrap(), - function_name: ClarityName::try_from("increment-many").unwrap(), - function_args: vec![], - }); - - let payload_data = tx_payload.serialize_to_vec(); - let payload_hex = format!("0x{}", to_hex(&payload_data)); - - eprintln!("Test: POST {}", path); + let mut response_estimated_costs = vec![]; + let mut response_top_fee_rates = vec![]; + for i in 1..11 { + submit_tx( + &http_origin, + &make_contract_call( + &spender_sk, + i, // nonce + i * 100000, // payment + &spender_addr.into(), + "increment-contract", + "increment-many", + &[], + ), + ); + run_until_burnchain_height( + &mut btc_regtest_controller, + &blocks_processed, + 212 + 2 * i, + &conf, + ); + // Check that we have processed the contract successfully, by checking that the contract call + // is in the block record. + let increment_calls_alice = select_transactions_where( + &test_observer::get_blocks(), + |transaction| match &transaction.payload { + TransactionPayload::ContractCall(contract) => { + contract.contract_name == ContractName::try_from("increment-contract").unwrap() + } + _ => false, + }, + ); - let body = json!({ "transaction_payload": payload_hex.clone() }); + warn!( + "increment_calls_alice.len() {}", + increment_calls_alice.len() + ); - let client = reqwest::blocking::Client::new(); - let fee_rate_result = client - .post(&path) - .json(&body) - .send() - .expect("Should be able to post") - .json::() - .expect("Failed to parse result into JSON"); - warn!("fee_rate_result1 {:#?}", &fee_rate_result); - } + { + // perform some tests of the fee rate interface + let path = format!("{}/v2/fees/transaction", &http_origin); - { - // perform some tests of the fee rate interface - let path = format!("{}/v2/fees/transaction", &http_origin); - - let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); - let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { - address: contract_addr.clone().into(), - contract_name: ContractName::try_from("increment-contract").unwrap(), - function_name: ClarityName::try_from("increment-many").unwrap(), - function_args: vec![], - }); + let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); + let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { + address: contract_addr.clone().into(), + contract_name: ContractName::try_from("increment-contract").unwrap(), + function_name: ClarityName::try_from("increment-many").unwrap(), + function_args: vec![], + }); - let payload_data = tx_payload.serialize_to_vec(); - let payload_hex = format!("0x{}", to_hex(&payload_data)); + let payload_data = tx_payload.serialize_to_vec(); + let payload_hex = format!("0x{}", to_hex(&payload_data)); - eprintln!("Test: POST {}", path); + eprintln!("Test: POST {}", path); - let body = json!({ "transaction_payload": payload_hex.clone() }); + let body = json!({ "transaction_payload": payload_hex.clone() }); - let client = reqwest::blocking::Client::new(); - let fee_rate_result = client - .post(&path) - .json(&body) - .send() - .expect("Should be able to post") - .json::() - .expect("Failed to parse result into JSON"); - warn!("fee_rate_result2 {:#?}", &fee_rate_result); + let client = reqwest::blocking::Client::new(); + let fee_rate_result = client + .post(&path) + .json(&body) + .send() + .expect("Should be able to post") + // .json::() + .json::() + .expect("Failed to parse result into JSON"); + + + warn!("fee_rate_result {} {:#?}", i, &fee_rate_result); + response_estimated_costs.push(fee_rate_result.estimated_cost_scalar); + response_top_fee_rates.push(fee_rate_result.estimations.last().unwrap().fee_rate); + } } - { - // perform some tests of the fee rate interface - let path = format!("{}/v2/fees/transaction", &http_origin); - - let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); - let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { - address: contract_addr.clone().into(), - contract_name: ContractName::try_from("increment-contract").unwrap(), - function_name: ClarityName::try_from("increment-many").unwrap(), - function_args: vec![], - }); - - let payload_data = tx_payload.serialize_to_vec(); - let payload_hex = format!("0x{}", to_hex(&payload_data)); - - eprintln!("Test: POST {}", path); + warn!("response_estimated_costs, {:#?}", &response_estimated_costs); + warn!("response_top_fee_rates, {:#?}", &response_top_fee_rates); + assert_eq!(response_estimated_costs.len(), response_top_fee_rates.len()); - let body = json!({ "transaction_payload": payload_hex.clone() }); + // Check that: + // 1) The cost is always the same. + // 2) The estimated fee rate only either i) moves up, ii) stays within random noise of where it + // was. + for i in 1..response_estimated_costs.len() { + let curr_rate = response_top_fee_rates[i] as f64; + let last_rate = response_top_fee_rates[i - 1] as f64; + // Check that either 1) curr_rate is bigger, or 2) is only smaller by random 11% noise. + assert!(curr_rate - last_rate > -1.0 * curr_rate * 0.11); - let client = reqwest::blocking::Client::new(); - let fee_rate_result = client - .post(&path) - .json(&body) - .send() - .expect("Should be able to post") - .json::() - .expect("Failed to parse result into JSON"); - warn!("fee_rate_result3 {:#?}", &fee_rate_result); + // The cost is always the same. + let curr_cost = response_estimated_costs[i]; + let last_cost = response_estimated_costs[i - 1]; + assert_eq!(curr_cost, last_cost); } channel.stop_chains_coordinator(); } + From 33cff43ecf84ec99ab03f8702da7503877802305 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 00:20:25 +0000 Subject: [PATCH 058/103] undoing the debug logging --- Cargo.lock | 1 - Cargo.toml | 1 - src/cost_estimates/fee_medians.rs | 3 -- src/cost_estimates/fee_rate_fuzzer.rs | 4 -- src/cost_estimates/fee_scalar.rs | 1 - src/cost_estimates/pessimistic.rs | 12 ----- src/net/rpc.rs | 15 +----- testnet/stacks-node/src/config.rs | 4 -- .../src/tests/neon_integrations.rs | 52 +------------------ 9 files changed, 3 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c15eff76c..729698ee09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,7 +238,6 @@ name = "blockstack-core" version = "0.0.1" dependencies = [ "assert-json-diff", - "backtrace", "chrono", "curve25519-dalek", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index b69dd79dd5..de67c629d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ name = "block_limits" harness = false [dependencies] -backtrace = "0.3.50" rand = "=0.7.2" rand_chacha = "=0.2.2" serde = "1" diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 5da6942882..9d93a9eb53 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -69,7 +69,6 @@ pub struct FeeRateAndWeight { impl WeightedMedianFeeRateEstimator { /// Open a fee rate estimator at the given db path. Creates if not existent. pub fn open(p: &Path, metric: M, window_size: u32) -> Result { - warn!("WeightedMedianFeeRateEstimator::open"); let mut db = sqlite_open( p, rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, @@ -196,7 +195,6 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - warn!("WeightedMedianFeeRateEstimator::nofity_block"); // Calculate sorted fee rate for each transaction in the block. let mut working_fee_rates: Vec = receipt .tx_receipts @@ -227,7 +225,6 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { - warn!("WeightedMedianFeeRateEstimator::get_rate_estimates"); Self::get_rate_estimates_from_sql(&self.db, self.window_size) } } diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 9253a09f9c..be9034072b 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -32,7 +32,6 @@ impl FeeRateFuzzer { underlying: UnderlyingEstimator, uniform_bound: f64, ) -> FeeRateFuzzer { - warn!("FeeRateFuzzer::new"); let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r @@ -60,7 +59,6 @@ impl FeeRateFuzzer { /// Add a uniform fuzz to input. fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { - warn!("FeeRateFuzzer::fuzz_estimate"); let mut rng: Box = (self.rng_creator)(); let normal = Uniform::new(-self.uniform_bound, self.uniform_bound); FeeRateEstimate { @@ -78,13 +76,11 @@ impl FeeEstimator for FeeRateFuzzer { receipt: &StacksEpochReceipt, block_limit: &ExecutionCost, ) -> Result<(), EstimatorError> { - warn!("FeeRateFuzzer::nofity_block"); self.underlying.notify_block(receipt, block_limit) } /// Call underlying estimator and add some fuzz. fn get_rate_estimates(&self) -> Result { - warn!("FeeRateFuzzer::get_rate_estimates"); match self.underlying.get_rate_estimates() { Ok(underlying_estimate) => Ok(self.fuzz_estimate(&underlying_estimate)), Err(e) => Err(e), diff --git a/src/cost_estimates/fee_scalar.rs b/src/cost_estimates/fee_scalar.rs index cc0647cd9e..3b46521cba 100644 --- a/src/cost_estimates/fee_scalar.rs +++ b/src/cost_estimates/fee_scalar.rs @@ -225,7 +225,6 @@ impl FeeEstimator for ScalarFeeRateEstimator { } fn get_rate_estimates(&self) -> Result { - warn!("ScalarFeeRateEstimator::get_rate_estimates"); let sql = "SELECT high, middle, low FROM scalar_fee_estimator WHERE estimate_key = ?"; self.db .query_row(sql, &[SINGLETON_ROW_ID], |row| { diff --git a/src/cost_estimates/pessimistic.rs b/src/cost_estimates/pessimistic.rs index 8705fa21ba..3467ceef3d 100644 --- a/src/cost_estimates/pessimistic.rs +++ b/src/cost_estimates/pessimistic.rs @@ -259,7 +259,6 @@ impl CostEstimator for PessimisticEstimator { block_limit: &ExecutionCost, evaluated_epoch: &StacksEpochId, ) -> Result<(), EstimatorError> { - warn!("PessimisticEstimator::notify_event"); if self.log_error { // only log the estimate error if an estimate could be constructed if let Ok(estimated_cost) = self.estimate_cost(tx, evaluated_epoch) { @@ -283,9 +282,7 @@ impl CostEstimator for PessimisticEstimator { let sql_tx = tx_begin_immediate_sqlite(&mut self.db)?; for field in CostField::ALL.iter() { - warn!("PessimisticEstimator::notify_event:field"); let key = PessimisticEstimator::get_estimate_key(tx, field, evaluated_epoch); - warn!("estimate_cost:notify_event {}", &key); let field_cost = field.select_key(actual_cost); let mut current_sample = Samples::get_sqlite(&sql_tx, &key); current_sample.update_with(field_cost); @@ -300,40 +297,31 @@ impl CostEstimator for PessimisticEstimator { tx: &TransactionPayload, evaluated_epoch: &StacksEpochId, ) -> Result { - let runtime = - PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch); - warn!("estimate_cost:runtime {}", &runtime); - warn!("PessimisticEstimator::estimate_cost"); let runtime = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::RuntimeCost, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; - warn!("PessimisticEstimator"); let read_count = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::ReadCount, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; - warn!("PessimisticEstimator"); let read_length = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::ReadLength, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; - warn!("PessimisticEstimator"); let write_count = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::WriteCount, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; - warn!("PessimisticEstimator"); let write_length = Samples::get_estimate_sqlite( &self.db, &PessimisticEstimator::get_estimate_key(tx, &CostField::WriteLength, evaluated_epoch), ) .ok_or_else(|| EstimatorError::NoEstimateAvailable)?; - warn!("PessimisticEstimator"); Ok(ExecutionCost { runtime, diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 5c404cd2e8..3d038ae423 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1726,7 +1726,6 @@ impl ConversationHttp { tx: &TransactionPayload, estimated_len: u64, ) -> Result<(), net_error> { - warn!("handle_post_fee_rate_estimate"); let response_metadata = HttpResponseMetadata::from(req); let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())?; let stacks_epoch = SortitionDB::get_stacks_epoch(sortdb.conn(), tip.block_height)? @@ -1738,14 +1737,9 @@ impl ConversationHttp { net_error::ChainstateError("Could not load Stacks epoch for canonical burn height".into()) })?; if let Some((cost_estimator, fee_estimator, metric)) = handler_args.get_estimators_ref() { - warn!("handle_post_fee_rate_estimate"); let estimated_cost = match cost_estimator.estimate_cost(tx, &stacks_epoch.epoch_id) { - Ok(x) => { - warn!("handle_post_fee_rate_estimate"); - x - } + Ok(x) => x, Err(e) => { - warn!("handle_post_fee_rate_estimate"); // fail here debug!( "Estimator RPC endpoint failed to estimate tx: {}", tx.name() @@ -1755,16 +1749,11 @@ impl ConversationHttp { } }; - warn!("handle_post_fee_rate_estimate"); let scalar_cost = metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len); let fee_rates = match fee_estimator.get_rate_estimates() { - Ok(x) => { - warn!("handle_post_fee_rate_estimate"); - x - } + Ok(x) => x, Err(e) => { - warn!("handle_post_fee_rate_estimate"); debug!( "Estimator RPC endpoint failed to estimate fees for tx: {}", tx.name() diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 3dfc6f9473..4a27713391 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1183,16 +1183,12 @@ impl Config { pub fn make_fee_estimator(&self) -> Option> { let metric = self.make_cost_metric()?; - let estimator_name = self.estimation.fee_estimator.as_ref(); - warn!("estimator_name {:?}", estimator_name); let fee_estimator: Box = match self.estimation.fee_estimator.as_ref()? { FeeEstimatorName::ScalarFeeRate => { - warn!("ScalarFeeRate"); self.estimation .make_scalar_fee_estimator(self.get_chainstate_path(), metric) } FeeEstimatorName::FuzzedWeightedMedianFeeRate => { - warn!("FuzzedWeightedMedianFeeRate"); self.estimation .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric) } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 5a60aae3db..795c74af15 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -305,7 +305,6 @@ pub fn next_block_and_wait( btc_controller: &mut BitcoinRegtestController, blocks_processed: &Arc, ) -> bool { - info!("next_block_and_wait"); let current = blocks_processed.load(Ordering::SeqCst); eprintln!( "Issuing block at {}, waiting for bump ({})", @@ -5885,7 +5884,6 @@ fn run_until_burnchain_height( target_height: u64, conf: &Config, ) -> bool { - warn!("run_until_burnchain_height"); let tip_info = get_chain_info(&conf); let mut current_height = tip_info.burn_block_height; @@ -5907,31 +5905,6 @@ fn run_until_burnchain_height( true } -/// Deserializes the `StacksTransaction` objects from `blocks` and returns all those that -/// match `test_fn`. -fn select_transactions_where( - blocks: &Vec, - test_fn: fn(&StacksTransaction) -> bool, -) -> Vec { - let mut result = vec![]; - warn!("blocks.len() {:?}", blocks.len()); - for block in blocks { - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - warn!("transactions.len() {:?}", transactions.len()); - for tx in transactions.iter() { - // warn!("tx) {:?}", &tx); - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if test_fn(&parsed) { - result.push(parsed); - } - } - } - - return result; -} - #[test] #[ignore] fn fuzzed_median_fee_rate_estimation_test() { @@ -5959,8 +5932,6 @@ fn fuzzed_median_fee_rate_estimation_test() { "# .to_string(); - warn!("max_contract_src {:?}", &max_contract_src); - let spender_sk = StacksPrivateKey::new(); let spender_addr = to_addr(&spender_sk); @@ -6039,25 +6010,9 @@ fn fuzzed_median_fee_rate_estimation_test() { 212 + 2 * i, &conf, ); - // Check that we have processed the contract successfully, by checking that the contract call - // is in the block record. - let increment_calls_alice = select_transactions_where( - &test_observer::get_blocks(), - |transaction| match &transaction.payload { - TransactionPayload::ContractCall(contract) => { - contract.contract_name == ContractName::try_from("increment-contract").unwrap() - } - _ => false, - }, - ); - - warn!( - "increment_calls_alice.len() {}", - increment_calls_alice.len() - ); { - // perform some tests of the fee rate interface + // Read from the fee estimation endpoin. let path = format!("{}/v2/fees/transaction", &http_origin); let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); @@ -6071,8 +6026,6 @@ fn fuzzed_median_fee_rate_estimation_test() { let payload_data = tx_payload.serialize_to_vec(); let payload_hex = format!("0x{}", to_hex(&payload_data)); - eprintln!("Test: POST {}", path); - let body = json!({ "transaction_payload": payload_hex.clone() }); let client = reqwest::blocking::Client::new(); @@ -6086,14 +6039,11 @@ fn fuzzed_median_fee_rate_estimation_test() { .expect("Failed to parse result into JSON"); - warn!("fee_rate_result {} {:#?}", i, &fee_rate_result); response_estimated_costs.push(fee_rate_result.estimated_cost_scalar); response_top_fee_rates.push(fee_rate_result.estimations.last().unwrap().fee_rate); } } - warn!("response_estimated_costs, {:#?}", &response_estimated_costs); - warn!("response_top_fee_rates, {:#?}", &response_top_fee_rates); assert_eq!(response_estimated_costs.len(), response_top_fee_rates.len()); // Check that: From 03fd00f3d030c72fcd0b42a2e73b192cd835ddab Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 00:46:15 +0000 Subject: [PATCH 059/103] changed logic to support fractional fuzz instead of absolute --- src/cost_estimates/fee_rate_fuzzer.rs | 34 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index be9034072b..fceec8abde 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -21,8 +21,10 @@ pub struct FeeRateFuzzer { /// Creator function for a new random generator. For prod, use `thread_rng`. For test, /// pass in a contrived generator. rng_creator: Box Box>, - /// The bound used for the uniform random fuzz. - uniform_bound: f64, + /// The fuzzed rate will be `R * (1 + alpha)`, where `R` is the original rate, and `alpha` is a + /// random number in `[-uniform_fuzz_bound, uniform_fuzz_bound]`. + /// Note: Must be `0 < uniform_fuzz_bound < 1`. + uniform_fuzz_bound: f64, } impl FeeRateFuzzer { @@ -30,8 +32,9 @@ impl FeeRateFuzzer { /// to get truly pseudo-random numbers. pub fn new( underlying: UnderlyingEstimator, - uniform_bound: f64, + uniform_fuzz_bound: f64, ) -> FeeRateFuzzer { + assert!(0.0 < uniform_fuzz_bound && uniform_fuzz_bound < 1.0); let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r @@ -39,7 +42,7 @@ impl FeeRateFuzzer { Self { underlying, rng_creator, - uniform_bound, + uniform_fuzz_bound, } } @@ -48,23 +51,32 @@ impl FeeRateFuzzer { pub fn new_custom_creator( underlying: UnderlyingEstimator, rng_creator: Box Box>, - uniform_bound: f64, + uniform_fuzz_bound: f64, ) -> FeeRateFuzzer { + assert!(0.0 < uniform_fuzz_bound && uniform_fuzz_bound < 1.0); Self { underlying, rng_creator, - uniform_bound, + uniform_fuzz_bound, } } + /// Fuzzes an individual number. The fuzzed rate will be `R * (1 + alpha)`, where `R` is the + /// original rate, and `alpha` is a random number in `[-uniform_fuzz_bound, + /// uniform_fuzz_bound]`. + fn fuzz_individual_scalar(&self, original: f64) -> f64 { + let mut rng: Box = (self.rng_creator)(); + let uniform = Uniform::new(-self.uniform_fuzz_bound, self.uniform_fuzz_bound); + let fuzz_fraction = uniform.sample(&mut rng); + original * (1f64 + fuzz_fraction) + } + /// Add a uniform fuzz to input. fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { - let mut rng: Box = (self.rng_creator)(); - let normal = Uniform::new(-self.uniform_bound, self.uniform_bound); FeeRateEstimate { - high: input.high + normal.sample(&mut rng), - middle: input.middle + normal.sample(&mut rng), - low: input.low + normal.sample(&mut rng), + high: self.fuzz_individual_scalar(input.high), + middle: self.fuzz_individual_scalar(input.middle), + low: self.fuzz_individual_scalar(input.low), } } } From 2158bc8831e5591a77d9a4db4705155f3e5de0e8 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 00:57:12 +0000 Subject: [PATCH 060/103] fixed tests and added comment --- src/cost_estimates/fee_rate_fuzzer.rs | 2 +- src/cost_estimates/tests/fee_rate_fuzzer.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index fceec8abde..a7e942a9ef 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -11,7 +11,7 @@ use rand::SeedableRng; /// The FeeRateFuzzer wraps an underlying FeeEstimator. It passes `notify_block` calls to the /// underlying estimator. On `get_rate_estimates` calls, it adds a random fuzz to the result coming -/// back from the underlying estimator. +/// back from the underlying estimator. The fuzz applied is as a random fraction of the base value. /// /// Note: We currently use "uniform" random noise instead of "normal" distributed noise to avoid /// importing a new crate just for this. diff --git a/src/cost_estimates/tests/fee_rate_fuzzer.rs b/src/cost_estimates/tests/fee_rate_fuzzer.rs index 4fa5e24bbf..70f1fdbf30 100644 --- a/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -62,9 +62,9 @@ fn test_fuzzing_seed1() { .get_rate_estimates() .expect("Estimate should exist."), FeeRateEstimate { - high: 95.01268903765265f64, - middle: 49.93182838353776f64, - low: 4.921037454936614f64 + high: 96.20545857700169f64, + middle: 50.63445188263247f64, + low: 5.0634451882632465f64 } ); } @@ -86,9 +86,9 @@ fn test_fuzzing_seed2() { .get_rate_estimates() .expect("Estimate should exist."), FeeRateEstimate { - high: 95.05348553928201f64, - middle: 50.031434211372954f64, - low: 5.043648532116769f64 + high: 100.08112623179122f64, + middle: 52.67427696410064f64, + low: 5.267427696410064f64 } ); } @@ -145,9 +145,9 @@ fn test_notify_pass_through() { .get_rate_estimates() .expect("Estimate should exist."), FeeRateEstimate { - high: 2.053485539282013f64, - middle: 2.0314342113729524f64, - low: 2.0436485321167686f64 + high: 2.1069710785640257f64, + middle: 2.1069710785640257f64, + low: 2.1069710785640257f64 }, ); } From 74c5f8c256bd5bbfcca12fe0243f1fa03a76f0d3 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 15:24:32 +0000 Subject: [PATCH 061/103] added comment for fuzzed_median_fee_rate_estimation_test --- testnet/stacks-node/src/tests/neon_integrations.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 795c74af15..2126e323b5 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5905,6 +5905,12 @@ fn run_until_burnchain_height( true } +/// This is an integration test for the "fuzzed_weighted_median_fee_rate" estimation. It tests that +/// 1) We can load up the "fuzzed_weighted_median_fee_rate" fee estimator. +/// 2) We get results that look "somewhat right", but we can't expect exact values due to +/// pseudo-random fuzz. +/// We test that, if fees are increasing every block round for the same transaction, that we +/// expect to see the fee rate estimates rise on every round, modulo the random noise. #[test] #[ignore] fn fuzzed_median_fee_rate_estimation_test() { @@ -6064,4 +6070,3 @@ fn fuzzed_median_fee_rate_estimation_test() { channel.stop_chains_coordinator(); } - From 619720027164e38d928fb96fed68dcc8e358b55c Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 15:33:05 +0000 Subject: [PATCH 062/103] added contract --- testnet/stacks-node/src/tests/neon_integrations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 2126e323b5..e24cbfb168 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5978,11 +5978,9 @@ fn fuzzed_median_fee_rate_estimation_test() { thread::spawn(move || run_loop.start(None, 0)); - // give the run loop some time to start up! wait_for_runloop(&blocks_processed); run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 210, &conf); - // Publish the contract so we can use it. submit_tx( &http_origin, &make_contract_publish( @@ -5995,6 +5993,8 @@ fn fuzzed_median_fee_rate_estimation_test() { ); run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); + // Loop 10 times. Each time, execute the same transaction, but increase the amount *paid*. + // We will then check that the fee rates always go up, but the cost stays the same. let mut response_estimated_costs = vec![]; let mut response_top_fee_rates = vec![]; for i in 1..11 { From 54aa0cf5fc253f59dc52b31918d8cab9e120ddf4 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 17 Dec 2021 16:50:41 +0000 Subject: [PATCH 063/103] ran fmt --- testnet/stacks-node/src/config.rs | 14 ++++++-------- testnet/stacks-node/src/tests/neon_integrations.rs | 3 +-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 4a27713391..8424eb62e2 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1184,14 +1184,12 @@ impl Config { pub fn make_fee_estimator(&self) -> Option> { let metric = self.make_cost_metric()?; let fee_estimator: Box = match self.estimation.fee_estimator.as_ref()? { - FeeEstimatorName::ScalarFeeRate => { - self.estimation - .make_scalar_fee_estimator(self.get_chainstate_path(), metric) - } - FeeEstimatorName::FuzzedWeightedMedianFeeRate => { - self.estimation - .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric) - } + FeeEstimatorName::ScalarFeeRate => self + .estimation + .make_scalar_fee_estimator(self.get_chainstate_path(), metric), + FeeEstimatorName::FuzzedWeightedMedianFeeRate => self + .estimation + .make_fuzzed_weighted_median_fee_estimator(self.get_chainstate_path(), metric), }; Some(fee_estimator) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index e24cbfb168..efb3aac686 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -74,10 +74,10 @@ use super::{ }; use crate::config::FeeEstimatorName; +use stacks::net::RPCFeeEstimateResponse; use stacks::vm::ClarityName; use stacks::vm::ContractName; use std::convert::TryFrom; -use stacks::net::RPCFeeEstimateResponse; pub fn neon_integration_test_conf() -> (Config, StacksAddress) { let mut conf = super::new_test_conf(); @@ -6044,7 +6044,6 @@ fn fuzzed_median_fee_rate_estimation_test() { .json::() .expect("Failed to parse result into JSON"); - response_estimated_costs.push(fee_rate_result.estimated_cost_scalar); response_top_fee_rates.push(fee_rate_result.estimations.last().unwrap().fee_rate); } From 55160dae86a1f0b46cf31eb869a0aafc71ef811d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 15:48:40 +0000 Subject: [PATCH 064/103] remove bad comment --- testnet/stacks-node/src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 8424eb62e2..9a3be7bb30 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -44,7 +44,6 @@ pub struct ConfigFile { pub ustx_balance: Option>, pub events_observer: Option>, pub connection_options: Option, - // Note: put the name here pub fee_estimation: Option, pub miner: Option, } From dbd1613ec005ea3653b45eef4e4f2a45a9499bcc Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 18:03:43 +0000 Subject: [PATCH 065/103] remove extra comment --- testnet/stacks-node/src/tests/neon_integrations.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index efb3aac686..e43c7a463a 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6040,7 +6040,6 @@ fn fuzzed_median_fee_rate_estimation_test() { .json(&body) .send() .expect("Should be able to post") - // .json::() .json::() .expect("Failed to parse result into JSON"); From a53f48a8155c2ba3572bd56312a35c967a4852e5 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 18:04:02 +0000 Subject: [PATCH 066/103] fuzz each eleement by the same factor --- src/cost_estimates/fee_rate_fuzzer.rs | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index a7e942a9ef..6ab9a5dc4c 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -61,23 +61,12 @@ impl FeeRateFuzzer { } } - /// Fuzzes an individual number. The fuzzed rate will be `R * (1 + alpha)`, where `R` is the - /// original rate, and `alpha` is a random number in `[-uniform_fuzz_bound, - /// uniform_fuzz_bound]`. - fn fuzz_individual_scalar(&self, original: f64) -> f64 { - let mut rng: Box = (self.rng_creator)(); + /// Add a uniform fuzz to input. Each element is multiplied by the same random factor. + fn fuzz_estimate(&self, input: FeeRateEstimate) -> FeeRateEstimate { + let mut rng = (self.rng_creator)(); let uniform = Uniform::new(-self.uniform_fuzz_bound, self.uniform_fuzz_bound); - let fuzz_fraction = uniform.sample(&mut rng); - original * (1f64 + fuzz_fraction) - } - - /// Add a uniform fuzz to input. - fn fuzz_estimate(&self, input: &FeeRateEstimate) -> FeeRateEstimate { - FeeRateEstimate { - high: self.fuzz_individual_scalar(input.high), - middle: self.fuzz_individual_scalar(input.middle), - low: self.fuzz_individual_scalar(input.low), - } + let fuzz_scale = 1f64 + uniform.sample(&mut rng); + input * fuzz_scale } } @@ -93,9 +82,7 @@ impl FeeEstimator for FeeRateFuzzer { /// Call underlying estimator and add some fuzz. fn get_rate_estimates(&self) -> Result { - match self.underlying.get_rate_estimates() { - Ok(underlying_estimate) => Ok(self.fuzz_estimate(&underlying_estimate)), - Err(e) => Err(e), - } + let underlying_estimate = self.underlying.get_rate_estimates()?; + Ok(self.fuzz_estimate(underlying_estimate)) } } From 582641766c70c40cdd9e2b7d9b94181210284ad4 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 18:19:21 +0000 Subject: [PATCH 067/103] added tests to yml file --- .github/workflows/bitcoin-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index efbfa70ae7..6e58d73850 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -53,6 +53,7 @@ jobs: - tests::neon_integrations::filter_low_fee_tx_integration_test - tests::neon_integrations::filter_long_runtime_tx_integration_test - tests::neon_integrations::mining_transactions_is_fair + - tests::neon_integrations::fuzzed_median_fee_rate_estimation_test - tests::epoch_205::test_dynamic_db_method_costs - tests::epoch_205::transition_empty_blocks - tests::epoch_205::test_cost_limit_switch_version205 From 3c93a6c831dffe2190b052a27293b9ce80c969ca Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 18:24:32 +0000 Subject: [PATCH 068/103] remove new estimator from the toml file; wait till we add it to all of them --- testnet/stacks-node/conf/mainnet-follower-conf.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/testnet/stacks-node/conf/mainnet-follower-conf.toml b/testnet/stacks-node/conf/mainnet-follower-conf.toml index 7dd81ee89f..f6b283e38e 100644 --- a/testnet/stacks-node/conf/mainnet-follower-conf.toml +++ b/testnet/stacks-node/conf/mainnet-follower-conf.toml @@ -13,9 +13,6 @@ password = "blockstacksystem" rpc_port = 8332 peer_port = 8333 -[fee_estimation] -fee_estimator = "fuzzed_weighted_median_fee_rate" - # Used for sending events to a local stacks-blockchain-api service # [[events_observer]] # endpoint = "localhost:3700" From e9afd2d27548fb431f7a208bae370fc7f8de1074 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 18:49:40 +0000 Subject: [PATCH 069/103] remove make_block_receipt --- src/cost_estimates/tests/cost_estimators.rs | 36 +-------------------- src/cost_estimates/tests/fee_medians.rs | 36 +-------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/src/cost_estimates/tests/cost_estimators.rs b/src/cost_estimates/tests/cost_estimators.rs index d6e7203cd3..d47173893f 100644 --- a/src/cost_estimates/tests/cost_estimators.rs +++ b/src/cost_estimates/tests/cost_estimators.rs @@ -29,6 +29,7 @@ use crate::types::chainstate::StacksAddress; use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; use core::BLOCK_LIMIT_MAINNET_20; +use cost_estimates::tests::common::*; fn instantiate_test_db() -> PessimisticEstimator { let mut path = env::temp_dir(); @@ -73,41 +74,6 @@ fn test_empty_pessimistic_estimator() { ); } -fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { - StacksEpochReceipt { - header: StacksHeaderInfo { - anchored_header: StacksBlockHeader { - version: 1, - total_work: StacksWorkScore { burn: 1, work: 1 }, - proof: VRFProof::empty(), - parent_block: BlockHeaderHash([0; 32]), - parent_microblock: BlockHeaderHash([0; 32]), - parent_microblock_sequence: 0, - tx_merkle_root: Sha512Trunc256Sum([0; 32]), - state_index_root: TrieHash([0; 32]), - microblock_pubkey_hash: Hash160([0; 20]), - }, - microblock_tail: None, - block_height: 1, - index_root: TrieHash([0; 32]), - consensus_hash: ConsensusHash([2; 20]), - burn_header_hash: BurnchainHeaderHash([1; 32]), - burn_header_height: 2, - burn_header_timestamp: 2, - anchored_block_size: 1, - }, - tx_receipts, - matured_rewards: vec![], - matured_rewards_info: None, - parent_microblocks_cost: ExecutionCost::zero(), - anchored_block_cost: ExecutionCost::zero(), - parent_burn_block_hash: BurnchainHeaderHash([0; 32]), - parent_burn_block_height: 1, - parent_burn_block_timestamp: 1, - evaluated_epoch: StacksEpochId::Epoch20, - } -} - fn make_dummy_coinbase_tx() -> StacksTransactionReceipt { StacksTransactionReceipt::from_coinbase(StacksTransaction::new( TransactionVersion::Mainnet, diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 06ce24639a..aeafcffc60 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -29,6 +29,7 @@ use crate::vm::types::{PrincipalData, StandardPrincipalData}; use crate::vm::Value; use cost_estimates::fee_medians::fee_rate_estimate_from_sorted_weighted_fees; use cost_estimates::fee_medians::FeeRateAndWeight; +use cost_estimates::tests::common::*; /// Tolerance for approximate comparison. const error_epsilon: f64 = 0.1; @@ -55,41 +56,6 @@ fn instantiate_test_db(m: CM) -> WeightedMedianFeeRateEstimator< .expect("Test failure: could not open fee rate DB") } -fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { - StacksEpochReceipt { - header: StacksHeaderInfo { - anchored_header: StacksBlockHeader { - version: 1, - total_work: StacksWorkScore { burn: 1, work: 1 }, - proof: VRFProof::empty(), - parent_block: BlockHeaderHash([0; 32]), - parent_microblock: BlockHeaderHash([0; 32]), - parent_microblock_sequence: 0, - tx_merkle_root: Sha512Trunc256Sum([0; 32]), - state_index_root: TrieHash([0; 32]), - microblock_pubkey_hash: Hash160([0; 20]), - }, - microblock_tail: None, - block_height: 1, - index_root: TrieHash([0; 32]), - consensus_hash: ConsensusHash([2; 20]), - burn_header_hash: BurnchainHeaderHash([1; 32]), - burn_header_height: 2, - burn_header_timestamp: 2, - anchored_block_size: 1, - }, - tx_receipts, - matured_rewards: vec![], - matured_rewards_info: None, - parent_microblocks_cost: ExecutionCost::zero(), - anchored_block_cost: ExecutionCost::zero(), - parent_burn_block_hash: BurnchainHeaderHash([0; 32]), - parent_burn_block_height: 1, - parent_burn_block_timestamp: 1, - evaluated_epoch: StacksEpochId::Epoch20, - } -} - fn make_dummy_coinbase_tx() -> StacksTransaction { StacksTransaction::new( TransactionVersion::Mainnet, From 0ad5063cb7634064354b898a0a38fac87349ac6d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 3 Jan 2022 19:40:34 +0000 Subject: [PATCH 070/103] replaced 0.5 with 0.1 as the fraction, changed variable name --- src/cost_estimates/fee_rate_fuzzer.rs | 20 ++++++++++---------- testnet/stacks-node/src/config.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 6ab9a5dc4c..460d71865e 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -22,9 +22,9 @@ pub struct FeeRateFuzzer { /// pass in a contrived generator. rng_creator: Box Box>, /// The fuzzed rate will be `R * (1 + alpha)`, where `R` is the original rate, and `alpha` is a - /// random number in `[-uniform_fuzz_bound, uniform_fuzz_bound]`. - /// Note: Must be `0 < uniform_fuzz_bound < 1`. - uniform_fuzz_bound: f64, + /// random number in `[-uniform_fuzz_fraction, uniform_fuzz_fraction]`. + /// Note: Must be `0 < uniform_fuzz_fraction < 1`. + uniform_fuzz_fraction: f64, } impl FeeRateFuzzer { @@ -32,9 +32,9 @@ impl FeeRateFuzzer { /// to get truly pseudo-random numbers. pub fn new( underlying: UnderlyingEstimator, - uniform_fuzz_bound: f64, + uniform_fuzz_fraction: f64, ) -> FeeRateFuzzer { - assert!(0.0 < uniform_fuzz_bound && uniform_fuzz_bound < 1.0); + assert!(0.0 < uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r @@ -42,7 +42,7 @@ impl FeeRateFuzzer { Self { underlying, rng_creator, - uniform_fuzz_bound, + uniform_fuzz_fraction, } } @@ -51,20 +51,20 @@ impl FeeRateFuzzer { pub fn new_custom_creator( underlying: UnderlyingEstimator, rng_creator: Box Box>, - uniform_fuzz_bound: f64, + uniform_fuzz_fraction: f64, ) -> FeeRateFuzzer { - assert!(0.0 < uniform_fuzz_bound && uniform_fuzz_bound < 1.0); + assert!(0.0 < uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); Self { underlying, rng_creator, - uniform_fuzz_bound, + uniform_fuzz_fraction, } } /// Add a uniform fuzz to input. Each element is multiplied by the same random factor. fn fuzz_estimate(&self, input: FeeRateEstimate) -> FeeRateEstimate { let mut rng = (self.rng_creator)(); - let uniform = Uniform::new(-self.uniform_fuzz_bound, self.uniform_fuzz_bound); + let uniform = Uniform::new(-self.uniform_fuzz_fraction, self.uniform_fuzz_fraction); let fuzz_scale = 1f64 + uniform.sample(&mut rng); input * fuzz_scale } diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 9a3be7bb30..1160b81833 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1237,7 +1237,7 @@ impl FeeEstimationConfig { let underlying_estimator = WeightedMedianFeeRateEstimator::open(&chainstate_path, metric, 5) .expect("Error opening fee estimator"); - Box::new(FeeRateFuzzer::new(underlying_estimator, 0.5)) + Box::new(FeeRateFuzzer::new(underlying_estimator, 0.1)) } else { panic!("BUG: Expected to configure a weighted median fee estimator"); } From 94f2c90ddc7cd3ffc74b5b80df9e88edbc434269 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 4 Jan 2022 16:24:28 +0000 Subject: [PATCH 071/103] mark make_block_receipt as for test --- src/cost_estimates/tests/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cost_estimates/tests/common.rs b/src/cost_estimates/tests/common.rs index 63d0a02d05..1b529c8d9a 100644 --- a/src/cost_estimates/tests/common.rs +++ b/src/cost_estimates/tests/common.rs @@ -14,6 +14,7 @@ use crate::chainstate::stacks::{ use crate::core::StacksEpochId; /// Make a block receipt from `tx_receipts` with some dummy values filled for test. +#[cfg(test)] pub fn make_block_receipt(tx_receipts: Vec) -> StacksEpochReceipt { StacksEpochReceipt { header: StacksHeaderInfo { From b77f5a8179724adc4f6c7a1c50cc3a7503ca4a69 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 4 Jan 2022 16:27:01 +0000 Subject: [PATCH 072/103] add constant MINIMUM_TX_FEE_RATE --- src/cost_estimates/fee_medians.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 9d93a9eb53..f104eb583d 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -39,6 +39,8 @@ CREATE TABLE median_fee_estimator ( low NUMBER NOT NULL )"; +const MINIMUM_TX_FEE_RATE:f64 = 1f64; + /// FeeRateEstimator with the following properties: /// /// 1) We use a "weighted" percentile approach for calculating the percentile values. Described @@ -299,7 +301,7 @@ fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_bl if total_weight < full_block_weight { let weight_remaining = full_block_weight - total_weight; working_rates.push(FeeRateAndWeight { - fee_rate: 1f64, + fee_rate: MINIMUM_TX_FEE_RATE, weight: weight_remaining, }) } @@ -335,17 +337,17 @@ fn fee_rate_and_weight_from_receipt( let denominator = if scalar_cost >= 1 { scalar_cost as f64 } else { - 1f64 + MINIMUM_TX_FEE_RATE }; let fee_rate = fee as f64 / denominator; - if fee_rate >= 1f64 && fee_rate.is_finite() { + if fee_rate >= MINIMUM_TX_FEE_RATE && fee_rate.is_finite() { Some(FeeRateAndWeight { fee_rate, weight: scalar_cost, }) } else { Some(FeeRateAndWeight { - fee_rate: 1f64, + fee_rate: MINIMUM_TX_FEE_RATE, weight: scalar_cost, }) } From b76585d84615ff1d89722ce4b0cd702dbe018ee7 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 4 Jan 2022 16:33:52 +0000 Subject: [PATCH 073/103] panic if weight is 0f64 --- src/cost_estimates/fee_medians.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index f104eb583d..5101423625 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -248,6 +248,11 @@ pub fn fee_rate_estimate_from_sorted_weighted_fees( for rate_and_weight in sorted_fee_rates { total_weight += rate_and_weight.weight as f64; } + + if total_weight <= 0f64 { + panic!("`total_weight` is 0f64. This is an error."); + } + let mut cumulative_weight = 0f64; let mut percentiles = Vec::new(); for rate_and_weight in sorted_fee_rates { From b0453029e4354bc20ffdfbc990f650dcdf0cfad1 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 4 Jan 2022 21:54:04 +0000 Subject: [PATCH 074/103] added changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed63bec8c9..e3274a15f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [Upcoming] + +### Changed + +- A new fee estimator intended to produce fewer over-estimates, by having less + sensitivity to outliers. Its characteristic features are: 1) use a window to + forget past estimates instead of exponential averaging, 2) use weighted + percentiles, so that bigger transactions influence the estimates more, 3) + assess empty blocks as having paid the "minimum fee", so that empty space is + accounted for, 4) use random "fuzz" so that in busy times we will not have + ties. We will now default to this estimator. + ## [2.05.0.0.0] This software update is a consensus changing release and the From eca6e0169927fd31dcd122bb31bc7b302ad8f6b2 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 18:16:22 +0000 Subject: [PATCH 075/103] added fuzzer options to config structs --- testnet/stacks-node/src/config.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 1160b81833..3e5da41666 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1112,6 +1112,12 @@ pub struct FeeEstimationConfig { pub fee_estimator: Option, pub cost_metric: Option, pub log_error: bool, + /// If using FeeRateFuzzer, the amount of random noise, as a percentage of the base value (in + /// [0, 1]) to add for fuzz. See comments on FeeRateFuzzer. + pub fee_rate_fuzzer_fraction: f64, + /// If using WeightedMedianFeeRateEstimator, the window size to use. See comments on + /// WeightedMedianFeeRateEstimator. + pub fee_rate_window_size: u64, } impl Default for FeeEstimationConfig { @@ -1121,6 +1127,8 @@ impl Default for FeeEstimationConfig { fee_estimator: Some(FeeEstimatorName::default()), cost_metric: Some(CostMetricName::default()), log_error: false, + fee_rate_fuzzer_fraction: 0.1f64, + fee_rate_window_size: 5u64, } } } @@ -1133,6 +1141,8 @@ impl From for FeeEstimationConfig { fee_estimator: None, cost_metric: None, log_error: false, + fee_rate_fuzzer_fraction: 0f64, + fee_rate_window_size: 0u64, }; } let cost_estimator = f @@ -1153,6 +1163,8 @@ impl From for FeeEstimationConfig { fee_estimator: Some(fee_estimator), cost_metric: Some(cost_metric), log_error, + fee_rate_fuzzer_fraction: f.fee_rate_fuzzer_fraction, + fee_rate_window_size: f.fee_rate_window_size, } } } @@ -1454,6 +1466,8 @@ pub struct FeeEstimationConfigFile { pub cost_metric: Option, pub disabled: Option, pub log_error: Option, + pub fee_rate_fuzzer_fraction: f64, + pub fee_rate_window_size: u64, } impl Default for FeeEstimationConfigFile { @@ -1464,6 +1478,8 @@ impl Default for FeeEstimationConfigFile { cost_metric: None, disabled: None, log_error: None, + fee_rate_fuzzer_fraction: 0f64, + fee_rate_window_size: 0u64, } } } From 2ceffad3de25ddffd5aed3d98de3cc9fd139b0c3 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 18:26:21 +0000 Subject: [PATCH 076/103] added the config options so they are used now --- src/cost_estimates/fee_medians.rs | 2 +- testnet/stacks-node/src/config.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 5101423625..d47d2bb0c2 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -39,7 +39,7 @@ CREATE TABLE median_fee_estimator ( low NUMBER NOT NULL )"; -const MINIMUM_TX_FEE_RATE:f64 = 1f64; +const MINIMUM_TX_FEE_RATE: f64 = 1f64; /// FeeRateEstimator with the following properties: /// diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 3e5da41666..43ad7b13d4 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1246,10 +1246,16 @@ impl FeeEstimationConfig { ) -> Box { if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() { chainstate_path.push("fee_fuzzed_weighted_median.sqlite"); - let underlying_estimator = - WeightedMedianFeeRateEstimator::open(&chainstate_path, metric, 5) - .expect("Error opening fee estimator"); - Box::new(FeeRateFuzzer::new(underlying_estimator, 0.1)) + let underlying_estimator = WeightedMedianFeeRateEstimator::open( + &chainstate_path, + metric, + self.fee_rate_window_size.try_into().unwrap(), + ) + .expect("Error opening fee estimator"); + Box::new(FeeRateFuzzer::new( + underlying_estimator, + self.fee_rate_fuzzer_fraction, + )) } else { panic!("BUG: Expected to configure a weighted median fee estimator"); } From 9f03d8c3343d46fe3b318e5b25bc0093381fbee9 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 19:05:06 +0000 Subject: [PATCH 077/103] updated integration test to use parameters --- src/cost_estimates/fee_rate_fuzzer.rs | 18 ++++--- .../src/tests/neon_integrations.rs | 49 +++++++++++++------ 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/cost_estimates/fee_rate_fuzzer.rs b/src/cost_estimates/fee_rate_fuzzer.rs index 460d71865e..2a54ad0d05 100644 --- a/src/cost_estimates/fee_rate_fuzzer.rs +++ b/src/cost_estimates/fee_rate_fuzzer.rs @@ -23,7 +23,7 @@ pub struct FeeRateFuzzer { rng_creator: Box Box>, /// The fuzzed rate will be `R * (1 + alpha)`, where `R` is the original rate, and `alpha` is a /// random number in `[-uniform_fuzz_fraction, uniform_fuzz_fraction]`. - /// Note: Must be `0 < uniform_fuzz_fraction < 1`. + /// Note: Must be `0 <= uniform_fuzz_fraction < 1`. uniform_fuzz_fraction: f64, } @@ -34,7 +34,7 @@ impl FeeRateFuzzer { underlying: UnderlyingEstimator, uniform_fuzz_fraction: f64, ) -> FeeRateFuzzer { - assert!(0.0 < uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); + assert!(0.0 <= uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); let rng_creator = Box::new(|| { let r: Box = Box::new(thread_rng()); r @@ -53,7 +53,7 @@ impl FeeRateFuzzer { rng_creator: Box Box>, uniform_fuzz_fraction: f64, ) -> FeeRateFuzzer { - assert!(0.0 < uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); + assert!(0.0 <= uniform_fuzz_fraction && uniform_fuzz_fraction < 1.0); Self { underlying, rng_creator, @@ -63,10 +63,14 @@ impl FeeRateFuzzer { /// Add a uniform fuzz to input. Each element is multiplied by the same random factor. fn fuzz_estimate(&self, input: FeeRateEstimate) -> FeeRateEstimate { - let mut rng = (self.rng_creator)(); - let uniform = Uniform::new(-self.uniform_fuzz_fraction, self.uniform_fuzz_fraction); - let fuzz_scale = 1f64 + uniform.sample(&mut rng); - input * fuzz_scale + if self.uniform_fuzz_fraction > 0f64 { + let mut rng = (self.rng_creator)(); + let uniform = Uniform::new(-self.uniform_fuzz_fraction, self.uniform_fuzz_fraction); + let fuzz_scale = 1f64 + uniform.sample(&mut rng); + input * fuzz_scale + } else { + input + } } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 2578d776ea..9fbe507697 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5937,15 +5937,11 @@ fn run_until_burnchain_height( true } -/// This is an integration test for the "fuzzed_weighted_median_fee_rate" estimation. It tests that -/// 1) We can load up the "fuzzed_weighted_median_fee_rate" fee estimator. -/// 2) We get results that look "somewhat right", but we can't expect exact values due to -/// pseudo-random fuzz. -/// We test that, if fees are increasing every block round for the same transaction, that we -/// expect to see the fee rate estimates rise on every round, modulo the random noise. -#[test] -#[ignore] -fn fuzzed_median_fee_rate_estimation_test() { +/// Run a fixed contract 20 times. Linearly increase the amount paid each time. The cost of the +/// contract should stay the same, and the fee rate paid should monotonically grow. The value +/// should grow faster for lower values of `window_size`, because a bigger window slows down the +/// growth. +fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value: f64) { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -5979,6 +5975,9 @@ fn fuzzed_median_fee_rate_estimation_test() { // Set this estimator as special. conf.estimation.fee_estimator = Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate); + // Use randomness of 0 to keep test constant. Randomness is tested in unit tests. + conf.estimation.fee_rate_fuzzer_fraction = 0f64; + conf.estimation.fee_rate_window_size = window_size; conf.initial_balances.push(InitialBalance { address: spender_addr.clone().into(), @@ -6025,11 +6024,11 @@ fn fuzzed_median_fee_rate_estimation_test() { ); run_until_burnchain_height(&mut btc_regtest_controller, &blocks_processed, 212, &conf); - // Loop 10 times. Each time, execute the same transaction, but increase the amount *paid*. - // We will then check that the fee rates always go up, but the cost stays the same. + // Loop 20 times. Each time, execute the same transaction, but increase the amount *paid*. + // This will exercise the window size. let mut response_estimated_costs = vec![]; let mut response_top_fee_rates = vec![]; - for i in 1..11 { + for i in 1..21 { submit_tx( &http_origin, &make_contract_call( @@ -6084,13 +6083,12 @@ fn fuzzed_median_fee_rate_estimation_test() { // Check that: // 1) The cost is always the same. - // 2) The estimated fee rate only either i) moves up, ii) stays within random noise of where it - // was. + // 2) Only grows. for i in 1..response_estimated_costs.len() { let curr_rate = response_top_fee_rates[i] as f64; let last_rate = response_top_fee_rates[i - 1] as f64; - // Check that either 1) curr_rate is bigger, or 2) is only smaller by random 11% noise. - assert!(curr_rate - last_rate > -1.0 * curr_rate * 0.11); + // Check that the rate is growing monotonically. + assert!(curr_rate >= last_rate); // The cost is always the same. let curr_cost = response_estimated_costs[i]; @@ -6098,9 +6096,28 @@ fn fuzzed_median_fee_rate_estimation_test() { assert_eq!(curr_cost, last_cost); } + // Check the final value is "near" what was expected. + assert!(response_top_fee_rates.last().unwrap() - expected_final_value < 0.1); + channel.stop_chains_coordinator(); } +/// Test the FuzzedWeightedMedianFeeRate with window size 5 and randomness 0. We increase the +/// amount paid linearly each time. This estimate should grow faster than with window size 10. +#[test] +#[ignore] +fn fuzzed_median_fee_rate_estimation_test_window5() { + fuzzed_median_fee_rate_estimation_test(5, 202680.0992) +} + +/// Test the FuzzedWeightedMedianFeeRate with window size 10 and randomness 0. We increase the +/// amount paid linearly each time. This estimate should grow faster than with window size 5. +#[test] +#[ignore] +fn fuzzed_median_fee_rate_estimation_test_window10() { + fuzzed_median_fee_rate_estimation_test(10, 90080.5496) +} + #[test] #[ignore] fn use_latest_tip_integration_test() { From 913f03e31d217e4f97bb209b18f961aff79b3f84 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 19:11:59 +0000 Subject: [PATCH 078/103] added to the README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 070926e428..13e753a2c6 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,9 @@ Fee and cost estimators can be configure via the config section `[fee_estimation ``` [fee_estimation] cost_estimator = naive_pessimistic -fee_estimator = scalar_fee_rate +fee_estimator = fuzzed_weighted_median_fee_rate +fee_rate_fuzzer_fraction = 0.1 +fee_rate_window_size = 5 cost_metric = proportion_dot_product log_error = true enabled = true @@ -378,6 +380,11 @@ are **not** consensus-critical components, but rather can be used by miners to rank transactions in the mempool or client to determine appropriate fee rates for transactions before broadcasting them. +The default estimator is now `fuzzed_weighted_median_fee_rate`. This uses a +median estimate from a window the past `fee_rate_window_size` fees paid. +Estimates are then randomly "fuzzed" using uniform random fuzz of size up to +`fee_rate_fuzzer_fraction` of the base estimate. + ## Non-Consensus Breaking Release Process For non-consensus breaking releases, this project uses the following release process: From 3cad19ec6f423650a46abd7fa2a81fdae8ea2d28 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 19:16:13 +0000 Subject: [PATCH 079/103] fixed the names in bitcoin-tests --- .github/workflows/bitcoin-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index cd1c51206b..c604224dd4 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -53,7 +53,8 @@ jobs: - tests::neon_integrations::filter_low_fee_tx_integration_test - tests::neon_integrations::filter_long_runtime_tx_integration_test - tests::neon_integrations::mining_transactions_is_fair - - tests::neon_integrations::fuzzed_median_fee_rate_estimation_test + - tests::neon_integrations::fuzzed_median_fee_rate_estimation_test_window5 + - tests::neon_integrations::fuzzed_median_fee_rate_estimation_test_window10 - tests::neon_integrations::use_latest_tip_integration_test - tests::epoch_205::test_dynamic_db_method_costs - tests::epoch_205::transition_empty_blocks From c8e9bc438264f5d7a142f1ce38234627da02c0d6 Mon Sep 17 00:00:00 2001 From: Gregory Coppola <60008382+gregorycoppola@users.noreply.github.com> Date: Wed, 5 Jan 2022 14:27:37 -0500 Subject: [PATCH 080/103] Update README.md Co-authored-by: Aaron Blankstein --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13e753a2c6..31b35b7882 100644 --- a/README.md +++ b/README.md @@ -381,7 +381,7 @@ rank transactions in the mempool or client to determine appropriate fee rates for transactions before broadcasting them. The default estimator is now `fuzzed_weighted_median_fee_rate`. This uses a -median estimate from a window the past `fee_rate_window_size` fees paid. +median estimate from a window of the fees paid in the last `fee_rate_window_size` blocks. Estimates are then randomly "fuzzed" using uniform random fuzz of size up to `fee_rate_fuzzer_fraction` of the base estimate. From ba534b3814a0347e8c3a13ca92115259de3b2d1d Mon Sep 17 00:00:00 2001 From: Gregory Coppola <60008382+gregorycoppola@users.noreply.github.com> Date: Wed, 5 Jan 2022 14:27:58 -0500 Subject: [PATCH 081/103] Update README.md Co-authored-by: Aaron Blankstein --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 31b35b7882..24aa01b475 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ are **not** consensus-critical components, but rather can be used by miners to rank transactions in the mempool or client to determine appropriate fee rates for transactions before broadcasting them. -The default estimator is now `fuzzed_weighted_median_fee_rate`. This uses a +The `fuzzed_weighted_median_fee_rate` uses a median estimate from a window of the fees paid in the last `fee_rate_window_size` blocks. Estimates are then randomly "fuzzed" using uniform random fuzz of size up to `fee_rate_fuzzer_fraction` of the base estimate. From c22ba1e5bcf329dbcaba264d2a3d3975c16d56db Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 19:28:48 +0000 Subject: [PATCH 082/103] remove "now defaulting to this" in CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3274a15f9..26b45a7d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ## [Upcoming] -### Changed +### Added - A new fee estimator intended to produce fewer over-estimates, by having less sensitivity to outliers. Its characteristic features are: 1) use a window to @@ -15,7 +15,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE percentiles, so that bigger transactions influence the estimates more, 3) assess empty blocks as having paid the "minimum fee", so that empty space is accounted for, 4) use random "fuzz" so that in busy times we will not have - ties. We will now default to this estimator. + ties. ## [2.05.0.0.0] From 0332ececdc3f6cc93d7ca0a492e1e497fadc13d5 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 19:30:48 +0000 Subject: [PATCH 083/103] use expect not unwrap --- testnet/stacks-node/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 43ad7b13d4..7f3edcca51 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1249,7 +1249,7 @@ impl FeeEstimationConfig { let underlying_estimator = WeightedMedianFeeRateEstimator::open( &chainstate_path, metric, - self.fee_rate_window_size.try_into().unwrap(), + self.fee_rate_window_size.try_into().expect("Configured fee rate window size out of bounds."), ) .expect("Error opening fee estimator"); Box::new(FeeRateFuzzer::new( From 281ed4c3ed09a19b2bb628366329b9d6720262ee Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 20:02:19 +0000 Subject: [PATCH 084/103] changed config options to be Option's --- testnet/stacks-node/src/config.rs | 28 +++++++++++-------- .../src/tests/neon_integrations.rs | 4 +-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 7f3edcca51..dc3fcca142 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1114,10 +1114,10 @@ pub struct FeeEstimationConfig { pub log_error: bool, /// If using FeeRateFuzzer, the amount of random noise, as a percentage of the base value (in /// [0, 1]) to add for fuzz. See comments on FeeRateFuzzer. - pub fee_rate_fuzzer_fraction: f64, + pub fee_rate_fuzzer_fraction: Option, /// If using WeightedMedianFeeRateEstimator, the window size to use. See comments on /// WeightedMedianFeeRateEstimator. - pub fee_rate_window_size: u64, + pub fee_rate_window_size: Option, } impl Default for FeeEstimationConfig { @@ -1127,8 +1127,8 @@ impl Default for FeeEstimationConfig { fee_estimator: Some(FeeEstimatorName::default()), cost_metric: Some(CostMetricName::default()), log_error: false, - fee_rate_fuzzer_fraction: 0.1f64, - fee_rate_window_size: 5u64, + fee_rate_fuzzer_fraction: Some(0.1f64), + fee_rate_window_size: Some(5u64), } } } @@ -1141,8 +1141,8 @@ impl From for FeeEstimationConfig { fee_estimator: None, cost_metric: None, log_error: false, - fee_rate_fuzzer_fraction: 0f64, - fee_rate_window_size: 0u64, + fee_rate_fuzzer_fraction: None, + fee_rate_window_size: None, }; } let cost_estimator = f @@ -1249,12 +1249,16 @@ impl FeeEstimationConfig { let underlying_estimator = WeightedMedianFeeRateEstimator::open( &chainstate_path, metric, - self.fee_rate_window_size.try_into().expect("Configured fee rate window size out of bounds."), + self.fee_rate_window_size + .expect("Must set `fee_rate_window_size`.") + .try_into() + .expect("Configured fee rate window size out of bounds."), ) .expect("Error opening fee estimator"); Box::new(FeeRateFuzzer::new( underlying_estimator, - self.fee_rate_fuzzer_fraction, + self.fee_rate_fuzzer_fraction + .expect("Must set `fee_rate_fuzzer_fraction`."), )) } else { panic!("BUG: Expected to configure a weighted median fee estimator"); @@ -1472,8 +1476,8 @@ pub struct FeeEstimationConfigFile { pub cost_metric: Option, pub disabled: Option, pub log_error: Option, - pub fee_rate_fuzzer_fraction: f64, - pub fee_rate_window_size: u64, + pub fee_rate_fuzzer_fraction: Option, + pub fee_rate_window_size: Option, } impl Default for FeeEstimationConfigFile { @@ -1484,8 +1488,8 @@ impl Default for FeeEstimationConfigFile { cost_metric: None, disabled: None, log_error: None, - fee_rate_fuzzer_fraction: 0f64, - fee_rate_window_size: 0u64, + fee_rate_fuzzer_fraction: None, + fee_rate_window_size: None, } } } diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 9fbe507697..22f3a73731 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5976,8 +5976,8 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // Set this estimator as special. conf.estimation.fee_estimator = Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate); // Use randomness of 0 to keep test constant. Randomness is tested in unit tests. - conf.estimation.fee_rate_fuzzer_fraction = 0f64; - conf.estimation.fee_rate_window_size = window_size; + conf.estimation.fee_rate_fuzzer_fraction = Some(0f64); + conf.estimation.fee_rate_window_size = Some(window_size); conf.initial_balances.push(InitialBalance { address: spender_addr.clone().into(), From cda4f08147b1aaddbf03176791e3224cb8991c62 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 20:12:50 +0000 Subject: [PATCH 085/103] fix test comments --- testnet/stacks-node/src/tests/neon_integrations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 22f3a73731..3214ec24cc 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6103,7 +6103,7 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value } /// Test the FuzzedWeightedMedianFeeRate with window size 5 and randomness 0. We increase the -/// amount paid linearly each time. This estimate should grow faster than with window size 10. +/// amount paid linearly each time. This estimate should grow *faster* than with window size 10. #[test] #[ignore] fn fuzzed_median_fee_rate_estimation_test_window5() { @@ -6111,7 +6111,7 @@ fn fuzzed_median_fee_rate_estimation_test_window5() { } /// Test the FuzzedWeightedMedianFeeRate with window size 10 and randomness 0. We increase the -/// amount paid linearly each time. This estimate should grow faster than with window size 5. +/// amount paid linearly each time. This estimate should grow *slower* than with window size 5. #[test] #[ignore] fn fuzzed_median_fee_rate_estimation_test_window10() { From b2913deacebfc51dd43e43212076801e3f8de0a3 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 5 Jan 2022 20:53:28 +0000 Subject: [PATCH 086/103] update default handling --- testnet/stacks-node/src/config.rs | 20 +++++++++---------- .../src/tests/neon_integrations.rs | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index dc3fcca142..3f57baaa80 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1114,10 +1114,10 @@ pub struct FeeEstimationConfig { pub log_error: bool, /// If using FeeRateFuzzer, the amount of random noise, as a percentage of the base value (in /// [0, 1]) to add for fuzz. See comments on FeeRateFuzzer. - pub fee_rate_fuzzer_fraction: Option, + pub fee_rate_fuzzer_fraction: f64, /// If using WeightedMedianFeeRateEstimator, the window size to use. See comments on /// WeightedMedianFeeRateEstimator. - pub fee_rate_window_size: Option, + pub fee_rate_window_size: u64, } impl Default for FeeEstimationConfig { @@ -1127,8 +1127,8 @@ impl Default for FeeEstimationConfig { fee_estimator: Some(FeeEstimatorName::default()), cost_metric: Some(CostMetricName::default()), log_error: false, - fee_rate_fuzzer_fraction: Some(0.1f64), - fee_rate_window_size: Some(5u64), + fee_rate_fuzzer_fraction: 0.1f64, + fee_rate_window_size: 5u64, } } } @@ -1141,8 +1141,8 @@ impl From for FeeEstimationConfig { fee_estimator: None, cost_metric: None, log_error: false, - fee_rate_fuzzer_fraction: None, - fee_rate_window_size: None, + fee_rate_fuzzer_fraction: 0f64, + fee_rate_window_size: 0u64, }; } let cost_estimator = f @@ -1163,8 +1163,8 @@ impl From for FeeEstimationConfig { fee_estimator: Some(fee_estimator), cost_metric: Some(cost_metric), log_error, - fee_rate_fuzzer_fraction: f.fee_rate_fuzzer_fraction, - fee_rate_window_size: f.fee_rate_window_size, + fee_rate_fuzzer_fraction: f.fee_rate_fuzzer_fraction.unwrap_or(0.1f64), + fee_rate_window_size: f.fee_rate_window_size.unwrap_or(5u64), } } } @@ -1250,15 +1250,13 @@ impl FeeEstimationConfig { &chainstate_path, metric, self.fee_rate_window_size - .expect("Must set `fee_rate_window_size`.") .try_into() .expect("Configured fee rate window size out of bounds."), ) .expect("Error opening fee estimator"); Box::new(FeeRateFuzzer::new( underlying_estimator, - self.fee_rate_fuzzer_fraction - .expect("Must set `fee_rate_fuzzer_fraction`."), + self.fee_rate_fuzzer_fraction, )) } else { panic!("BUG: Expected to configure a weighted median fee estimator"); diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 3214ec24cc..c78d51266a 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -5976,8 +5976,8 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // Set this estimator as special. conf.estimation.fee_estimator = Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate); // Use randomness of 0 to keep test constant. Randomness is tested in unit tests. - conf.estimation.fee_rate_fuzzer_fraction = Some(0f64); - conf.estimation.fee_rate_window_size = Some(window_size); + conf.estimation.fee_rate_fuzzer_fraction = 0f64; + conf.estimation.fee_rate_window_size = window_size; conf.initial_balances.push(InitialBalance { address: spender_addr.clone().into(), From 26165de20a1fd46e94cd54682d881ccb26661da5 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 7 Jan 2022 02:31:54 +0000 Subject: [PATCH 087/103] move comment for symmetry --- testnet/stacks-node/src/tests/neon_integrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index c78d51266a..036c93a4a2 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6085,9 +6085,9 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // 1) The cost is always the same. // 2) Only grows. for i in 1..response_estimated_costs.len() { + // Check that the rate is growing monotonically. let curr_rate = response_top_fee_rates[i] as f64; let last_rate = response_top_fee_rates[i - 1] as f64; - // Check that the rate is growing monotonically. assert!(curr_rate >= last_rate); // The cost is always the same. From c1d2fde7cd941fbfd351f1bf0a1b73439813424e Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 7 Jan 2022 03:02:11 +0000 Subject: [PATCH 088/103] readability --- .../src/tests/neon_integrations.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 036c93a4a2..c32eec82cf 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -449,6 +449,11 @@ fn find_microblock_privkey( return None; } +// Returns true if relative difference between `a` and `b` is less than 0.01. +fn is_near(a:f64, b:f64) -> bool { + (a - b).abs() < 0.01 +} + #[test] #[ignore] fn bitcoind_integration_test() { @@ -6083,21 +6088,19 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // Check that: // 1) The cost is always the same. - // 2) Only grows. + // 2) Fee rate grows monotonically. for i in 1..response_estimated_costs.len() { - // Check that the rate is growing monotonically. - let curr_rate = response_top_fee_rates[i] as f64; - let last_rate = response_top_fee_rates[i - 1] as f64; - assert!(curr_rate >= last_rate); - - // The cost is always the same. let curr_cost = response_estimated_costs[i]; let last_cost = response_estimated_costs[i - 1]; assert_eq!(curr_cost, last_cost); + + let curr_rate = response_top_fee_rates[i] as f64; + let last_rate = response_top_fee_rates[i - 1] as f64; + assert!(curr_rate >= last_rate); } - // Check the final value is "near" what was expected. - assert!(response_top_fee_rates.last().unwrap() - expected_final_value < 0.1); + // Check the final value is near input parameter. + assert!(is_near(*response_top_fee_rates.last().unwrap(), expected_final_value)); channel.stop_chains_coordinator(); } From 2e151f1c567e12aec33cba4d2d9c4bd2f794ee32 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 7 Jan 2022 03:18:32 +0000 Subject: [PATCH 089/103] added two extra calls to `next_block_and_wait` to make sure block processed --- .../src/tests/neon_integrations.rs | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index c32eec82cf..76ed43fdcb 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -328,6 +328,38 @@ pub fn next_block_and_wait( true } +/// This function will call `next_block_and_wait` until the burnchain height underlying `BitcoinRegtestController` +/// reaches *exactly* `target_height`. +/// +/// Returns `false` if `next_block_and_wait` times out. +fn run_until_burnchain_height( + btc_regtest_controller: &mut BitcoinRegtestController, + blocks_processed: &Arc, + target_height: u64, + conf: &Config, +) -> bool { + let tip_info = get_chain_info(&conf); + let mut current_height = tip_info.burn_block_height; + + while current_height < target_height { + eprintln!( + "run_until_burnchain_height: Issuing block at {}, current_height burnchain height is ({})", + get_epoch_time_secs(), + current_height + ); + let next_result = next_block_and_wait(btc_regtest_controller, &blocks_processed); + if !next_result { + return false; + } + let tip_info = get_chain_info(&conf); + current_height = tip_info.burn_block_height; + } + + assert_eq!(current_height, target_height); + true +} + + pub fn wait_for_runloop(blocks_processed: &Arc) { let start = Instant::now(); while blocks_processed.load(Ordering::SeqCst) == 0 { @@ -5911,37 +5943,6 @@ fn atlas_stress_integration_test() { test_observer::clear(); } -/// This function will call `next_block_and_wait` until the burnchain height underlying `BitcoinRegtestController` -/// reaches *exactly* `target_height`. -/// -/// Returns `false` if `next_block_and_wait` times out. -fn run_until_burnchain_height( - btc_regtest_controller: &mut BitcoinRegtestController, - blocks_processed: &Arc, - target_height: u64, - conf: &Config, -) -> bool { - let tip_info = get_chain_info(&conf); - let mut current_height = tip_info.burn_block_height; - - while current_height < target_height { - eprintln!( - "run_until_burnchain_height: Issuing block at {}, current_height burnchain height is ({})", - get_epoch_time_secs(), - current_height - ); - let next_result = next_block_and_wait(btc_regtest_controller, &blocks_processed); - if !next_result { - return false; - } - let tip_info = get_chain_info(&conf); - current_height = tip_info.burn_block_height; - } - - assert_eq!(current_height, target_height); - true -} - /// Run a fixed contract 20 times. Linearly increase the amount paid each time. The cost of the /// contract should stay the same, and the fee rate paid should monotonically grow. The value /// should grow faster for lower values of `window_size`, because a bigger window slows down the @@ -6084,6 +6085,10 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value } } + // Wait two extra blocks to be sure. + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + assert_eq!(response_estimated_costs.len(), response_top_fee_rates.len()); // Check that: From 0316d4b6e71181a6a001b34facc4a5558a34ac69 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Fri, 7 Jan 2022 07:03:33 +0000 Subject: [PATCH 090/103] fmt --- testnet/stacks-node/src/tests/neon_integrations.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 76ed43fdcb..e9a43f13e2 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -359,7 +359,6 @@ fn run_until_burnchain_height( true } - pub fn wait_for_runloop(blocks_processed: &Arc) { let start = Instant::now(); while blocks_processed.load(Ordering::SeqCst) == 0 { @@ -482,7 +481,7 @@ fn find_microblock_privkey( } // Returns true if relative difference between `a` and `b` is less than 0.01. -fn is_near(a:f64, b:f64) -> bool { +fn is_near(a: f64, b: f64) -> bool { (a - b).abs() < 0.01 } @@ -6105,7 +6104,10 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value } // Check the final value is near input parameter. - assert!(is_near(*response_top_fee_rates.last().unwrap(), expected_final_value)); + assert!(is_near( + *response_top_fee_rates.last().unwrap(), + expected_final_value + )); channel.stop_chains_coordinator(); } From 9d8134208bb04c4023bde87fa8fe94c77cad661b Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 10 Jan 2022 17:14:01 +0000 Subject: [PATCH 091/103] address jude's comments --- src/cost_estimates/fee_medians.rs | 20 +++++++++++--------- src/cost_estimates/tests/fee_medians.rs | 15 ++++++++++----- testnet/stacks-node/src/config.rs | 3 +++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index d47d2bb0c2..ecd8ec34fe 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -120,9 +120,9 @@ impl WeightedMedianFeeRateEstimator { let mut lows = Vec::with_capacity(window_size as usize); let results = stmt .query_and_then::<_, SqliteError, _, _>(&[window_size], |row| { - let high: f64 = row.get(0)?; - let middle: f64 = row.get(1)?; - let low: f64 = row.get(2)?; + let high: f64 = row.get("high")?; + let middle: f64 = row.get("middle")?; + let low: f64 = row.get("low")?; Ok((low, middle, high)) }) .expect("SQLite failure"); @@ -240,18 +240,20 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { pub fn fee_rate_estimate_from_sorted_weighted_fees( sorted_fee_rates: &Vec, ) -> FeeRateEstimate { - if sorted_fee_rates.is_empty() { - panic!("`sorted_fee_rates` cannot be empty."); - } + assert!( + !sorted_fee_rates.is_empty(), + "`sorted_fee_rates` cannot be empty." + ); let mut total_weight = 0f64; for rate_and_weight in sorted_fee_rates { total_weight += rate_and_weight.weight as f64; } - if total_weight <= 0f64 { - panic!("`total_weight` is 0f64. This is an error."); - } + assert!( + total_weight > 0f64, + "`total_weight` is `0.0`. Must be positive." + ); let mut cumulative_weight = 0f64; let mut percentiles = Vec::new(); diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index aeafcffc60..53053b382a 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -31,15 +31,20 @@ use cost_estimates::fee_medians::fee_rate_estimate_from_sorted_weighted_fees; use cost_estimates::fee_medians::FeeRateAndWeight; use cost_estimates::tests::common::*; -/// Tolerance for approximate comparison. -const error_epsilon: f64 = 0.1; +/// Tolerance for approximate relative comparison. Values must be within 0.1% of each other. +const error_epsilon: f64 = 0.001; + +fn is_close_f64(a: f64, b: f64) -> bool { + let error = (a - b).abs() / a.abs(); + error < error_epsilon +} /// Returns `true` iff each value in `left` is within `error_epsilon` of the /// corresponding value in `right`. fn is_close(left: FeeRateEstimate, right: FeeRateEstimate) -> bool { - let is_ok = (left.high - right.high).abs() < error_epsilon - && (left.middle - right.middle).abs() < error_epsilon - && (left.low - right.low).abs() < error_epsilon; + let is_ok = is_close_f64(left.high, right.high) + && is_close_f64(left.middle, right.middle) + && is_close_f64(left.low, right.low); if !is_ok { warn!("ExecutionCost's are not close. {:?} vs {:?}", left, right); } diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 3f57baaa80..df972f9ef0 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1213,6 +1213,7 @@ impl FeeEstimationConfig { mut chainstate_path: PathBuf, ) -> PessimisticEstimator { if let Some(CostEstimatorName::NaivePessimistic) = self.cost_estimator.as_ref() { + chainstate_path.push("fee_estimation"); chainstate_path.push("cost_estimator_pessimistic.sqlite"); PessimisticEstimator::open(&chainstate_path, self.log_error) .expect("Error opening cost estimator") @@ -1227,6 +1228,7 @@ impl FeeEstimationConfig { metric: CM, ) -> Box { if let Some(FeeEstimatorName::ScalarFeeRate) = self.fee_estimator.as_ref() { + chainstate_path.push("fee_estimation"); chainstate_path.push("fee_estimator_scalar_rate.sqlite"); Box::new( ScalarFeeRateEstimator::open(&chainstate_path, metric) @@ -1245,6 +1247,7 @@ impl FeeEstimationConfig { metric: CM, ) -> Box { if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() { + chainstate_path.push("fee_estimation"); chainstate_path.push("fee_fuzzed_weighted_median.sqlite"); let underlying_estimator = WeightedMedianFeeRateEstimator::open( &chainstate_path, From cac9ce1400098f7d251b3bf6a6a6d4175f948d98 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 10 Jan 2022 17:16:37 +0000 Subject: [PATCH 092/103] small readability things --- src/cost_estimates/fee_medians.rs | 10 ++-------- src/cost_estimates/tests/fee_medians.rs | 3 +-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index ecd8ec34fe..f8ff10bb0e 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -240,20 +240,14 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { pub fn fee_rate_estimate_from_sorted_weighted_fees( sorted_fee_rates: &Vec, ) -> FeeRateEstimate { - assert!( - !sorted_fee_rates.is_empty(), - "`sorted_fee_rates` cannot be empty." - ); + assert!(!sorted_fee_rates.is_empty()); let mut total_weight = 0f64; for rate_and_weight in sorted_fee_rates { total_weight += rate_and_weight.weight as f64; } - assert!( - total_weight > 0f64, - "`total_weight` is `0.0`. Must be positive." - ); + assert!(total_weight > 0f64); let mut cumulative_weight = 0f64; let mut percentiles = Vec::new(); diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 53053b382a..8b79c8f681 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -39,8 +39,7 @@ fn is_close_f64(a: f64, b: f64) -> bool { error < error_epsilon } -/// Returns `true` iff each value in `left` is within `error_epsilon` of the -/// corresponding value in `right`. +/// Returns `true` iff each value in `left` "close" to its counterpart in `right`. fn is_close(left: FeeRateEstimate, right: FeeRateEstimate) -> bool { let is_ok = is_close_f64(left.high, right.high) && is_close_f64(left.middle, right.middle) From 3a247045a1fbfa865bf7384daadfa63ba5a8789d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 10 Jan 2022 21:05:41 +0000 Subject: [PATCH 093/103] replace vec with array --- src/cost_estimates/fee_medians.rs | 2 +- testnet/stacks-node/src/config.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index f8ff10bb0e..c24bd94abe 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -238,7 +238,7 @@ impl FeeEstimator for WeightedMedianFeeRateEstimator { /// /// `sorted_fee_rates` must be non-empty. pub fn fee_rate_estimate_from_sorted_weighted_fees( - sorted_fee_rates: &Vec, + sorted_fee_rates: &[FeeRateAndWeight], ) -> FeeRateEstimate { assert!(!sorted_fee_rates.is_empty()); diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index df972f9ef0..7e40cca5d4 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1213,7 +1213,6 @@ impl FeeEstimationConfig { mut chainstate_path: PathBuf, ) -> PessimisticEstimator { if let Some(CostEstimatorName::NaivePessimistic) = self.cost_estimator.as_ref() { - chainstate_path.push("fee_estimation"); chainstate_path.push("cost_estimator_pessimistic.sqlite"); PessimisticEstimator::open(&chainstate_path, self.log_error) .expect("Error opening cost estimator") @@ -1597,3 +1596,5 @@ pub struct InitialBalanceFile { pub address: String, pub amount: u64, } + + From 644c64091eb88cc3d03e928c193eafd8cb2ebea1 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Mon, 10 Jan 2022 21:47:01 +0000 Subject: [PATCH 094/103] remove the fee_estimation --- testnet/stacks-node/src/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 7e40cca5d4..aa9bb1ef08 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1227,7 +1227,6 @@ impl FeeEstimationConfig { metric: CM, ) -> Box { if let Some(FeeEstimatorName::ScalarFeeRate) = self.fee_estimator.as_ref() { - chainstate_path.push("fee_estimation"); chainstate_path.push("fee_estimator_scalar_rate.sqlite"); Box::new( ScalarFeeRateEstimator::open(&chainstate_path, metric) @@ -1246,7 +1245,6 @@ impl FeeEstimationConfig { metric: CM, ) -> Box { if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() { - chainstate_path.push("fee_estimation"); chainstate_path.push("fee_fuzzed_weighted_median.sqlite"); let underlying_estimator = WeightedMedianFeeRateEstimator::open( &chainstate_path, From 6305f488161d0d1605e0a0ce3af1ab085979271e Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 11 Jan 2022 14:12:07 +0000 Subject: [PATCH 095/103] pavi's comments --- CHANGELOG.md | 6 +++--- .../src/tests/neon_integrations.rs | 21 ------------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2a8739ad..df91491586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,9 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE sensitivity to outliers. Its characteristic features are: 1) use a window to forget past estimates instead of exponential averaging, 2) use weighted percentiles, so that bigger transactions influence the estimates more, 3) - assess empty blocks as having paid the "minimum fee", so that empty space is - accounted for, 4) use random "fuzz" so that in busy times we will not have - ties. + assess empty space in blocks as having paid the "minimum fee", so that empty + space is accounted for, 4) use random "fuzz" so that in busy times we will + not have ties. ### Changed diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index f000b3698d..265028a67d 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -734,25 +734,6 @@ fn get_pox_info(http_origin: &str) -> RPCPoxInfoData { .unwrap() } -fn get_fee_rate_info( - http_origin: &str, - fee_rate_input: &FeeRateEstimateRequestBody, -) -> RPCPoxInfoData { - let client = reqwest::blocking::Client::new(); - let path = format!("{}/v2/fees/transaction", http_origin); - - let body = { serde_json::to_vec(&json!(fee_rate_input)).unwrap() }; - - client - .post(&path) - .header("Content-Type", "application/json") - .body(body) - .send() - .unwrap() - .json::() - .unwrap() -} - fn get_chain_tip(http_origin: &str) -> (ConsensusHash, BlockHeaderHash) { let client = reqwest::blocking::Client::new(); let path = format!("{}/v2/info", http_origin); @@ -6196,8 +6177,6 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value let spender_sk = StacksPrivateKey::new(); let spender_addr = to_addr(&spender_sk); - // 1) a basic test `Config` - // 2) the miner's let (mut conf, _) = neon_integration_test_conf(); // Set this estimator as special. From 0ee4c4fe9fda89ce0de7bb717e7e6b652a396f9f Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 11 Jan 2022 14:12:26 +0000 Subject: [PATCH 096/103] fmt --- testnet/stacks-node/src/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index e8f8a7c598..b8ef749ccb 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1595,5 +1595,3 @@ pub struct InitialBalanceFile { pub address: String, pub amount: u64, } - - From 25aaa1d564a4a625ce3130582208b6c0a1e9bd5d Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Tue, 11 Jan 2022 23:10:18 +0000 Subject: [PATCH 097/103] add spender addr --- testnet/stacks-node/src/tests/neon_integrations.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 265028a67d..a0fc415441 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6258,9 +6258,8 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // Read from the fee estimation endpoin. let path = format!("{}/v2/fees/transaction", &http_origin); - let contract_addr = to_addr(&StacksPrivateKey::from_hex(SK_1).unwrap()); let tx_payload = TransactionPayload::ContractCall(TransactionContractCall { - address: contract_addr.clone().into(), + address: spender_addr.clone().into(), contract_name: ContractName::try_from("increment-contract").unwrap(), function_name: ClarityName::try_from("increment-many").unwrap(), function_args: vec![], From 0e64d280f11180efbedccbc15a5ad6651d54d495 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 12:32:00 +0000 Subject: [PATCH 098/103] code clean ups --- src/cost_estimates/fee_medians.rs | 26 ++++++++----------- src/cost_estimates/tests/fee_medians.rs | 6 ++--- .../src/tests/neon_integrations.rs | 9 ++++--- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index c24bd94abe..145c8dbc97 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -335,21 +335,17 @@ fn fee_rate_and_weight_from_receipt( metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) } }; - let denominator = if scalar_cost >= 1 { - scalar_cost as f64 - } else { - MINIMUM_TX_FEE_RATE - }; + let denominator = cmp::min(scalar_cost, 1) as f64; let fee_rate = fee as f64 / denominator; - if fee_rate >= MINIMUM_TX_FEE_RATE && fee_rate.is_finite() { - Some(FeeRateAndWeight { - fee_rate, - weight: scalar_cost, - }) + + assert!(fee_rate.is_finite()); + let effective_fee_rate = if fee_rate < MINIMUM_TX_FEE_RATE { + MINIMUM_TX_FEE_RATE } else { - Some(FeeRateAndWeight { - fee_rate: MINIMUM_TX_FEE_RATE, - weight: scalar_cost, - }) - } + fee_rate + }; + Some(FeeRateAndWeight { + fee_rate: effective_fee_rate, + weight: scalar_cost, + }) } diff --git a/src/cost_estimates/tests/fee_medians.rs b/src/cost_estimates/tests/fee_medians.rs index 8b79c8f681..cc67423aab 100644 --- a/src/cost_estimates/tests/fee_medians.rs +++ b/src/cost_estimates/tests/fee_medians.rs @@ -31,12 +31,10 @@ use cost_estimates::fee_medians::fee_rate_estimate_from_sorted_weighted_fees; use cost_estimates::fee_medians::FeeRateAndWeight; use cost_estimates::tests::common::*; -/// Tolerance for approximate relative comparison. Values must be within 0.1% of each other. -const error_epsilon: f64 = 0.001; - +/// Returns true iff `b` is within `0.1%` of `a`. fn is_close_f64(a: f64, b: f64) -> bool { let error = (a - b).abs() / a.abs(); - error < error_epsilon + error < 0.001 } /// Returns `true` iff each value in `left` "close" to its counterpart in `right`. diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index a0fc415441..6dd8d09718 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -503,9 +503,10 @@ fn find_microblock_privkey( return None; } -// Returns true if relative difference between `a` and `b` is less than 0.01. -fn is_near(a: f64, b: f64) -> bool { - (a - b).abs() < 0.01 +/// Returns true iff `b` is within `0.1%` of `a`. +fn is_close_f64(a: f64, b: f64) -> bool { + let error = (a - b).abs() / a.abs(); + error < 0.001 } #[test] @@ -6304,7 +6305,7 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value } // Check the final value is near input parameter. - assert!(is_near( + assert!(is_close_f64( *response_top_fee_rates.last().unwrap(), expected_final_value )); From 14ca280726148df68373ec6a359aa0000654f2b7 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 12:37:23 +0000 Subject: [PATCH 099/103] return none if infinite --- src/cost_estimates/fee_medians.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 145c8dbc97..be37891e69 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -309,6 +309,10 @@ fn maybe_add_minimum_fee_rate(working_rates: &mut Vec, full_bl } /// Depending on the type of the transaction, calculate fee rate and total cost. +/// +/// Returns None if: +/// 1) There is no fee rate for the tx. +/// 2) Cacluated fee rate is infinite. fn fee_rate_and_weight_from_receipt( metric: &dyn CostMetric, tx_receipt: &StacksTransactionReceipt, @@ -338,14 +342,17 @@ fn fee_rate_and_weight_from_receipt( let denominator = cmp::min(scalar_cost, 1) as f64; let fee_rate = fee as f64 / denominator; - assert!(fee_rate.is_finite()); - let effective_fee_rate = if fee_rate < MINIMUM_TX_FEE_RATE { - MINIMUM_TX_FEE_RATE + if fee_rate.is_infinite() { + None } else { - fee_rate - }; - Some(FeeRateAndWeight { - fee_rate: effective_fee_rate, - weight: scalar_cost, - }) + let effective_fee_rate = if fee_rate < MINIMUM_TX_FEE_RATE { + MINIMUM_TX_FEE_RATE + } else { + fee_rate + }; + Some(FeeRateAndWeight { + fee_rate: effective_fee_rate, + weight: scalar_cost, + }) + } } From ff52409272cbd8375590ac38a351624fb71a5dac Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 12:45:24 +0000 Subject: [PATCH 100/103] added warn --- src/cost_estimates/fee_medians.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index be37891e69..435d76023e 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -343,6 +343,7 @@ fn fee_rate_and_weight_from_receipt( let fee_rate = fee as f64 / denominator; if fee_rate.is_infinite() { + warn!("fee_rate is infinite for {:?}", tx_receipt); None } else { let effective_fee_rate = if fee_rate < MINIMUM_TX_FEE_RATE { From 65f5d9ff1e1c433249cc541c386ecc3ea4696297 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 15:23:52 +0000 Subject: [PATCH 101/103] fixed min/max bug --- src/cost_estimates/fee_medians.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cost_estimates/fee_medians.rs b/src/cost_estimates/fee_medians.rs index 435d76023e..a2c1bc88e6 100644 --- a/src/cost_estimates/fee_medians.rs +++ b/src/cost_estimates/fee_medians.rs @@ -339,7 +339,7 @@ fn fee_rate_and_weight_from_receipt( metric.from_cost_and_len(&tx_receipt.execution_cost, &block_limit, tx_size) } }; - let denominator = cmp::min(scalar_cost, 1) as f64; + let denominator = cmp::max(scalar_cost, 1) as f64; let fee_rate = fee as f64 / denominator; if fee_rate.is_infinite() { From e5990680cabcbba6f0e0bfa91fb62bde44d30a14 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 18:13:21 +0000 Subject: [PATCH 102/103] restart the tests --- testnet/stacks-node/src/tests/neon_integrations.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 6dd8d09718..54086ad09b 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6569,3 +6569,4 @@ fn use_latest_tip_integration_test() { ) .is_ok()); } + From e6cff5e8b1949fb6bf7bc1ec75dc3bce0588dc44 Mon Sep 17 00:00:00 2001 From: Greg Coppola Date: Wed, 12 Jan 2022 18:59:40 +0000 Subject: [PATCH 103/103] start tests again --- testnet/stacks-node/src/tests/neon_integrations.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 54086ad09b..6dd8d09718 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6569,4 +6569,3 @@ fn use_latest_tip_integration_test() { ) .is_ok()); } -