Merge rust-bitcoin/rust-bitcoin#1338: Implement PartiallySignedTransaction::fee

c34d5f8f85 Implement PartiallySignedTransaction::fee (hashmap)

Pull request description:

  to calculate fee if previous outputs are available.
  Closes https://github.com/rust-bitcoin/rust-bitcoin/issues/1220

ACKs for top commit:
  Kixunil:
    ACK c34d5f8f85 if CI passes
  tcharding:
    ACK c34d5f8f85
  apoelstra:
    ACK c34d5f8f85

Tree-SHA512: 697b837de2fb21bbd5d489c524c06a56bb35b73c0f32cc5b0500f5508f3c539b21d327cd556a04ee847ccf8d98829da994d90c19e80c457ddba2cd9d3469476e
This commit is contained in:
Andrew Poelstra 2022-10-24 23:31:45 +00:00
commit 29d3bc0108
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
2 changed files with 133 additions and 2 deletions

View File

@ -74,6 +74,10 @@ pub enum Error {
CombineInconsistentKeySources(Box<ExtendedPubKey>), CombineInconsistentKeySources(Box<ExtendedPubKey>),
/// Serialization error in bitcoin consensus-encoded structures /// Serialization error in bitcoin consensus-encoded structures
ConsensusEncoding, ConsensusEncoding,
/// Negative fee
NegativeFee,
/// Integer overflow in fee calculation
FeeOverflow,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -101,6 +105,9 @@ impl fmt::Display for Error {
}, },
Error::CombineInconsistentKeySources(ref s) => { write!(f, "combine conflict: {}", s) }, Error::CombineInconsistentKeySources(ref s) => { write!(f, "combine conflict: {}", s) },
Error::ConsensusEncoding => f.write_str("bitcoin consensus or BIP-174 encoding error"), Error::ConsensusEncoding => f.write_str("bitcoin consensus or BIP-174 encoding error"),
Error::NegativeFee => f.write_str("PSBT has a negative fee which is not allowed"),
Error::FeeOverflow => f.write_str("integer overflow in fee calculation"),
} }
} }
} }
@ -128,7 +135,9 @@ impl std::error::Error for Error {
| NonStandardSighashType(_) | NonStandardSighashType(_)
| InvalidPreimageHashPair{ .. } | InvalidPreimageHashPair{ .. }
| CombineInconsistentKeySources(_) | CombineInconsistentKeySources(_)
| ConsensusEncoding => None, | ConsensusEncoding
| NegativeFee
| FeeOverflow => None,
} }
} }
} }

View File

@ -16,7 +16,7 @@ use core::ops::Deref;
use secp256k1::{Message, Secp256k1, Signing}; use secp256k1::{Message, Secp256k1, Signing};
use bitcoin_internals::write_err; use bitcoin_internals::write_err;
use crate::prelude::*; use crate::{prelude::*, Amount};
use crate::io; use crate::io;
use crate::blockdata::script::Script; use crate::blockdata::script::Script;
@ -437,6 +437,28 @@ impl PartiallySignedTransaction {
// because there has been a new softfork that we do not yet support. // because there has been a new softfork that we do not yet support.
Err(SignError::UnknownOutputType) Err(SignError::UnknownOutputType)
} }
/// Calculates transaction fee.
///
/// 'Fee' being the amount that will be paid for mining a transaction with the current inputs
/// and outputs i.e., the difference in value of the total inputs and the total outputs.
///
/// ## Errors
///
/// - [`Error::MissingUtxo`] when UTXO information for any input is not present or is invalid.
/// - [`Error::NegativeFee`] if calculated value is negative.
/// - [`Error::FeeOverflow`] if an integer overflow occurs.
pub fn fee(&self) -> Result<Amount, Error> {
let mut inputs: u64 = 0;
for utxo in self.iter_funding_utxos() {
inputs = inputs.checked_add(utxo?.value).ok_or(Error::FeeOverflow)?;
}
let mut outputs: u64 = 0;
for out in &self.unsigned_tx.output {
outputs = outputs.checked_add(out.value).ok_or(Error::FeeOverflow)?;
}
inputs.checked_sub(outputs).map(Amount::from_sat).ok_or(Error::NegativeFee)
}
} }
/// Data required to call [`GetKey`] to get the private key to sign an input. /// Data required to call [`GetKey`] to get the private key to sign an input.
@ -1689,6 +1711,106 @@ mod tests {
assert_eq!(got.unwrap(), priv_key) assert_eq!(got.unwrap(), priv_key)
} }
#[test]
fn test_fee() {
let output_0_val = 99999699;
let output_1_val = 100000000;
let prev_output_val = 200000000;
let mut t = PartiallySignedTransaction {
unsigned_tx: Transaction {
version: 2,
lock_time: absolute::PackedLockTime(1257139),
input: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_hex(
"f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126",
).unwrap(),
vout: 0,
},
sequence: Sequence::ENABLE_LOCKTIME_NO_RBF,
..Default::default()
}],
output: vec![
TxOut {
value: output_0_val,
..Default::default()
},
TxOut {
value: output_1_val,
..Default::default()
},
],
},
xpub: Default::default(),
version: 0,
proprietary: BTreeMap::new(),
unknown: BTreeMap::new(),
inputs: vec![Input {
non_witness_utxo: Some(Transaction {
version: 1,
lock_time: absolute::PackedLockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: Txid::from_hex(
"e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389",
).unwrap(),
vout: 1,
},
sequence: Sequence::MAX,
..Default::default()
},
TxIn {
previous_output: OutPoint {
txid: Txid::from_hex(
"b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886",
).unwrap(),
vout: 1,
},
sequence: Sequence::MAX,
..Default::default()
}],
output: vec![
TxOut {
value: prev_output_val,
..Default::default()
},
TxOut {
value: 190303501938,
..Default::default()
},
],
}),
..Default::default()
},],
outputs: vec![
Output {
..Default::default()
},
Output {
..Default::default()
},
],
};
assert_eq!(
t.fee().expect("fee calculation"),
Amount::from_sat(prev_output_val - (output_0_val + output_1_val))
);
// no previous output
let mut t2 = t.clone();
t2.inputs[0].non_witness_utxo = None;
assert_eq!(t2.fee(), Err(Error::MissingUtxo));
// negative fee
let mut t3 = t.clone();
t3.unsigned_tx.output[0].value = prev_output_val;
assert_eq!(t3.fee(), Err(Error::NegativeFee));
// overflow
t.unsigned_tx.output[0].value = u64::max_value();
t.unsigned_tx.output[1].value = u64::max_value();
assert_eq!(t.fee(), Err(Error::FeeOverflow));
}
#[test] #[test]
#[cfg(feature = "rand")] #[cfg(feature = "rand")]
fn sign_psbt() { fn sign_psbt() {