From 5f497e11deabbe83461661b8b05f0a3ed158a0bd Mon Sep 17 00:00:00 2001 From: DanGould Date: Mon, 1 Aug 2022 16:05:50 +0800 Subject: [PATCH] Parse optional parameters Params from query --- bip78/src/lib.rs | 2 +- bip78/src/params.rs | 93 +++++++++++++++++++++++++++++++++++++ bip78/src/receiver/error.rs | 1 + bip78/src/receiver/mock.rs | 18 +++++++ bip78/src/receiver/mod.rs | 36 +++++++++++--- bip78/tests/integration.rs | 2 +- 6 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 bip78/src/params.rs create mode 100644 bip78/src/receiver/mock.rs diff --git a/bip78/src/lib.rs b/bip78/src/lib.rs index 19d5bb4f..82798b34 100644 --- a/bip78/src/lib.rs +++ b/bip78/src/lib.rs @@ -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; diff --git a/bip78/src/params.rs b/bip78/src/params.rs new file mode 100644 index 00000000..49b5d9f3 --- /dev/null +++ b/bip78/src/params.rs @@ -0,0 +1,93 @@ +use std::borrow::Borrow; +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 from_query_pairs(pairs: I) -> Result + where + I: Iterator, + K: Borrow + Into, + V: Borrow + Into, + { + 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::() { + 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::() { + 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, + } + } +} diff --git a/bip78/src/receiver/error.rs b/bip78/src/receiver/error.rs index 36109ddb..56783045 100644 --- a/bip78/src/receiver/error.rs +++ b/bip78/src/receiver/error.rs @@ -8,6 +8,7 @@ pub(crate) enum InternalRequestError { InvalidContentType(String), InvalidContentLength(std::num::ParseIntError), ContentLengthTooLarge(u64), + SenderParams(crate::params::ParamsError) } impl From for RequestError { diff --git a/bip78/src/receiver/mock.rs b/bip78/src/receiver/mock.rs new file mode 100644 index 00000000..7d5e3989 --- /dev/null +++ b/bip78/src/receiver/mock.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +pub struct MockHeaders(HashMap); + +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) + } +} \ No newline at end of file diff --git a/bip78/src/receiver/mod.rs b/bip78/src/receiver/mod.rs index c1ccb165..b199c78b 100644 --- a/bip78/src/receiver/mod.rs +++ b/bip78/src/receiver/mod.rs @@ -1,9 +1,11 @@ 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>; @@ -11,18 +13,22 @@ pub trait Headers { 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 { @@ -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, }) } @@ -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 @@ -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, + } } } @@ -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, + } } } @@ -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, + } } } @@ -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] diff --git a/bip78/tests/integration.rs b/bip78/tests/integration.rs index 0ff0ef40..a19764a2 100644 --- a/bip78/tests/integration.rs +++ b/bip78/tests/integration.rs @@ -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 }