Skip to content

Commit

Permalink
v2: Add Finalizer and Extractor Roles
Browse files Browse the repository at this point in the history
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
tcharding committed Jan 30, 2024
1 parent 4295d7a commit 0aa5252
Show file tree
Hide file tree
Showing 10 changed files with 1,451 additions and 173 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ no-std = ["bitcoin/no-std", "core2"]
serde = ["actual-serde", "bitcoin/serde"]
base64 = ["bitcoin/base64"]

# TODO: There is curently no way to turn on miniscript/serde and base64
miniscript-std = ["std", "miniscript/std"]
miniscript-no-std = ["no-std", "miniscript/no-std"]

[dependencies]
bitcoin = { version = "0.31.0", default-features = false, features = [] }

# Consider using "miniscript-std" or "miniscript-no-std"
# Currenty miniscript only works in with "std" enabled.
miniscript = { version = "11.0.0", default-features = false, optional = true }

# Do NOT use this as a feature! Use the `serde` feature instead.
actual-serde = { package = "serde", version = "1.0.103", default-features = false, features = [ "derive", "alloc" ], optional = true }
# There is no reason to use this dependency directly, it is activated by the "no-std" feature.
Expand Down
5 changes: 2 additions & 3 deletions examples/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use psbt::bitcoin::{
script, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, TxOut, Txid,
};
use psbt::v2::{
self, Constructor, InputBuilder, Modifiable, Output, OutputBuilder, Psbt, Signer,
Updater,
self, Constructor, InputBuilder, Modifiable, Output, OutputBuilder, Psbt, Signer, Updater,
};

pub const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
Expand Down Expand Up @@ -64,7 +63,7 @@ fn main() -> anyhow::Result<()> {

// If no lock time is required we can just create the `Input` directly.
let input_b = InputBuilder::new(previous_output_b)
// .segwit_fund(txout); TODO: Add funding utxo.
// .segwit_fund(txout); TODO: Add funding utxo.
.build();

// Build Alice's change output.
Expand Down
60 changes: 2 additions & 58 deletions src/v2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

use core::fmt;

use bitcoin::{sighash, FeeRate, Transaction};
use bitcoin::sighash;

use crate::error::{write_err, FundingUtxoError};
use crate::v2::map::{global, input, output};
use crate::v2::Psbt;

/// Error while deserializing a PSBT.
///
Expand Down Expand Up @@ -102,59 +101,6 @@ impl std::error::Error for IndexOutOfBoundsError {
}
}

/// This error is returned when extracting a [`Transaction`] from a PSBT..
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtractTxError {
/// The [`FeeRate`] is too high
AbsurdFeeRate {
/// The [`FeeRate`]
fee_rate: FeeRate,
/// The extracted [`Transaction`] (use this to ignore the error)
tx: Transaction,
},
/// One or more of the inputs lacks value information (witness_utxo or non_witness_utxo)
MissingInputValue {
/// The extracted [`Transaction`] (use this to ignore the error)
tx: Transaction,
},
/// Input value is less than Output Value, and the [`Transaction`] would be invalid.
SendingTooMuch {
/// The original `Psbt` is returned untouched.
psbt: Psbt,
},
}

impl fmt::Display for ExtractTxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ExtractTxError::*;

match *self {
AbsurdFeeRate { fee_rate, .. } =>
write!(f, "An absurdly high fee rate of {}", fee_rate),
MissingInputValue { .. } => write!(
f,
"One of the inputs lacked value information (witness_utxo or non_witness_utxo)"
),
SendingTooMuch { .. } => write!(
f,
"Transaction would be invalid due to output value being greater than input value."
),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for ExtractTxError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ExtractTxError::*;

match *self {
AbsurdFeeRate { .. } | MissingInputValue { .. } | SendingTooMuch { .. } => None,
}
}
}

/// Errors encountered while calculating the sighash message.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
Expand Down Expand Up @@ -302,9 +248,7 @@ impl fmt::Display for InputsNotModifiableError {
}

#[cfg(feature = "std")]
impl std::error::Error for InputsNotModifiableError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
impl std::error::Error for InputsNotModifiableError {}

/// Error when passing an PSBT with outputs not modifiable to an output adding `Constructor`.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
218 changes: 218 additions & 0 deletions src/v2/extract.rs
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) }
}
Loading

0 comments on commit 0aa5252

Please sign in to comment.