Skip to content

Commit

Permalink
Abstract send feature error variants into separate feature-specific a…
Browse files Browse the repository at this point in the history
…bstractions (payjoin#464)

This reduces the feature flag coupling toward payjoin#392 and works toward
payjoin#403.
  • Loading branch information
DanGould authored Jan 7, 2025
2 parents bb68226 + be7df15 commit 1baa8a0
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 119 deletions.
163 changes: 85 additions & 78 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,30 +86,67 @@ impl std::error::Error for BuildSenderError {
/// This is currently opaque type because we aren't sure which variants will stay.
/// You can only display it.
#[derive(Debug)]
pub struct ValidationError {
internal: InternalValidationError,
}
pub struct ValidationError(InternalValidationError);

#[derive(Debug)]
pub(crate) enum InternalValidationError {
Parse,
Io(std::io::Error),
Proposal(InternalProposalError),
#[cfg(feature = "v2")]
V2Encapsulation(crate::send::v2::EncapsulationError),
}

impl From<InternalValidationError> for ValidationError {
fn from(value: InternalValidationError) -> Self { ValidationError(value) }
}

impl From<crate::psbt::AddressTypeError> for ValidationError {
fn from(value: crate::psbt::AddressTypeError) -> Self {
ValidationError(InternalValidationError::Proposal(
InternalProposalError::InvalidAddressType(value),
))
}
}

impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use InternalValidationError::*;

match &self.0 {
Parse => write!(f, "couldn't decode as PSBT or JSON",),
Io(e) => write!(f, "couldn't read PSBT: {}", e),
Proposal(e) => write!(f, "proposal PSBT error: {}", e),
#[cfg(feature = "v2")]
V2Encapsulation(e) => write!(f, "v2 encapsulation error: {}", e),
}
}
}

impl std::error::Error for ValidationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use InternalValidationError::*;

match &self.0 {
Parse => None,
Io(error) => Some(error),
Proposal(e) => Some(e),
#[cfg(feature = "v2")]
V2Encapsulation(e) => Some(e),
}
}
}

