-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: convert fees from BTC/kB to sats/vB #136
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, see my comment over on the issue, but why not have estimate_fee
/batch_estimate_fee
return bitcoin::FeeRate
s?
Otherwise this would amount to a silent change of the API semantics that could have severe consequences for users that expect to get a fee rate in a certain denomination and now suddenly we'd return a different one.
I was just emulating what we already do in /// Get a fee value in sats/vbytes from the estimates
/// that matches the confirmation target set as parameter.
pub fn convert_fee_rate(target: usize, estimates: HashMap<u16, f64>) -> Result<f32, Error> {
let fee_val = {
let mut pairs = estimates.into_iter().collect::<Vec<(u16, f64)>>();
pairs.sort_unstable_by_key(|(k, _)| std::cmp::Reverse(*k));
pairs
.into_iter()
.find(|(k, _)| *k as usize <= target)
.map(|(_, v)| v)
.unwrap_or(1.0)
};
Ok(fee_val as f32)
} Shall we use |
I think it would be preferable, yes. At least a lot less error-prone and unambiguous than using a f32 or similar. |
I agree and like the idea. Will update the PR soon. Thanks! |
b20aa40
to
6dbda08
Compare
I was unable to get a correct fee estimation let res = client.estimate_fee(1).unwrap().to_sat_per_vb_ceil();
println!("{:#?}", res); returns
when the current recommended fee from mempool.space (I'd expect a similar, if not exact value) is {"fastestFee":5,"halfHourFee":5,"hourFee":5,"economyFee":4,"minimumFee":2} I think the convert fee rate function needs work. |
src/utils.rs
Outdated
@@ -41,3 +45,10 @@ pub fn validate_merkle_proof( | |||
|
|||
cur == merkle_root.to_raw_hash() | |||
} | |||
|
|||
/// Converts a fee rate in BTC/kB to sats/vbyte. | |||
pub fn convert_fee_rate(fee_rate: f64) -> FeeRate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be pub
. Possibly, we should also just inline the require calculation as it's trivial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree, you can just do this inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok moved to pub(crate)
because it's being called in other files.
src/utils.rs
Outdated
pub fn convert_fee_rate(fee_rate: f64) -> FeeRate { | ||
// 1 kB = 1_000 bytes | ||
// 1 vbyte = 4 weight units | ||
FeeRate::from_sat_per_kwu((fee_rate.ceil() as u64 * SATOSHIS_IN_BTC) / 4_000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems wrong. How about?:
pub fn convert_fee_rate(fee_rate_btc_kvb: f64) -> Option<FeeRate> {
/// Convert a fee rate in BTC/kvB to sats/vB
let fee_rate_sat_vb = (fee_rate_btc_kvb * 100_000.0) as u64;
FeeRate::from_sat_per_vb(fee_rate_sat_vb)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think same applies here, you probably want to also use Weight
too (if needed for the conversion).
for thought: Is this conversion something we could upstream to rust-bitcoin units module as a new FeeRate module ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/utils.rs
Outdated
@@ -41,3 +45,10 @@ pub fn validate_merkle_proof( | |||
|
|||
cur == merkle_root.to_raw_hash() | |||
} | |||
|
|||
/// Converts a fee rate in BTC/kB to sats/vbyte. | |||
pub fn convert_fee_rate(fee_rate: f64) -> FeeRate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree, you can just do this inline.
src/utils.rs
Outdated
pub fn convert_fee_rate(fee_rate: f64) -> FeeRate { | ||
// 1 kB = 1_000 bytes | ||
// 1 vbyte = 4 weight units | ||
FeeRate::from_sat_per_kwu((fee_rate.ceil() as u64 * SATOSHIS_IN_BTC) / 4_000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think same applies here, you probably want to also use Weight
too (if needed for the conversion).
for thought: Is this conversion something we could upstream to rust-bitcoin units module as a new FeeRate module ?
6dbda08
to
696ad72
Compare
I think the math is right, but the existing test might need to be updated. What you think? |
As discussed offline, I think the tests need fixing. |
696ad72
to
29f5e9f
Compare
The tests are CRAZY! They are even not reproducible. They pretty much ping a live server |
29f5e9f
to
496479c
Compare
Maybe we should switch to use a local regtest electrs that we pre-populate with a certain amount of transactions and blocks to make them ~reproducible? Or, we could consider building a mock Electrum server, which shouldn't be hard to do? Especially given the confusion around fee rate conversion it would be good to test it properly in CI? |
Moved to #142 so that we can move with this PR into review/merge mode. |
src/raw_client.rs
Outdated
impl_batch_call!(self, numbers, estimate_fee, apply_deref) | ||
let fee_rate_num: Result<Vec<f64>, Error> = | ||
impl_batch_call!(self, numbers, estimate_fee, apply_deref); | ||
let fee_rate: Vec<FeeRate> = fee_rate_num?.into_iter().map(convert_fee_rate).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid the additional allocation here by just collecting
a Vec
once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I modified the macro impl_batch_call!
. Any ideas on how to fix it? Very little experience with macros here...
src/utils.rs
Outdated
/// Converts a fee rate in BTC/kB to sats/vbyte. | ||
/// | ||
/// # Note | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Superfluous blank line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not superfluous. In rustdocs it is common to leave a blank line before and after headers, just like markdown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair, excuse the noise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I've removed since we are returning an error.
src/utils.rs
Outdated
/// 1 sat/vbyte. | ||
pub(crate) fn convert_fee_rate(fee_rate_kvb: f64) -> FeeRate { | ||
let fee_rate_sat_vb = (Amount::ONE_BTC.to_sat() as f64) * fee_rate_kvb; | ||
FeeRate::from_sat_per_vb(fee_rate_sat_vb as u64).unwrap_or(FeeRate::from_sat_per_vb(1).unwrap()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid the default fallback here? I think we're just about to drop this over at rust-esplora-client
(cf bitcoindevkit/rust-esplora-client#80), would make sense to also avoid it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now returns an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, though see #136 (comment)
496479c
to
3b93638
Compare
@@ -880,7 +882,8 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> { | |||
|
|||
result | |||
.as_f64() | |||
.ok_or_else(|| Error::InvalidResponse(result.clone())) | |||
.map(convert_fee_rate) | |||
.expect("Invalid response") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, please never panic on input we receive via network from a third party. Any calls to convert_fee_rate
also need to be fallible and the user needs to handle the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, and you can probably add a new error variant to Error
such as: CouldNotConvertFeeRate
, or reuse some of the existing ones.
Also changes all fee rates from f64 to proper FeeRate
3b93638
to
d9bd4aa
Compare
@@ -189,7 +189,7 @@ pub trait ElectrumApi { | |||
/// | |||
/// Takes a list of `numbers` of blocks and returns a list of fee required in | |||
/// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks. | |
/// [`FeeRate`] to confirm a transaction in the given number of blocks. |
/// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks. | ||
fn estimate_fee(&self, number: usize) -> Result<f64, Error>; | ||
/// Estimates the fee required in [`FeeRate`] to confirm a transaction in `number` blocks. | ||
fn estimate_fee(&self, number: usize) -> Result<FeeRate, Error>; | ||
|
||
/// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**. | |
/// Returns the minimum accepted fee by the server's node in [`FeeRate`]. |
@@ -880,7 +882,8 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> { | |||
|
|||
result | |||
.as_f64() | |||
.ok_or_else(|| Error::InvalidResponse(result.clone())) | |||
.map(convert_fee_rate) | |||
.expect("Invalid response") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, and you can probably add a new error variant to Error
such as: CouldNotConvertFeeRate
, or reuse some of the existing ones.
let resp = client.relay_fee().unwrap().to_sat_per_vb_ceil(); | ||
assert!(resp > 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably need some time to take a better look at the tests to wrap my mind around it, I'm afraid we can be missing something due to the current state of testing at crate level 🤔
/// There was an error parsing a fee rate | ||
FeeRate(String), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, now I see that you already added a new variant here.
@@ -365,6 +366,7 @@ impl Display for Error { | |||
Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"), | |||
Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"), | |||
Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"), | |||
Error::FeeRate(e) => f.write_str(e), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: You can probably leave the error message here, as it becomes a single point of change in the future.
Fix for bitcoindevkit/bdk#1519