-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
v2: Add Finalizer and Extractor Roles
Add types for the two new roles. Feature gate the `Finalizer` on "miniscript". Because another entity may be the Finalizer do not feature gate the `Extractor` oven thought transaction extraction is only possible for a finalized PSBT.
- Loading branch information
Showing
10 changed files
with
1,451 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
// SPDX-License-Identifier: CC0-1.0 | ||
|
||
//! Implementation of the Extractor role as defined in [BIP-174]. | ||
//! | ||
//! # Extractor Role | ||
//! | ||
//! > The Transaction Extractor does not need to know how to interpret scripts in order | ||
//! > to extract the network serialized transaction. | ||
//! | ||
//! It is only possible to extract a transaction from a PSBT _after_ it has been finalized. However | ||
//! the Extractor role may be fulfilled by a separate entity to the Finalizer hence this is a | ||
//! separate module and does not require `rust-miniscript`. | ||
//! | ||
//! [BIP-174]: <https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki> | ||
|
||
use core::fmt; | ||
|
||
use bitcoin::{FeeRate, Transaction}; | ||
|
||
use crate::error::{write_err, FeeError}; | ||
use crate::v2::{DetermineLockTimeError, Psbt}; | ||
|
||
/// Implements the BIP-370 Finalized role. | ||
#[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] | ||
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] | ||
pub struct Extractor(Psbt); | ||
|
||
impl Extractor { | ||
/// Creates an `Extractor`. | ||
/// | ||
/// An extractor can only accept a PSBT that has been finalized. | ||
pub fn new(psbt: Psbt) -> Result<Self, PsbtNotFinalizedError> { | ||
if psbt.inputs.iter().any(|input| !input.is_finalized()) { | ||
return Err(PsbtNotFinalizedError); | ||
} | ||
|
||
Ok(Self(psbt)) | ||
} | ||
} | ||
|
||
impl Extractor { | ||
/// The default `max_fee_rate` value used for extracting transactions with [`extract_tx`] | ||
/// | ||
/// As of 2023, even the biggest overpayers during the highest fee markets only paid around | ||
/// 1000 sats/vByte. 25k sats/vByte is obviously a mistake at this point. | ||
/// | ||
/// [`extract_tx`]: Psbt::extract_tx | ||
pub const DEFAULT_MAX_FEE_RATE: FeeRate = FeeRate::from_sat_per_vb_unchecked(25_000); | ||
|
||
/// An alias for [`extract_tx_fee_rate_limit`]. | ||
/// | ||
/// [`extract_tx_fee_rate_limit`]: Psbt::extract_tx_fee_rate_limit | ||
pub fn extract_tx(&self) -> Result<Transaction, ExtractTxFeeRateError> { | ||
self.internal_extract_tx_with_fee_rate_limit(Self::DEFAULT_MAX_FEE_RATE) | ||
} | ||
|
||
/// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information. | ||
/// | ||
/// ## Errors | ||
/// | ||
/// `ExtractTxError` variants will contain either the [`Psbt`] itself or the [`Transaction`] | ||
/// that was extracted. These can be extracted from the Errors in order to recover. | ||
/// See the error documentation for info on the variants. In general, it covers large fees. | ||
pub fn extract_tx_fee_rate_limit(&self) -> Result<Transaction, ExtractTxFeeRateError> { | ||
self.internal_extract_tx_with_fee_rate_limit(Self::DEFAULT_MAX_FEE_RATE) | ||
} | ||
|
||
/// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information. | ||
/// | ||
/// ## Errors | ||
/// | ||
/// See [`extract_tx`]. | ||
/// | ||
/// [`extract_tx`]: Psbt::extract_tx | ||
pub fn extract_tx_with_fee_rate_limit( | ||
&self, | ||
max_fee_rate: FeeRate, | ||
) -> Result<Transaction, ExtractTxFeeRateError> { | ||
self.internal_extract_tx_with_fee_rate_limit(max_fee_rate) | ||
} | ||
|
||
/// Perform [`extract_tx_fee_rate_limit`] without the fee rate check. | ||
/// | ||
/// This can result in a transaction with absurdly high fees. Use with caution. | ||
/// | ||
/// [`extract_tx_fee_rate_limit`]: Psbt::extract_tx_fee_rate_limit | ||
pub fn extract_tx_unchecked_fee_rate(&self) -> Result<Transaction, ExtractTxError> { | ||
self.internal_extract_tx() | ||
} | ||
|
||
#[inline] | ||
fn internal_extract_tx_with_fee_rate_limit( | ||
&self, | ||
max_fee_rate: FeeRate, | ||
) -> Result<Transaction, ExtractTxFeeRateError> { | ||
let fee = self.0.fee()?; | ||
let tx = self.internal_extract_tx()?; | ||
|
||
// Now that the extracted Transaction is made, decide how to return it. | ||
let fee_rate = | ||
FeeRate::from_sat_per_kwu(fee.to_sat().saturating_mul(1000) / tx.weight().to_wu()); | ||
// Prefer to return an AbsurdFeeRate error when both trigger. | ||
if fee_rate > max_fee_rate { | ||
return Err(ExtractTxFeeRateError::FeeTooHigh { fee: fee_rate, max: max_fee_rate }); | ||
} | ||
|
||
Ok(tx) | ||
} | ||
|
||
/// Extracts a finalized transaction from the [`Psbt`]. | ||
/// | ||
/// Uses `miniscript` to do interpreter checks. | ||
#[inline] | ||
fn internal_extract_tx(&self) -> Result<Transaction, ExtractTxError> { | ||
if !self.0.is_finalized() { | ||
return Err(ExtractTxError::Unfinalized); | ||
} | ||
|
||
let lock_time = self.0.determine_lock_time()?; | ||
|
||
let tx = Transaction { | ||
version: self.0.global.tx_version, | ||
lock_time, | ||
input: self.0.inputs.iter().map(|input| input.signed_tx_in()).collect(), | ||
output: self.0.outputs.iter().map(|ouput| ouput.tx_out()).collect(), | ||
}; | ||
|
||
Ok(tx) | ||
} | ||
} | ||
|
||
/// Attempted to extract tx from an unfinalized PSBT. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
#[non_exhaustive] | ||
pub struct PsbtNotFinalizedError; | ||
|
||
impl fmt::Display for PsbtNotFinalizedError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "attempted to extract tx from an unfinalized PSBT") | ||
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
impl std::error::Error for PsbtNotFinalizedError {} | ||
|
||
/// Error caused by fee calculation when extracting a [`Transaction`] from a PSBT. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
#[non_exhaustive] | ||
pub enum ExtractTxFeeRateError { | ||
/// Error calculating the fee rate. | ||
Fee(FeeError), | ||
/// The calculated fee rate exceeds max. | ||
FeeTooHigh { | ||
/// Calculated fee. | ||
fee: FeeRate, | ||
/// Maximum allowable fee. | ||
max: FeeRate, | ||
}, | ||
/// Error extracting the transaction. | ||
ExtractTx(ExtractTxError), | ||
} | ||
|
||
impl fmt::Display for ExtractTxFeeRateError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
use ExtractTxFeeRateError::*; | ||
|
||
match *self { | ||
Fee(ref e) => write_err!(f, "fee calculation"; e), | ||
FeeTooHigh { fee, max } => write!(f, "fee {} is greater than max {}", fee, max), | ||
ExtractTx(ref e) => write_err!(f, "extract"; e), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
impl std::error::Error for ExtractTxFeeRateError { | ||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | ||
use ExtractTxFeeRateError::*; | ||
|
||
match *self { | ||
Fee(ref e) => Some(e), | ||
ExtractTx(ref e) => Some(e), | ||
FeeTooHigh { .. } => None, | ||
} | ||
} | ||
} | ||
|
||
impl From<FeeError> for ExtractTxFeeRateError { | ||
fn from(e: FeeError) -> Self { Self::Fee(e) } | ||
} | ||
|
||
impl From<ExtractTxError> for ExtractTxFeeRateError { | ||
fn from(e: ExtractTxError) -> Self { Self::ExtractTx(e) } | ||
} | ||
|
||
/// Error extracting a [`Transaction`] from a PSBT. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
#[non_exhaustive] | ||
pub enum ExtractTxError { | ||
/// Attempted to extract transaction from an unfinalized PSBT. | ||
Unfinalized, | ||
/// Failed to determine lock time. | ||
DetermineLockTime(DetermineLockTimeError), | ||
} | ||
|
||
impl fmt::Display for ExtractTxError { | ||
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!() } | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
impl std::error::Error for ExtractTxError { | ||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { todo!() } | ||
} | ||
|
||
impl From<DetermineLockTimeError> for ExtractTxError { | ||
fn from(e: DetermineLockTimeError) -> Self { Self::DetermineLockTime(e) } | ||
} |
Oops, something went wrong.