/// Error that may occur when the proposal PSBT from receiver is malformed.
#[derive(Debug)]
pub(crate) enum InternalProposalError {
InvalidAddressType(crate::psbt::AddressTypeError),
NoInputs,
PrevTxOut(crate::psbt::PrevTxOutError),
InputWeight(crate::psbt::InputWeightError),
VersionsDontMatch {
proposed: Version,
original: Version,
},
LockTimesDontMatch {
proposed: LockTime,
original: LockTime,
},
SenderTxinSequenceChanged {
proposed: Sequence,
original: Sequence,
},
VersionsDontMatch { proposed: Version, original: Version },
LockTimesDontMatch { proposed: LockTime, original: LockTime },
SenderTxinSequenceChanged { proposed: Sequence, original: Sequence },
SenderTxinContainsNonWitnessUtxo,
SenderTxinContainsWitnessUtxo,
SenderTxinContainsFinalScriptSig,
Expand All @@ -119,10 +156,7 @@ pub(crate) enum InternalValidationError {
ReceiverTxinNotFinalized,
ReceiverTxinMissingUtxoInfo,
MixedSequence,
MixedInputTypes {
proposed: AddressType,
original: AddressType,
},
MixedInputTypes { proposed: AddressType, original: AddressType },
MissingOrShuffledInputs,
TxOutContainsKeyPaths,
FeeContributionExceedsMaximum,
Expand All @@ -134,33 +168,19 @@ pub(crate) enum InternalValidationError {
FeeContributionPaysOutputSizeIncrease,
FeeRateBelowMinimum,
Psbt(bitcoin::psbt::Error),
#[cfg(feature = "v2")]
Hpke(crate::hpke::HpkeError),
#[cfg(feature = "v2")]
OhttpEncapsulation(crate::ohttp::OhttpEncapsulationError),
#[cfg(feature = "v2")]
UnexpectedStatusCode,
#[cfg(feature = "v2")]
UnexpectedResponseSize(usize),
}

impl From<InternalValidationError> for ValidationError {
fn from(value: InternalValidationError) -> Self { ValidationError { internal: value } }
}

impl From<crate::psbt::AddressTypeError> for InternalValidationError {
impl From<crate::psbt::AddressTypeError> for InternalProposalError {
fn from(value: crate::psbt::AddressTypeError) -> Self {
InternalValidationError::InvalidAddressType(value)
InternalProposalError::InvalidAddressType(value)
}
}

impl fmt::Display for ValidationError {
impl fmt::Display for InternalProposalError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use InternalValidationError::*;
use InternalProposalError::*;

match &self.internal {
Parse => write!(f, "couldn't decode as PSBT or JSON",),
Io(e) => write!(f, "couldn't read PSBT: {}", e),
match &self {
InvalidAddressType(e) => write!(f, "invalid input address type: {}", e),
NoInputs => write!(f, "PSBT doesn't have any inputs"),
PrevTxOut(e) => write!(f, "missing previous txout information: {}", e),
Expand Down Expand Up @@ -189,25 +209,15 @@ impl fmt::Display for ValidationError {
FeeContributionPaysOutputSizeIncrease => write!(f, "fee contribution pays for additional outputs"),
FeeRateBelowMinimum => write!(f, "the fee rate of proposed transaction is below minimum"),
Psbt(e) => write!(f, "psbt error: {}", e),
#[cfg(feature = "v2")]
Hpke(e) => write!(f, "v2 error: {}", e),
#[cfg(feature = "v2")]
OhttpEncapsulation(e) => write!(f, "Ohttp encapsulation error: {}", e),
#[cfg(feature = "v2")]
UnexpectedStatusCode => write!(f, "unexpected status code"),
#[cfg(feature = "v2")]
UnexpectedResponseSize(size) => write!(f, "unexpected response size {}, expected {} bytes", size, crate::ohttp::ENCAPSULATED_MESSAGE_BYTES),
}
}
}

impl std::error::Error for ValidationError {
impl std::error::Error for InternalProposalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use InternalValidationError::*;
use InternalProposalError::*;

match &self.internal {
Parse => None,
Io(error) => Some(error),
match self {
InvalidAddressType(error) => Some(error),
NoInputs => None,
PrevTxOut(error) => Some(error),
Expand Down Expand Up @@ -236,14 +246,6 @@ impl std::error::Error for ValidationError {
FeeContributionPaysOutputSizeIncrease => None,
FeeRateBelowMinimum => None,
Psbt(error) => Some(error),
#[cfg(feature = "v2")]
Hpke(error) => Some(error),
#[cfg(feature = "v2")]
OhttpEncapsulation(error) => Some(error),
#[cfg(feature = "v2")]
UnexpectedStatusCode => None,
#[cfg(feature = "v2")]
UnexpectedResponseSize(_) => None,
}
}
}
Expand All @@ -256,17 +258,17 @@ pub enum ResponseError {
///
/// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
WellKnown(WellKnownError),

/// Errors caused by malformed responses.
Validation(ValidationError),

/// `Unrecognized` Errors are NOT defined in the [`BIP78::ReceiverWellKnownError`] spec.
///
/// Its not safe to display `Unrecognized` errors to end users as they could be used
/// It is NOT safe to display `Unrecognized` errors to end users as they could be used
/// maliciously to phish a non technical user. Only display them in debug logs.
///
/// [`BIP78::ReceiverWellKnownError`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_well_known_errors
Unrecognized { error_code: String, message: String },
/// Errors caused by malformed responses.
///
/// These errors are only displayed in debug logs.
Validation(ValidationError),
}

impl ResponseError {
Expand Down Expand Up @@ -323,19 +325,23 @@ impl From<WellKnownError> for ResponseError {
}

impl From<InternalValidationError> for ResponseError {
fn from(value: InternalValidationError) -> Self {
Self::Validation(ValidationError { internal: value })
fn from(value: InternalValidationError) -> Self { Self::Validation(ValidationError(value)) }
}

impl From<InternalProposalError> for ResponseError {
fn from(value: InternalProposalError) -> Self {
ResponseError::Validation(ValidationError(InternalValidationError::Proposal(value)))
}
}

// It is imperative to carefully display pre-defined messages to end users and the details in debug.
impl Display for ResponseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::WellKnown(e) => e.fmt(f),
// Don't display unknowns to end users, only debug logs
Self::Validation(e) => write!(f, "The receiver sent an invalid response: {}", e),

// Do NOT display unrecognized errors to end users, only debug logs
Self::Unrecognized { .. } => write!(f, "The receiver sent an unrecognized error."),
Self::Validation(_) => write!(f, "The receiver sent an invalid response."),
}
}
}
Expand All @@ -350,12 +356,13 @@ impl fmt::Debug for ResponseError {
e.error_code(),
e.message()
),
Self::Validation(e) => write!(f, "Validation({:?})", e),

Self::Unrecognized { error_code, message } => write!(
f,
r#"Unrecognized error: {{ "errorCode": "{}", "message": "{}" }}"#,
error_code, message
),
Self::Validation(e) => write!(f, "Validation({:?})", e),
}
}
}
Expand Down Expand Up @@ -419,17 +426,17 @@ mod tests {
_ => panic!("Expected WellKnown error"),
};
let unrecognized_error = r#"{"errorCode":"random", "message":"random"}"#;
assert_eq!(
ResponseError::parse(unrecognized_error).to_string(),
"The receiver sent an unrecognized error."
);
assert!(matches!(
ResponseError::parse(unrecognized_error),
ResponseError::Unrecognized { .. }
));
let invalid_json_error = json!({
"err": "random",
"message": "This version of payjoin is not supported."
});
assert_eq!(
ResponseError::from_json(invalid_json_error).to_string(),
"The receiver sent an invalid response."
);
assert!(matches!(
ResponseError::from_json(invalid_json_error),
ResponseError::Validation(_)
));
}
}
38 changes: 10 additions & 28 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::str::FromStr;
use bitcoin::psbt::Psbt;
use bitcoin::{Amount, FeeRate, Script, ScriptBuf, TxOut, Weight};
pub use error::{BuildSenderError, ResponseError, ValidationError};
pub(crate) use error::{InternalBuildSenderError, InternalValidationError};
pub(crate) use error::{InternalBuildSenderError, InternalProposalError, InternalValidationError};
use url::Url;

