Skip to content

Commit

Permalink
Parse optional parameters Params from query
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Aug 1, 2022
1 parent b92b718 commit 9c16190
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 6 deletions.
84 changes: 84 additions & 0 deletions bip78/src/params.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::fmt;

use crate::fee_rate::FeeRate;

pub(crate) struct Params {
// version
// v: usize,
// disableoutputsubstitution
pub disable_output_substitution: bool,
// maxadditionalfeecontribution, additionalfeeoutputindex
pub additional_fee_contribution: Option<(bitcoin::Amount, usize)>,
// minfeerate
pub min_feerate: FeeRate,
}

impl Default for Params {
fn default() -> Self {
Params {
disable_output_substitution: false,
additional_fee_contribution: None,
min_feerate: FeeRate::ZERO,
}
}
}

impl Params {
#[cfg(feature = "receiver")]
pub fn parse(query: &str) -> Result<Params, ParamsError> {

let mut params = Params::default();

let mut additional_fee_output_index = None;
let mut max_additional_fee_contribution = None;

let query = url::Url::parse(query).ok();
if let Some(query) = query {
for (k, v) in query.query_pairs().into_owned() {
match (k.as_str(), v) {
("v", v) => if v != "1" {
return Err(ParamsError::UnknownVersion)
},
("additionalfeeoutputindex", index) => {
if let Ok(index) = index.parse::<usize>() {
// Check for index out of bounds at fee application.
// Params doesn't need to know about psbt.
additional_fee_output_index = Some(index);
}
},
("maxadditionalfeecontribution", fee) => {
max_additional_fee_contribution = bitcoin::Amount::from_str_in(&fee, bitcoin::Denomination::Bitcoin).ok();
}
("minfeerate", feerate) => {
if let Ok(rate) = feerate.parse::<u64>() {
params.min_feerate = FeeRate::from_sat_per_vb(rate);
}
}
("disableoutputsubstitution", _) => params.disable_output_substitution = true, // existance is truthy
_ => (),
}
}
}
if let (Some(amount), Some(index)) = (max_additional_fee_contribution, additional_fee_output_index) {
// TODO check index in psbt, not our output
params.additional_fee_contribution = Some((amount, index));
}

Ok(params)
}
}

#[derive(Debug)]
pub(crate) enum ParamsError {
UnknownVersion,
}

impl fmt::Display for ParamsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParamsError::UnknownVersion => write!(f, "unknown version"),
}
}
}

impl std::error::Error for ParamsError {}
1 change: 1 addition & 0 deletions bip78/src/receiver/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) enum InternalRequestError {
InvalidContentType(String),
InvalidContentLength(std::num::ParseIntError),
ContentLengthTooLarge(u64),
SenderParams(crate::params::ParamsError)
}

impl From<InternalRequestError> for RequestError {
Expand Down
42 changes: 36 additions & 6 deletions bip78/src/receiver/mod.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, AddressType, Script, TxOut};

use crate::params::Params;
mod error;

use error::InternalRequestError;
pub use error::RequestError;


pub trait Headers {
fn get_header(&self, key: &str) -> Option<&str>;
}

pub struct UncheckedProposal {
psbt: Psbt,
params: Params,
}

pub struct MaybeInputsOwned {
psbt: Psbt,
params: Params,
}

pub struct MaybeMixedInputScripts {
psbt: Psbt,
params: Params,
}

pub struct MaybeInputsSeen {
psbt: Psbt,
params: Params,
}

impl UncheckedProposal {
pub fn from_request(
body: impl std::io::Read,
query: &str,
query: Option<&str>,
headers: impl Headers,
) -> Result<Self, RequestError> {
use crate::bitcoin::consensus::Decodable;
Expand All @@ -54,8 +60,16 @@ impl UncheckedProposal {
let reader = base64::read::DecoderReader::new(&mut limited, base64::STANDARD);
let psbt = Psbt::consensus_decode(reader).map_err(InternalRequestError::Decode)?;

let params = match query {
Some(query) => Params::parse(query).map_err(InternalRequestError::SenderParams)?,
None => Params::default(),
};

// TODO check params are valid for psbt

Ok(UncheckedProposal {
psbt,
params,
})
}

Expand All @@ -75,7 +89,10 @@ impl UncheckedProposal {
///
/// Call this after checking downstream.
pub fn assume_tested_and_scheduled_broadcast(self) -> MaybeInputsOwned {
MaybeInputsOwned { psbt: self.psbt }
MaybeInputsOwned {
psbt: self.psbt,
params: self.params,
}
}

/// Call this method if the only way to initiate a PayJoin with this receiver
Expand All @@ -84,7 +101,10 @@ impl UncheckedProposal {
/// So-called "non-interactive" receivers, like payment processors, that allow arbitrary requests are otherwise vulnerable to probing attacks.
/// Those receivers call `get_transaction_to_check_broadcast()` and `attest_tested_and_scheduled_broadcast()` after making those checks downstream.
pub fn assume_interactive_receive_endpoint(self) -> MaybeInputsOwned {
MaybeInputsOwned { psbt: self.psbt }
MaybeInputsOwned {
psbt: self.psbt,
params: self.params,
}
}
}

Expand All @@ -100,7 +120,10 @@ impl MaybeInputsOwned {
///
/// Call this after checking downstream.
pub fn assume_inputs_not_owned(self) -> MaybeMixedInputScripts {
MaybeMixedInputScripts { psbt: self.psbt }
MaybeMixedInputScripts {
psbt: self.psbt,
params: self.params,
}
}
}

Expand All @@ -119,7 +142,10 @@ impl MaybeMixedInputScripts {
/// Note: mixed spends do not necessarily indicate distinct wallet fingerprints.
/// This check is intended to prevent some types of wallet fingerprinting.
pub fn assume_no_mixed_input_scripts(self) -> MaybeInputsSeen {
MaybeInputsSeen { psbt: self.psbt }
MaybeInputsSeen {
psbt: self.psbt,
params: self.params,
}
}
}

Expand Down Expand Up @@ -224,6 +250,10 @@ pub mod test {

#[cfg(test)]
fn get_proposal_from_test_vector() -> Result<UncheckedProposal, RequestError> {
use std::str::FromStr;

use bitcoin::Amount;

use super::test_util::MockHeaders;

// OriginalPSBT Test Vector from BIP
Expand All @@ -234,7 +264,7 @@ pub mod test {

let body = original_psbt.as_bytes();
let headers = MockHeaders::from_vec(body);
UncheckedProposal::from_request(body, "", headers)
UncheckedProposal::from_request(body, Some("?maxadditionalfeecontribution=0.00000182?additionalfeeoutputindex=0"), headers)
}

#[test]
Expand Down

0 comments on commit 9c16190

Please sign in to comment.