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 Oct 27, 2022
1 parent a5f5b26 commit 98fb17f
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 8 deletions.
2 changes: 1 addition & 1 deletion bip78/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mod uri;
pub(crate) mod weight;
#[cfg(any(feature = "sender", feature = "receiver"))]
pub(crate) mod fee_rate;
#[cfg(any(feature = "sender", feature = "receiver"))]
#[cfg(any(feature = "receiver"))]
pub(crate) mod params;
#[cfg(any(feature = "sender", feature = "receiver"))]
pub(crate) mod psbt;
Expand Down
94 changes: 94 additions & 0 deletions bip78/src/params.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::borrow::Borrow;
use std::fmt;

use crate::fee_rate::FeeRate;

#[derive(Debug)]
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 from_query_pairs<K, V, I>(pairs: I) -> Result<Self, ParamsError>
where
I: Iterator<Item = (K, V)>,
K: Borrow<str> + Into<String>,
V: Borrow<str> + Into<String>,
{
let mut params = Params::default();

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

for (k, v) in pairs {
match (k.borrow(), v.borrow()) {
("v", v) => if v != "1" {
return Err(ParamsError::UnknownVersion)
},
("additionalfeeoutputindex", index) => {
if let Ok(index) = index.parse::<usize>() {
additional_fee_output_index = Some(index);
}
},
("maxadditionalfeecontribution", fee) => {
max_additional_fee_contribution = bitcoin::Amount::from_str_in(&fee, bitcoin::Denomination::Bitcoin).ok();
}
("minfeerate", feerate) => {
params.min_feerate = match feerate.parse::<u64>() {
Ok(rate) => FeeRate::from_sat_per_vb(rate),
Err(e) => return Err(ParamsError::FeeRate(e)),
}
}
("disableoutputsubstitution", v) => params.disable_output_substitution = v == "true", // existance is truthy
_ => (),
}
}
if let (Some(amount), Some(index)) = (max_additional_fee_contribution, additional_fee_output_index) {
params.additional_fee_contribution = Some((amount, index));
}

Ok(params)
}
}

#[derive(Debug)]
pub(crate) enum ParamsError {
UnknownVersion,
FeeRate(std::num::ParseIntError),
}

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

impl std::error::Error for ParamsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ParamsError::FeeRate(error) => Some(error),
_ => None,
}
}
}
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
18 changes: 18 additions & 0 deletions bip78/src/receiver/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::collections::HashMap;

pub struct MockHeaders(HashMap<String, String>);

impl crate::receiver::Headers for MockHeaders {
fn get_header(&self, key: &str) -> Option<&str> {
self.0.get(key).map(|e| e.as_str())
}
}

impl MockHeaders {
pub fn from_slice(body: &[u8]) -> MockHeaders {
let mut h = HashMap::new();
h.insert("content-type".to_string(), "text/plain".to_string());
h.insert("content-length".to_string(), body.len().to_string());
MockHeaders(h)
}
}
36 changes: 30 additions & 6 deletions bip78/src/receiver/mod.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, AddressType, Script, TxOut};

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

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


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 {
Expand Down Expand Up @@ -52,8 +58,14 @@ impl UncheckedProposal {
let reader = base64::read::DecoderReader::new(&mut limited, base64::STANDARD);
let psbt = Psbt::consensus_decode(reader).map_err(InternalRequestError::Decode)?;

let pairs = url::form_urlencoded::parse(query.as_bytes());
let params = Params::from_query_pairs(pairs).map_err(InternalRequestError::SenderParams)?;

// TODO check params are valid for request Origianl PSBT

Ok(UncheckedProposal {
psbt,
params,
})
}

Expand All @@ -75,7 +87,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 +99,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 @@ -102,7 +120,10 @@ impl MaybeInputsOwned {
/// An attacker could try to spend receiver's own inputs. This check prevents that.
/// 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 @@ -121,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 @@ -232,7 +256,7 @@ mod test {

let body = original_psbt.as_bytes();
let headers = MockHeaders::new(body.len() as u64);
UncheckedProposal::from_request(body, "", headers)
UncheckedProposal::from_request(body,"?maxadditionalfeecontribution=0.00000182?additionalfeeoutputindex=0", headers)
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion bip78/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ mod integration {
let headers = HeaderMock::from_vec(&req.body);

// Receiver receive payjoin proposal, IRL it will be an HTTP request (over ssl or onion)
let proposal = bip78::receiver::UncheckedProposal::from_request(req.body.as_slice(), "", headers).unwrap();
let proposal = bip78::receiver::UncheckedProposal::from_request(req.body.as_slice(), req.url.query().unwrap_or(""), headers).unwrap();

// TODO
}
Expand Down

0 comments on commit 98fb17f

Please sign in to comment.