use crate::psbt::PsbtExt;
Expand All @@ -25,25 +25,7 @@ pub mod v1;
#[cfg(feature = "v2")]
pub mod v2;

type InternalResult<T> = Result<T, InternalValidationError>;

/// Data required to validate the response.
///
/// This type is used to process a BIP78 response.
/// Then call [`Self::process_response`] on it to continue BIP78 flow.
#[derive(Debug, Clone)]
pub struct V1Context {
psbt_context: PsbtContext,
}

impl V1Context {
pub fn process_response(
self,
response: &mut impl std::io::Read,
) -> Result<Psbt, ResponseError> {
self.psbt_context.process_response(response)
}
}
type InternalResult<T> = Result<T, InternalProposalError>;

/// Data required to validate the response against the original PSBT.
#[derive(Debug, Clone)]
Expand All @@ -60,7 +42,7 @@ macro_rules! check_eq {
($proposed:expr, $original:expr, $error:ident) => {
match ($proposed, $original) {
(proposed, original) if proposed != original =>
return Err(InternalValidationError::$error { proposed, original }),
return Err(InternalProposalError::$error { proposed, original }),
_ => (),
}
};
Expand All @@ -69,7 +51,7 @@ macro_rules! check_eq {
macro_rules! ensure {
($cond:expr, $error:ident) => {
if !($cond) {
return Err(InternalValidationError::$error);
return Err(InternalProposalError::$error);
}
};
}
Expand Down Expand Up @@ -100,8 +82,8 @@ impl PsbtContext {
}

fn check_fees(&self, proposal: &Psbt, contributed_fee: Amount) -> InternalResult<()> {
let proposed_fee = proposal.fee().map_err(InternalValidationError::Psbt)?;
let original_fee = self.original_psbt.fee().map_err(InternalValidationError::Psbt)?;
let proposed_fee = proposal.fee().map_err(InternalProposalError::Psbt)?;
let original_fee = self.original_psbt.fee().map_err(InternalProposalError::Psbt)?;
ensure!(original_fee <= proposed_fee, AbsoluteFeeDecreased);
ensure!(contributed_fee <= proposed_fee - original_fee, PayeeTookContributedFee);
let original_weight = self.original_psbt.clone().extract_tx_unchecked_fee_rate().weight();
Expand All @@ -112,7 +94,7 @@ impl PsbtContext {
.map(|input_pair| {
input_pair
.previous_txout()
.map_err(InternalValidationError::PrevTxOut)
.map_err(InternalProposalError::PrevTxOut)
.map(|txout| txout.script_pubkey.clone())
})
.collect::<InternalResult<Vec<ScriptBuf>>>()?;
Expand All @@ -121,14 +103,14 @@ impl PsbtContext {
|acc, input_pair| -> InternalResult<Weight> {
let spk = &input_pair
.previous_txout()
.map_err(InternalValidationError::PrevTxOut)?
.map_err(InternalProposalError::PrevTxOut)?
.script_pubkey;
if original_spks.contains(spk) {
Ok(acc)
} else {
let weight = input_pair
.expected_input_weight()
.map_err(InternalValidationError::InputWeight)?;
.map_err(InternalProposalError::InputWeight)?;
Ok(acc + weight)
}
},
Expand Down Expand Up @@ -196,7 +178,7 @@ impl PsbtContext {
.original_psbt
.input_pairs()
.next()
.ok_or(InternalValidationError::NoInputs)?;
.ok_or(InternalProposalError::NoInputs)?;
// Verify the PSBT input is finalized
ensure!(
proposed.psbtin.final_script_sig.is_some()
Expand Down
18 changes: 18 additions & 0 deletions payjoin/src/send/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,21 @@ impl Sender {

pub fn endpoint(&self) -> &Url { &self.endpoint }
}

/// Data required to validate the response.
///
/// This type is used to process a BIP78 response.
/// Then call [`Self::process_response`] on it to continue BIP78 flow.
#[derive(Debug, Clone)]
pub struct V1Context {
psbt_context: PsbtContext,
}

impl V1Context {
pub fn process_response(
self,
response: &mut impl std::io::Read,
) -> Result<Psbt, ResponseError> {
self.psbt_context.process_response(response)
}
}
Loading

0 comments on commit 1baa8a0

Please sign in to comment.