Merge rust-bitcoin/rust-bitcoin#2230: Add effective value calculation

d69d62822d Add effective_value method (yancy)

Pull request description:

  Draft PR for adding effective value calculation to TxOut.  Adding this method was discussed here: https://github.com/rust-bitcoin/rust-bitcoin/pull/2217

ACKs for top commit:
  Kixunil:
    ACK d69d62822d
  apoelstra:
    ACK d69d62822d

Tree-SHA512: e74788fea2f97a10ccf2015406178bdac99a54207fe51f0c830937d2106d564beeebe6f70b49aded572aa17ccb42f47e25905a8f791bd9b442eff223ea59baae
This commit is contained in:
Andrew Poelstra 2024-01-16 14:17:16 +00:00
commit a3d698ac7c
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
1 changed files with 62 additions and 2 deletions

View File

@ -19,11 +19,11 @@ use internals::write_err;
use io::{Read, Write}; use io::{Read, Write};
use super::Weight; use super::Weight;
use crate::blockdata::fee_rate::FeeRate;
use crate::blockdata::locktime::absolute::{self, Height, Time}; use crate::blockdata::locktime::absolute::{self, Height, Time};
use crate::blockdata::locktime::relative; use crate::blockdata::locktime::relative;
use crate::blockdata::script::{Script, ScriptBuf}; use crate::blockdata::script::{Script, ScriptBuf};
use crate::blockdata::witness::Witness; use crate::blockdata::witness::Witness;
use crate::blockdata::FeeRate;
use crate::consensus::{encode, Decodable, Encodable}; use crate::consensus::{encode, Decodable, Encodable};
use crate::internal_macros::{impl_consensus_encoding, impl_hashencode}; use crate::internal_macros::{impl_consensus_encoding, impl_hashencode};
use crate::parse::impl_parse_str_from_int_infallible; use crate::parse::impl_parse_str_from_int_infallible;
@ -31,7 +31,7 @@ use crate::prelude::*;
#[cfg(doc)] #[cfg(doc)]
use crate::sighash::{EcdsaSighashType, TapSighashType}; use crate::sighash::{EcdsaSighashType, TapSighashType};
use crate::string::FromHexStr; use crate::string::FromHexStr;
use crate::{Amount, VarInt}; use crate::{Amount, SignedAmount, VarInt};
#[rustfmt::skip] // Keep public re-exports separate. #[rustfmt::skip] // Keep public re-exports separate.
#[cfg(feature = "bitcoinconsensus")] #[cfg(feature = "bitcoinconsensus")]
@ -226,6 +226,12 @@ pub struct TxIn {
} }
impl TxIn { impl TxIn {
/// Returns the input base weight.
///
/// Base weight excludes the witness and script.
const BASE_WEIGHT: Weight =
Weight::from_vb_unwrap(OutPoint::SIZE as u64 + Sequence::SIZE as u64);
/// Returns true if this input enables the [`absolute::LockTime`] (aka `nLockTime`) of its /// Returns true if this input enables the [`absolute::LockTime`] (aka `nLockTime`) of its
/// [`Transaction`]. /// [`Transaction`].
/// ///
@ -1168,6 +1174,29 @@ impl From<&Transaction> for Wtxid {
fn from(tx: &Transaction) -> Wtxid { tx.wtxid() } fn from(tx: &Transaction) -> Wtxid { tx.wtxid() }
} }
/// Computes the value of an output accounting for the cost of spending it.
///
/// The effective value is the value of an output value minus the amount to spend it. That is, the
/// effective_value can be calculated as: value - (fee_rate * weight).
///
/// Note: the effective value of a [`Transaction`] may increase less than the effective value of
/// a [`TxOut`] when adding another [`TxOut`] to the transaction. This happens when the new
/// [`TxOut`] added causes the output length `VarInt` to increase its encoding length.
///
/// # Arguments
///
/// * `fee_rate` - the fee rate of the transaction being created.
/// * `satisfaction_weight` - satisfied spending conditions weight.
pub fn effective_value(
fee_rate: FeeRate,
satisfaction_weight: Weight,
value: Amount,
) -> Option<SignedAmount> {
let weight = satisfaction_weight.checked_add(TxIn::BASE_WEIGHT)?;
let signed_input_fee = fee_rate.checked_mul_by_weight(weight)?.to_signed().ok()?;
value.to_signed().ok()?.checked_sub(signed_input_fee)
}
/// Predicts the weight of a to-be-constructed transaction. /// Predicts the weight of a to-be-constructed transaction.
/// ///
/// This function computes the weight of a transaction which is not fully known. All that is needed /// This function computes the weight of a transaction which is not fully known. All that is needed
@ -1989,6 +2018,37 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
} }
#[test]
fn effective_value_happy_path() {
let value = Amount::from_str("1 cBTC").unwrap();
let fee_rate = FeeRate::from_sat_per_kwu(10);
let satisfaction_weight = Weight::from_wu(204);
let effective_value = effective_value(fee_rate, satisfaction_weight, value).unwrap();
// 10 sat/kwu * (204wu + BASE_WEIGHT) = 4 sats
let expected_fee = SignedAmount::from_str("4 sats").unwrap();
let expected_effective_value = value.to_signed().unwrap() - expected_fee;
assert_eq!(effective_value, expected_effective_value);
}
#[test]
fn effective_value_fee_rate_does_not_overflow() {
let eff_value = effective_value(FeeRate::MAX, Weight::ZERO, Amount::ZERO);
assert!(eff_value.is_none());
}
#[test]
fn effective_value_weight_does_not_overflow() {
let eff_value = effective_value(FeeRate::ZERO, Weight::MAX, Amount::ZERO);
assert!(eff_value.is_none());
}
#[test]
fn effective_value_value_does_not_overflow() {
let eff_value = effective_value(FeeRate::ZERO, Weight::ZERO, Amount::MAX);
assert!(eff_value.is_none());
}
#[test] #[test]
fn txin_txout_weight() { fn txin_txout_weight() {
// [(is_segwit, tx_hex, expected_weight)] // [(is_segwit, tx_hex, expected_weight)]