From 7e916379ddff4bde1108bd64335cac5175702fb9 Mon Sep 17 00:00:00 2001 From: JSwambo Date: Tue, 19 Oct 2021 17:07:20 +0100 Subject: [PATCH 1/2] poller: prepare update_feerate_estimates for testing --- src/bitcoind/interface.rs | 27 +++++++++++++++++++++++++++ src/poller.rs | 17 +++++++++++++++++ tests/test_chain.py | 26 ++++++++++++++++++++++---- tests/test_framework/bitcoind.py | 3 +++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/bitcoind/interface.rs b/src/bitcoind/interface.rs index 24fa2b9..8bdf7a8 100644 --- a/src/bitcoind/interface.rs +++ b/src/bitcoind/interface.rs @@ -425,6 +425,27 @@ impl BitcoinD { self.make_node_request_failible("sendrawtransaction", ¶ms!(tx_hex)) .map(|_| ()) } + + /// Get fee-rate estimate + /// TODO: limit conf_target to range 1 to 1008 + pub fn feerate_estimate(&self, conf_target: i16) -> Option { + let result = self.make_node_request("estimatesmartfee", ¶ms!(conf_target)); + + if let Some(_) = result.get("errors") { + None + } else { + let feerate = result + .get("feerate") + .and_then(|f| f.as_f64()) + .expect("'estimatesmartfee' didn't return a 'feerate' entry"); + let blocks = result + .get("blocks") + .and_then(|n| n.as_i64()) + .expect("'estimatesmartfee' didn't return a 'blocks' entry"); + + Some(FeeRate { feerate, blocks }) + } + } } /// Info about bitcoind's sync state @@ -448,3 +469,9 @@ pub struct UtxoInfo { pub bestblock: BlockHash, pub value: Amount, } + +/// FeeRate information from estimatesmartfee +pub struct FeeRate { + pub feerate: f64, + pub blocks: i64, +} diff --git a/src/poller.rs b/src/poller.rs index ac2e234..7d91339 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -380,6 +380,22 @@ fn maybe_revault( Ok(()) } +fn update_feerate_estimates( + db_path: &path::Path, + bitcoind: &BitcoinD, + current_tip: &ChainTip, +) -> Result<(), PollerError> { + let conf_target = 1; + if let Some(feerate) = bitcoind.feerate_estimate(conf_target) { + log::debug!( + "At block {} feerate estimate is {:?}", + current_tip.height, + feerate.feerate + ) + } + Ok(()) +} + // We only do actual processing on new blocks. This puts a natural limit on the amount of work // we are doing, reduces the number of edge cases we need to handle, and there is no benefit to try // to cancel Unvaults right after their broadcast. @@ -393,6 +409,7 @@ fn new_block( ) -> Result<(), PollerError> { // Update the fee-bumping reserves estimates // TODO + update_feerate_estimates(db_path, bitcoind, current_tip)?; // Any vault to forget and feebump coins to unregister? // TODO diff --git a/tests/test_chain.py b/tests/test_chain.py index 8ff0c99..d5a0d31 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -32,7 +32,8 @@ def test_simple_unvault_broadcast(miradord, bitcoind): [ f"Got a confirmed Unvault UTXO at '{unvault_txid}:0'", f"Broadcasted Cancel transaction '{txs['cancel']['tx']}'", - f"Unvault transaction '{unvault_txid}' for vault at '{deposit_outpoint}' is still unspent", + f"Unvault transaction '{unvault_txid}' for vault at '{deposit_outpoint}' is" + " still unspent", ] ) @@ -70,7 +71,8 @@ def test_spent_cancel_detection(miradord, bitcoind): [ f"Got a confirmed Unvault UTXO at '{unvault_txid}:0'", f"Broadcasted Cancel transaction '{txs['cancel']['tx']}'", - f"Unvault transaction '{unvault_txid}' for vault at '{deposit_outpoint}' is still unspent", + f"Unvault transaction '{unvault_txid}' for vault at '{deposit_outpoint}' is" + " still unspent", ] ) @@ -82,7 +84,8 @@ def test_spent_cancel_detection(miradord, bitcoind): bitcoind.generate_block(1, wait_for_mempool=[cancel_tx["txid"], unvault_txid]) miradord.wait_for_log( - f"Noticed at height .* that Cancel transaction was confirmed for vault at '{deposit_outpoint}'" + "Noticed at height .* that Cancel transaction was confirmed for vault at" + f" '{deposit_outpoint}'" ) @@ -119,9 +122,24 @@ def test_simple_spend_detection(miradord, bitcoind): bitcoind.rpc.sendrawtransaction(txs["spend"]["tx"]) bitcoind.generate_block(1, 1) miradord.wait_for_log( - f"Noticed .* that Spend transaction was confirmed for vault at '{deposit_outpoint}'" + "Noticed .* that Spend transaction was confirmed for vault at" + f" '{deposit_outpoint}'" ) # Generate two days worth of blocks, the WT should forget about this vault bitcoind.generate_block(288) miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'") + + +def test_feerate_estimation(miradord, bitcoind): + # Generate some transaction history for estimatesmartfee. + # 10 transactions in 50 blocks (send to deposit address) + amount = 1 + for block in range(0, 50): + wait_for_mempool = [] + for tx in range(0, 10): + txid, outpoint = bitcoind.create_utxo(DEPOSIT_ADDRESS, amount) + wait_for_mempool.append(txid) + bitcoind.generate_block(1, wait_for_mempool) + feerate = bitcoind.estimatesmartfee(1)["feerate"].normalize() + miradord.wait_for_logs([f"feerate estimate is {feerate}"]) diff --git a/tests/test_framework/bitcoind.py b/tests/test_framework/bitcoind.py index 9a0c261..84e2d44 100644 --- a/tests/test_framework/bitcoind.py +++ b/tests/test_framework/bitcoind.py @@ -132,6 +132,9 @@ def generate_blocks_censor(self, n, txids): for txid in txids: self.rpc.prioritisetransaction(txid, None, fee_delta) + def estimatesmartfee(self, conf_target): + return self.rpc.estimatesmartfee(conf_target) + def simple_reorg(self, height, shift=0): """ Reorganize chain by creating a fork at height={height} and: From 6dc871ae403b3730d766f9b0e118d0da75b8c9e1 Mon Sep 17 00:00:00 2001 From: JSwambo Date: Thu, 16 Dec 2021 16:59:47 +0000 Subject: [PATCH 2/2] feerates: implement vault_reserve_feerate and feerate_estimate --- src/bitcoind/interface.rs | 164 ++++++++++++++++++++++++++++--- src/database/mod.rs | 102 ++++++++++++++++--- src/database/schema.rs | 33 +++++++ src/poller.rs | 66 ++++++++++--- tests/test_chain.py | 44 ++++++++- tests/test_framework/bitcoind.py | 29 ++++++ 6 files changed, 394 insertions(+), 44 deletions(-) diff --git a/src/bitcoind/interface.rs b/src/bitcoind/interface.rs index 8bdf7a8..76ca722 100644 --- a/src/bitcoind/interface.rs +++ b/src/bitcoind/interface.rs @@ -428,24 +428,164 @@ impl BitcoinD { /// Get fee-rate estimate /// TODO: limit conf_target to range 1 to 1008 - pub fn feerate_estimate(&self, conf_target: i16) -> Option { + pub fn feerate_estimate( + &self, + conf_target: i16, + ) -> Result { let result = self.make_node_request("estimatesmartfee", ¶ms!(conf_target)); if let Some(_) = result.get("errors") { - None + // Fallback feerate estimate, using "85Q1H" strategy + let height = self + .make_node_request("getblockcount", ¶ms!()) + .as_i64() + .expect("Invalid height value"); + let window_len = 6; // 1 hour of blocks + let mut feerates = Vec::with_capacity(window_len); + for i in height - window_len as i64..height { + feerates.push(self.block_stats(i as i32)?.avgfeerate); + } + let q: f64 = 0.85; + if let Some(f) = quantile(feerates, q) { + Ok(BlockFeerateEstimate { + feerate: f, + block: height, + }) + } else { + // FIXME: return Error instead of constant feerate? + Ok(BlockFeerateEstimate { + feerate: 5, + block: height, + }) + } } else { - let feerate = result + // estimatesmartfee succeeded, feerate units of the response are BTC/kb + let mut f = result .get("feerate") .and_then(|f| f.as_f64()) - .expect("'estimatesmartfee' didn't return a 'feerate' entry"); - let blocks = result - .get("blocks") - .and_then(|n| n.as_i64()) - .expect("'estimatesmartfee' didn't return a 'blocks' entry"); + .expect("'estimatesmartfee' didn't return a valid 'feerate' entry") + * 100000.0_f64; // convert to sat/vB + f = f.round(); + + Ok(BlockFeerateEstimate { + feerate: f as u64, + block: result + .get("blocks") + .and_then(|n| n.as_i64()) + .expect("'estimatesmartfee' didn't return a 'blocks' entry"), + }) + } + } + + /// Get block stats + pub fn block_stats(&self, block_height: i32) -> Result { + let res = self.make_node_request_failible("getblockstats", ¶ms!(block_height))?; + + Ok(BlockStats { + block: block_height, + avgfee: res + .get("avgfee") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + avgfeerate: res + .get("avgfeerate") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + avgtxsize: res + .get("avgtxsize") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + feerate_percentiles: res + .get("feerate_percentiles") + .expect("'getblockstats' didn't return a valid entry for 'feerate_percentiles'") + .as_array() + .expect("'getblockstats' didn't return a valid array for 'feerate_percentiles'") + .into_iter() + .map(|f| f.as_u64().expect("Failed to cast to u64")) + .collect(), + maxfee: res + .get("maxfee") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + maxfeerate: res + .get("maxfeerate") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + minfee: res + .get("minfee") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + minfeerate: res + .get("minfeerate") + .and_then(|f| f.as_u64()) + .expect("'getblockstats' didn't return a valid entry"), + }) + } + + /// Get feerate reserve per vault with 95Q90D + pub fn feerate_reserve_per_vault( + &self, + block_height: i32, + ) -> Result, BitcoindError> { + let window_len: usize = 144 * 90; // 90 days of blocks + + // Not enough blocks to perform the computation, return None + if block_height < window_len as i32 { + return Ok(None); + } + + let mut feerates = Vec::with_capacity(window_len); - Some(FeeRate { feerate, blocks }) + for i in block_height - window_len as i32..block_height + 1 { + feerates.push(self.block_stats(i)?.avgfeerate) + } + + let q: f64 = 0.95; // 95th quantile + if let Some(f) = quantile(feerates, q) { + return Ok(Some(f)); + } else { + Ok(None) + } + } +} + +/// Helper function to compute the value of a collection that sits at or immediately above the +/// given quantile +pub fn quantile(mut collection: Vec, quantile: f64) -> Option { + let len = collection.len(); + if len == 0 { + return None; + } + collection.sort(); + for (i, f) in collection.iter().enumerate() { + if i as f64 >= quantile * len as f64 { + return Some(*f); } } + None +} + +/// TODO: Feerate type +// #[derive(Debug)] +// pub struct Feerate(pub f64); + +/// Block Statistics +pub struct BlockStats { + pub block: i32, + pub avgfee: u64, + pub avgfeerate: u64, + pub avgtxsize: u64, + pub feerate_percentiles: Vec, + pub maxfee: u64, + pub maxfeerate: u64, + pub minfee: u64, + pub minfeerate: u64, +} + +/// Feerate estimate at associated block +pub struct BlockFeerateEstimate { + pub feerate: u64, + pub block: i64, } /// Info about bitcoind's sync state @@ -469,9 +609,3 @@ pub struct UtxoInfo { pub bestblock: BlockHash, pub value: Amount, } - -/// FeeRate information from estimatesmartfee -pub struct FeeRate { - pub feerate: f64, - pub blocks: i64, -} diff --git a/src/database/mod.rs b/src/database/mod.rs index 8f8fe48..4957e45 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -4,13 +4,16 @@ use revault_tx::{ bitcoin::{secp256k1, util::bip32, Amount, BlockHash, Network, OutPoint}, scripts::{CpfpDescriptor, DepositDescriptor, UnvaultDescriptor}, }; -use schema::{DbInstance, DbSignature, DbVault, SigTxType, SCHEMA}; +use schema::{DbFeerate, DbInstance, DbSignature, DbVault, SigTxType, SCHEMA}; use std::{collections, convert::TryInto, fs, io, os::unix::fs::OpenOptionsExt, path, time}; use rusqlite::params; pub const DB_VERSION: u32 = 0; +// FIXME: Use correct values +pub const INIT_VAULT_RESERVE_FEERATE: u64 = 1000; +pub const INIT_VAULT_RESERVE_HEIGHT: i32 = 680000; #[derive(Debug)] pub enum DatabaseError { @@ -22,6 +25,8 @@ pub enum DatabaseError { DescriptorMismatch(String, String), /// An operation was requested on a vault that doesn't exist UnknownVault(Box), + /// Vault reserve feerate not found + VaultReserveFeerateNotFound(String), } impl std::fmt::Display for DatabaseError { @@ -43,6 +48,9 @@ impl std::fmt::Display for DatabaseError { "Operation requested on vault at '{:?}' but no such vault exist in database.", *id ), + Self::VaultReserveFeerateNotFound(ref e) => { + write!(f, "Vault reserve feerate not found: {}", e) + } } } } @@ -412,6 +420,41 @@ pub fn db_cancel_signatures( db_sigs_by_type(db_path, vault_id, SigTxType::Cancel) } +pub fn db_update_vault_reserve_feerate( + db_path: &path::Path, + last_update: i32, + vault_reserve_feerate: u64, +) -> Result<(), DatabaseError> { + db_exec(db_path, |db_tx| { + db_tx.execute( + "UPDATE feerates + SET last_update = (?1), + vault_reserve_feerate = (?2)", + params![last_update, vault_reserve_feerate], + )?; + Ok(()) + }) +} + +pub fn db_vault_reserve_feerate(db_path: &path::Path) -> Result { + let res: Option = db_query( + db_path, + "SELECT * FROM feerates ORDER BY last_update DESC LIMIT 1", + [], + |row| row.try_into(), + )? + .pop(); + + match res { + Some(db_feerate) => Ok(db_feerate), + None => { + return Err(DatabaseError::VaultReserveFeerateNotFound(String::from( + "Feerates table not correctly initialised.", + ))) + } + } +} + // Create the db file with RW permissions only for the user fn create_db_file(db_path: &path::Path) -> Result<(), DatabaseError> { let mut options = fs::OpenOptions::new(); @@ -460,7 +503,19 @@ fn create_db( vec![0u8; 32], ], )?; - + if network == Network::Bitcoin { + tx.execute( + "INSERT INTO feerates (last_update, vault_reserve_feerate) + VALUES (?1,?2)", + params![INIT_VAULT_RESERVE_HEIGHT, INIT_VAULT_RESERVE_FEERATE], + )?; + } else { + tx.execute( + "INSERT INTO feerates (last_update, vault_reserve_feerate) + VALUES (?1,?2)", + params![0, INIT_VAULT_RESERVE_FEERATE], + )?; + } Ok(()) }) } @@ -547,7 +602,7 @@ mod tests { use super::*; // Create a dummy database and return its path (to be deleted by the caller) - fn get_db() -> path::PathBuf { + fn get_db(network: Network) -> path::PathBuf { let db_path: path::PathBuf = format!("scratch_test_{:?}.sqlite3", thread::current().id()).into(); let deposit_desc = DepositDescriptor::from_str("wsh(multi(2,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*,xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))#n3cj9mhy").unwrap(); @@ -557,14 +612,7 @@ mod tests { // Remove any potential leftover from a previous crashed session fs::remove_file(&db_path).unwrap_or_else(|_| ()); - setup_db( - &db_path, - &deposit_desc, - &unvault_desc, - &cpfp_desc, - Network::Bitcoin, - ) - .unwrap(); + setup_db(&db_path, &deposit_desc, &unvault_desc, &cpfp_desc, network).unwrap(); db_path } @@ -698,7 +746,7 @@ mod tests { // Sanity check we can create, delegate and delete a vault #[test] fn db_vault_creation() { - let db_path = get_db(); + let db_path = get_db(Network::Bitcoin); let outpoint_a = OutPoint::from_str( "5bebdb97b54e2268b3fccd4aeea99419d87a92f88f27e906ceea5e863946a731:0", ) @@ -974,7 +1022,7 @@ mod tests { #[test] fn db_tip_update() { - let db_path = get_db(); + let db_path = get_db(Network::Bitcoin); let height = 21; let hash = @@ -997,4 +1045,32 @@ mod tests { // Cleanup fs::remove_file(&db_path).unwrap(); } + + #[test] + fn db_feerates_table() { + let db_path = get_db(Network::Testnet); + let vault_reserve_feerate = 1001; + let test_feerate = 1337; + + let init_feerate = db_vault_reserve_feerate(&db_path) + .unwrap() + .vault_reserve_feerate; + assert_eq!(init_feerate, INIT_VAULT_RESERVE_FEERATE); + + for last_update in 1..=10 { + if last_update < 10 { + db_update_vault_reserve_feerate(&db_path, last_update, vault_reserve_feerate) + .unwrap(); + } else { + db_update_vault_reserve_feerate(&db_path, last_update, test_feerate).unwrap(); + } + } + + let row = db_vault_reserve_feerate(&db_path).unwrap(); + + assert_eq!(row.last_update, 10); + assert_eq!(row.vault_reserve_feerate, test_feerate); + + fs::remove_file(&db_path).unwrap(); + } } diff --git a/src/database/schema.rs b/src/database/schema.rs index 04bc214..d46fc25 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -73,6 +73,15 @@ CREATE TABLE signatures ( ON UPDATE RESTRICT ON DELETE RESTRICT ); + +/* Track vault_feerate_reserve value - the cumulative maximum of the 95-th + quantile of mean block feerates over a 90 day window +*/ +CREATE TABLE feerates ( + id INTEGER PRIMARY KEY NOT NULL, + last_update INTEGER NOT NULL, + vault_reserve_feerate INTEGER NOT NULL +); "; /// A row in the "instances" table @@ -247,3 +256,27 @@ impl TryFrom<&rusqlite::Row<'_>> for DbSignature { }) } } + +/// A row in the "feerates" table +#[derive(Clone, Debug)] +pub struct DbFeerate { + pub id: i64, + pub last_update: i32, + pub vault_reserve_feerate: u64, +} + +impl TryFrom<&rusqlite::Row<'_>> for DbFeerate { + type Error = rusqlite::Error; + + fn try_from(row: &rusqlite::Row) -> Result { + let id = row.get(0)?; + let last_update = row.get(1)?; + let vault_reserve_feerate = row.get(2)?; + + Ok(DbFeerate { + id, + last_update, + vault_reserve_feerate, + }) + } +} diff --git a/src/poller.rs b/src/poller.rs index 7d91339..ac6600b 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -7,7 +7,8 @@ use crate::{ database::{ db_cancel_signatures, db_del_vault, db_delegated_vaults, db_instance, db_should_cancel_vault, db_should_not_cancel_vault, db_unvault_spender_confirmed, - db_unvaulted_vaults, db_update_tip, db_vault, schema::DbVault, DatabaseError, + db_unvaulted_vaults, db_update_tip, db_update_vault_reserve_feerate, db_vault, + db_vault_reserve_feerate, schema::DbVault, DatabaseError, }, plugins::{NewBlockInfo, VaultInfo}, }; @@ -380,20 +381,53 @@ fn maybe_revault( Ok(()) } -fn update_feerate_estimates( +fn update_vault_reserve_feerate( db_path: &path::Path, bitcoind: &BitcoinD, current_tip: &ChainTip, ) -> Result<(), PollerError> { - let conf_target = 1; - if let Some(feerate) = bitcoind.feerate_estimate(conf_target) { + let vault_reserve_feerate = db_vault_reserve_feerate(&db_path)?; + + // Don't update if last_update was at current_tip + if vault_reserve_feerate.last_update == current_tip.height { log::debug!( - "At block {} feerate estimate is {:?}", - current_tip.height, - feerate.feerate - ) + "vault reserve feerate already up-to-date: {}", + vault_reserve_feerate.vault_reserve_feerate + ); + return Ok(()); + } else { + // Compute the vault_reserve_feerate for each block from last update to current tip and + // update if it has increased. + for height in vault_reserve_feerate.last_update..=current_tip.height { + if let Some(candidate) = bitcoind.feerate_reserve_per_vault(height)? { + if vault_reserve_feerate.vault_reserve_feerate < candidate { + db_update_vault_reserve_feerate(&db_path, height, candidate)?; + log::debug!( + "vault reserve feerate updated to {} at block {}", + candidate, + height, + ); + } else { + // update the last_update value + db_update_vault_reserve_feerate( + &db_path, + height, + vault_reserve_feerate.vault_reserve_feerate, + )?; + log::debug!("last_update for vault reserve feerate set to {}", height,); + } + } else { + log::debug!("Not enough blocks to compute vault reserve feerate"); + db_update_vault_reserve_feerate( + &db_path, + height, + vault_reserve_feerate.vault_reserve_feerate, + )?; + log::debug!("last_update for vault reserve feerate set to {}", height,); + } + } + Ok(()) } - Ok(()) } // We only do actual processing on new blocks. This puts a natural limit on the amount of work @@ -407,9 +441,17 @@ fn new_block( bitcoind: &BitcoinD, current_tip: &ChainTip, ) -> Result<(), PollerError> { - // Update the fee-bumping reserves estimates - // TODO - update_feerate_estimates(db_path, bitcoind, current_tip)?; + log::debug!("Begin processing new block: {:?}", current_tip.height); + // Update the vault_reserve_feerate + update_vault_reserve_feerate(db_path, bitcoind, current_tip)?; + + // Update the current feerate estimate + let feerate_estimate = bitcoind.feerate_estimate(1)?; + log::debug!( + "At block {} feerate estimate is {:?}", + current_tip.height, + feerate_estimate.feerate + ); // Any vault to forget and feebump coins to unregister? // TODO diff --git a/tests/test_chain.py b/tests/test_chain.py index d5a0d31..2f36afe 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -1,4 +1,6 @@ import os +from random import randint +from math import ceil from fixtures import * from test_framework.utils import COIN, DEPOSIT_ADDRESS, DERIV_INDEX, CSV @@ -131,15 +133,49 @@ def test_simple_spend_detection(miradord, bitcoind): miradord.wait_for_log(f"Forgetting about consumed vault at '{deposit_outpoint}'") +def test_vault_reserve_feerate_update(miradord, bitcoind): + """ + Check that the vault_reserve_feerate is updated as expected with each new block. + """ + START_BLOCK = 120 + WINDOW_LEN = 144 * 90 + HIGH_FEERATE = 2000 + # Starting at block 120, with no transactions in the next block the vault_reserve_feerate + # should not increase. + bitcoind.generate_block(1, []) + miradord.wait_for_logs([f"Not enough blocks to compute vault reserve feerate"]) + miradord.wait_for_logs( + [f"last_update for vault reserve feerate set to {START_BLOCK+1}"] + ) + + # FIXME: generating so many blocks takes me ~25 minutes + # for block in range(START_BLOCK+1, WINDOW_LEN+5): + # wait_for_mempool = [] + # for tx in range(0, 10): + # txid = bitcoind.generate_tx_with_feerate(HIGH_FEERATE) + # wait_for_mempool.append(txid) + # bitcoind.generate_block(1, wait_for_mempool) + + # miradord.wait_for_logs[f"vault reserve feerate updated to"] + + def test_feerate_estimation(miradord, bitcoind): + """ + Test estimatesmartfee usage and fallback + """ # Generate some transaction history for estimatesmartfee. - # 10 transactions in 50 blocks (send to deposit address) + # 10 transactions in 25 blocks (send to deposit address) amount = 1 - for block in range(0, 50): + for block in range(0, 25): wait_for_mempool = [] - for tx in range(0, 10): + for tx in range(0, randint(5, 15)): txid, outpoint = bitcoind.create_utxo(DEPOSIT_ADDRESS, amount) wait_for_mempool.append(txid) bitcoind.generate_block(1, wait_for_mempool) - feerate = bitcoind.estimatesmartfee(1)["feerate"].normalize() + + feerate = ceil( + bitcoind.estimatesmartfee(1)["feerate"] * 100000 + ) # Convert from BTC/kb to sat/vB miradord.wait_for_logs([f"feerate estimate is {feerate}"]) + + # FIXME: How to test fallback? diff --git a/tests/test_framework/bitcoind.py b/tests/test_framework/bitcoind.py index 84e2d44..5ab7c2c 100644 --- a/tests/test_framework/bitcoind.py +++ b/tests/test_framework/bitcoind.py @@ -116,6 +116,35 @@ def create_utxo(self, address, amount): return txid, f"{txid}:{vout}" vout += 1 + def generate_tx_with_feerate(self, feerate): + """Send 1 coin to an address""" + addr = self.rpc.getnewaddress() + amount = 1 + comment = "" + comment_to = "" + subtract_fee_amount = False + replaceable = False + conf_target = None + est_mode = "unset" + avoid_reuse = False + fee_rate = feerate + txid = self.rpc.sendtoaddress( + addr, + amount, + comment, + comment_to, + subtract_fee_amount, + replaceable, + conf_target, + est_mode, + avoid_reuse, + feerate, + ) + return txid + + def get_block_stats(self, block_height): + return self.rpc.getblockstats(block_height) + def get_coins(self, amount_btc): # subsidy halving is every 150 blocks on regtest, it's a rough estimate # to avoid looping in most cases