diff --git a/payjoin/src/hpke.rs b/payjoin/src/hpke.rs index b88434f7..5dc2caa2 100644 --- a/payjoin/src/hpke.rs +++ b/payjoin/src/hpke.rs @@ -1,7 +1,9 @@ use std::ops::Deref; use std::{error, fmt}; -use bitcoin::key::constants::{ELLSWIFT_ENCODING_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}; +use bitcoin::key::constants::{ + ELLSWIFT_ENCODING_SIZE, PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE, +}; use bitcoin::secp256k1::ellswift::ElligatorSwift; use hpke::aead::ChaCha20Poly1305; use hpke::kdf::HkdfSha256; @@ -97,7 +99,7 @@ impl<'de> serde::Deserialize<'de> for HpkeSecretKey { pub struct HpkePublicKey(pub PublicKey); impl HpkePublicKey { - pub fn to_compressed_bytes(&self) -> [u8; 33] { + pub fn to_compressed_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] { let compressed_key = bitcoin::secp256k1::PublicKey::from_slice(&self.0.to_bytes()) .expect("Invalid public key from known valid bytes"); compressed_key.serialize() @@ -148,7 +150,7 @@ impl<'de> serde::Deserialize<'de> for HpkePublicKey { /// Message A is sent from the sender to the receiver containing an Original PSBT payload #[cfg(feature = "send")] pub fn encrypt_message_a( - body: Vec<u8>, + body: Vec<u8>, // FIXME: could be &[u8] reply_pk: &HpkePublicKey, receiver_pk: &HpkePublicKey, ) -> Result<Vec<u8>, HpkeError> { @@ -172,7 +174,7 @@ pub fn encrypt_message_a( #[cfg(feature = "receive")] pub fn decrypt_message_a( message_a: &[u8], - receiver_sk: HpkeSecretKey, + receiver_sk: HpkeSecretKey, // FIXME: could be &HpkeSecretKey ) -> Result<(Vec<u8>, HpkePublicKey), HpkeError> { use std::io::{Cursor, Read}; @@ -203,7 +205,7 @@ pub fn decrypt_message_a( /// Message B is sent from the receiver to the sender containing a Payjoin PSBT payload or an error #[cfg(feature = "receive")] pub fn encrypt_message_b( - mut plaintext: Vec<u8>, + mut plaintext: Vec<u8>, // FIXME: could be &[u8] receiver_keypair: &HpkeKeyPair, sender_pk: &HpkePublicKey, ) -> Result<Vec<u8>, HpkeError> { @@ -227,8 +229,8 @@ pub fn encrypt_message_b( #[cfg(feature = "send")] pub fn decrypt_message_b( message_b: &[u8], - receiver_pk: HpkePublicKey, - sender_sk: HpkeSecretKey, + receiver_pk: HpkePublicKey, // FIXME: could be &HpkePublicKey + sender_sk: HpkeSecretKey, // FIXME: could be &HpkeSecretKey ) -> Result<Vec<u8>, HpkeError> { let enc = message_b.get(..ELLSWIFT_ENCODING_SIZE).ok_or(HpkeError::PayloadTooShort)?; let enc = encapped_key_from_ellswift_bytes(enc)?; @@ -242,6 +244,8 @@ pub fn decrypt_message_b( Ok(plaintext) } +// FIXME: could be &mut [u8; padded_length] and return &[u8; padded_length] +// ACTYUALLY function should not exist at all fn pad_plaintext(msg: &mut Vec<u8>, padded_length: usize) -> Result<&[u8], HpkeError> { if msg.len() > padded_length { return Err(HpkeError::PayloadTooLarge { actual: msg.len(), max: padded_length }); diff --git a/payjoin/src/io.rs b/payjoin/src/io.rs index 1886e6e1..7b537420 100644 --- a/payjoin/src/io.rs +++ b/payjoin/src/io.rs @@ -9,7 +9,7 @@ use crate::{OhttpKeys, Url}; /// /// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This /// directory stores and forwards payjoin client payloads. -/// +/// // FIXME split into two functions so that the docstring can be right /// * `cert_der` (optional): The DER-encoded certificate to use for local HTTPS connections. This /// parameter is only available when the "danger-local-https" feature is enabled. #[cfg(feature = "v2")] diff --git a/payjoin/src/ohttp.rs b/payjoin/src/ohttp.rs index 9bd7d147..7e955cd4 100644 --- a/payjoin/src/ohttp.rs +++ b/payjoin/src/ohttp.rs @@ -4,7 +4,9 @@ use std::{error, fmt}; use bitcoin::base64::prelude::BASE64_URL_SAFE_NO_PAD; use bitcoin::base64::Engine; -pub fn ohttp_encapsulate( +pub const PADDED_MESSAGE_BYTES: usize = 8192; + +pub(crate) fn ohttp_encapsulate( ohttp_keys: &mut ohttp::KeyConfig, method: &str, target_resource: &str, @@ -38,7 +40,7 @@ pub fn ohttp_encapsulate( } /// decapsulate ohttp, bhttp response and return http response body and status code -pub fn ohttp_decapsulate( +pub(crate) fn ohttp_decapsulate( res_ctx: ohttp::ClientResponse, ohttp_body: &[u8], ) -> Result<http::Response<Vec<u8>>, OhttpEncapsulationError> { @@ -57,7 +59,7 @@ pub fn ohttp_decapsulate( /// Error from de/encapsulating an Oblivious HTTP request or response. #[derive(Debug)] -pub enum OhttpEncapsulationError { +pub(crate) enum OhttpEncapsulationError { Http(http::Error), Ohttp(ohttp::Error), Bhttp(bhttp::Error), diff --git a/payjoin/src/psbt.rs b/payjoin/src/psbt.rs index 46b6cdf1..610b8c34 100644 --- a/payjoin/src/psbt.rs +++ b/payjoin/src/psbt.rs @@ -36,9 +36,9 @@ pub(crate) trait PsbtExt: Sized { fn proprietary_mut(&mut self) -> &mut BTreeMap<psbt::raw::ProprietaryKey, Vec<u8>>; fn unknown_mut(&mut self) -> &mut BTreeMap<psbt::raw::Key, Vec<u8>>; fn input_pairs(&self) -> Box<dyn Iterator<Item = InternalInputPair<'_>> + '_>; - // guarantees that length of psbt input matches that of unsigned_tx inputs and same + /// guarantees that length of psbt input matches that of unsigned_tx inputs and same /// thing for outputs. - fn validate(self) -> Result<Self, InconsistentPsbt>; + fn validate(self) -> Result<Self, InconsistentPsbt>; // FIXME a ValidPsbt wrapper makes more semantic sense fn validate_input_utxos(&self, treat_missing_as_error: bool) -> Result<(), PsbtInputsError>; } @@ -70,6 +70,8 @@ impl PsbtExt for Psbt { } fn validate(self) -> Result<Self, InconsistentPsbt> { + // TODO try to simplify trait to handle all validation + // TODO use validate_input_utxos here let tx_ins = self.unsigned_tx.input.len(); let psbt_ins = self.inputs.len(); let tx_outs = self.unsigned_tx.output.len(); @@ -135,7 +137,7 @@ impl InternalInputPair<'_> { pub fn validate_utxo( &self, - treat_missing_as_error: bool, + treat_missing_as_error: bool, // FIXME never used! remove! ) -> Result<(), InternalPsbtInputError> { match (&self.psbtin.non_witness_utxo, &self.psbtin.witness_utxo) { (None, None) if treat_missing_as_error => diff --git a/payjoin/src/receive/mod.rs b/payjoin/src/receive/mod.rs index 071c744d..349a88fd 100644 --- a/payjoin/src/receive/mod.rs +++ b/payjoin/src/receive/mod.rs @@ -109,7 +109,7 @@ pub struct UncheckedProposal { impl UncheckedProposal { pub fn from_request( - mut body: impl std::io::Read, + mut body: impl std::io::Read, // FIXME: could be &[u8] bc you need the whole thing query: &str, headers: impl Headers, ) -> Result<Self, RequestError> { @@ -142,9 +142,7 @@ impl UncheckedProposal { let params = Params::from_query_pairs(pairs).map_err(InternalRequestError::SenderParams)?; log::debug!("Received request with params: {:?}", params); - // TODO check that params are valid for the request's Original PSBT - - Ok(UncheckedProposal { psbt, params }) + Ok(Self { psbt, params }) } /// The Sender's Original PSBT transaction @@ -354,7 +352,7 @@ impl WantsOutputs { pub fn substitute_receiver_script( self, output_script: &Script, - ) -> Result<WantsOutputs, OutputSubstitutionError> { + ) -> Result<Self, OutputSubstitutionError> { let output_value = self.original_psbt.unsigned_tx.output[self.change_vout].value; let outputs = vec![TxOut { value: output_value, script_pubkey: output_script.into() }]; self.replace_receiver_outputs(outputs, output_script) @@ -369,7 +367,7 @@ impl WantsOutputs { self, replacement_outputs: Vec<TxOut>, drain_script: &Script, - ) -> Result<WantsOutputs, OutputSubstitutionError> { + ) -> Result<Self, OutputSubstitutionError> { let mut payjoin_psbt = self.original_psbt.clone(); let mut outputs = vec![]; let mut replacement_outputs = replacement_outputs.clone(); @@ -427,7 +425,7 @@ impl WantsOutputs { // Update the payjoin PSBT outputs payjoin_psbt.outputs = vec![Default::default(); outputs.len()]; payjoin_psbt.unsigned_tx.output = outputs; - Ok(WantsOutputs { + Ok(Self { original_psbt: self.original_psbt, payjoin_psbt, params: self.params, @@ -494,7 +492,7 @@ impl WantsInputs { /// A simple consolidation is otherwise chosen if available. pub fn try_preserving_privacy( &self, - candidate_inputs: impl IntoIterator<Item = InputPair>, + candidate_inputs: impl IntoIterator<Item = InputPair>, // FIXME &InputPair & clone out ) -> Result<InputPair, SelectionError> { let mut candidate_inputs = candidate_inputs.into_iter().peekable(); if candidate_inputs.peek().is_none() { @@ -521,7 +519,7 @@ impl WantsInputs { /// https://eprint.iacr.org/2022/589.pdf fn avoid_uih( &self, - candidate_inputs: impl IntoIterator<Item = InputPair>, + candidate_inputs: impl IntoIterator<Item = InputPair>, // FIXME &InputPair & clone out ) -> Result<InputPair, SelectionError> { let min_original_out_sats = self .payjoin_psbt @@ -559,7 +557,7 @@ impl WantsInputs { fn select_first_candidate( &self, - candidate_inputs: impl IntoIterator<Item = InputPair>, + candidate_inputs: impl IntoIterator<Item = InputPair>, // FIXME &InputPair & clone out ) -> Result<InputPair, SelectionError> { candidate_inputs.into_iter().next().ok_or(InternalSelectionError::NotFound.into()) } @@ -569,7 +567,7 @@ impl WantsInputs { pub fn contribute_inputs( self, inputs: impl IntoIterator<Item = InputPair>, - ) -> Result<WantsInputs, InputContributionError> { + ) -> Result<Self, InputContributionError> { let mut payjoin_psbt = self.payjoin_psbt.clone(); // The payjoin proposal must not introduce mixed input sequence numbers let original_sequence = self @@ -609,7 +607,7 @@ impl WantsInputs { return Err(InternalInputContributionError::ValueTooLow.into()); } - Ok(WantsInputs { + Ok(Self { original_psbt: self.original_psbt, payjoin_psbt, params: self.params, @@ -897,10 +895,6 @@ pub struct PayjoinProposal { } impl PayjoinProposal { - pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> { - self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output) - } - pub fn is_output_substitution_disabled(&self) -> bool { self.params.disable_output_substitution } diff --git a/payjoin/src/receive/v2/mod.rs b/payjoin/src/receive/v2/mod.rs index c62bc8c4..5526bce0 100644 --- a/payjoin/src/receive/v2/mod.rs +++ b/payjoin/src/receive/v2/mod.rs @@ -113,7 +113,7 @@ impl Receiver { /// indicating no UncheckedProposal is available yet. pub fn process_res( &mut self, - mut body: impl std::io::Read, + mut body: impl std::io::Read, // FIXME: could be &[u8], not streamed context: ohttp::ClientResponse, ) -> Result<Option<UncheckedProposal>, Error> { let mut buf = Vec::new(); @@ -139,10 +139,12 @@ impl Receiver { ohttp_encapsulate(&mut self.context.ohttp_keys, "GET", fallback_target.as_str(), None) } + // FIXME: could be &str fn extract_proposal_from_v1(&mut self, response: String) -> Result<UncheckedProposal, Error> { Ok(self.unchecked_from_payload(response)?) } + // FIXME: could be &[u8] because we've got a known length fn extract_proposal_from_v2(&mut self, response: Vec<u8>) -> Result<UncheckedProposal, Error> { let (payload_bytes, e) = decrypt_message_a(&response, self.context.s.secret_key().clone())?; self.context.e = Some(e); @@ -152,7 +154,7 @@ impl Receiver { fn unchecked_from_payload( &mut self, - payload: String, + payload: String, // FIXME: could be &str ) -> Result<UncheckedProposal, RequestError> { let (base64, padded_query) = payload.split_once('\n').unwrap_or_default(); let query = padded_query.trim_matches('\0'); @@ -347,9 +349,9 @@ impl WantsOutputs { pub fn substitute_receiver_script( self, output_script: &Script, - ) -> Result<WantsOutputs, OutputSubstitutionError> { + ) -> Result<Self, OutputSubstitutionError> { let inner = self.inner.substitute_receiver_script(output_script)?; - Ok(WantsOutputs { inner, context: self.context }) + Ok(Self { inner, context: self.context }) } /// Replace **all** receiver outputs with one or more provided outputs. @@ -359,11 +361,11 @@ impl WantsOutputs { /// receiver needs to pay for additional miner fees (e.g. in the case of adding many outputs). pub fn replace_receiver_outputs( self, - replacement_outputs: Vec<TxOut>, + replacement_outputs: Vec<TxOut>, // FIXME could be Iterator<Item = &TxOut> drain_script: &Script, - ) -> Result<WantsOutputs, OutputSubstitutionError> { + ) -> Result<Self, OutputSubstitutionError> { let inner = self.inner.replace_receiver_outputs(replacement_outputs, drain_script)?; - Ok(WantsOutputs { inner, context: self.context }) + Ok(Self { inner, context: self.context }) } /// Proceed to the input contribution step. @@ -395,7 +397,7 @@ impl WantsInputs { /// https://eprint.iacr.org/2022/589.pdf pub fn try_preserving_privacy( &self, - candidate_inputs: impl IntoIterator<Item = InputPair>, + candidate_inputs: impl IntoIterator<Item = &InputPair>, // FIXME &InputPair & clone out ) -> Result<InputPair, SelectionError> { self.inner.try_preserving_privacy(candidate_inputs) } @@ -404,7 +406,7 @@ impl WantsInputs { /// Any excess input amount is added to the change_vout output indicated previously. pub fn contribute_inputs( self, - inputs: impl IntoIterator<Item = InputPair>, + inputs: impl IntoIterator<Item = InputPair>, // Own and place in payjoin_psbt ) -> Result<WantsInputs, InputContributionError> { let inner = self.inner.contribute_inputs(inputs)?; Ok(WantsInputs { inner, context: self.context }) @@ -450,10 +452,6 @@ pub struct PayjoinProposal { } impl PayjoinProposal { - pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> { - self.inner.utxos_to_be_locked() - } - pub fn is_output_substitution_disabled(&self) -> bool { self.inner.is_output_substitution_disabled() } @@ -509,7 +507,7 @@ impl PayjoinProposal { /// choose to broadcast the original PSBT. pub fn process_res( &self, - res: Vec<u8>, + res: Vec<u8>, // FIXME could be &[u8] ohttp_context: ohttp::ClientResponse, ) -> Result<(), Error> { let res = ohttp_decapsulate(ohttp_context, &res)?; diff --git a/payjoin/src/request.rs b/payjoin/src/request.rs index a093aa10..1ad32d83 100644 --- a/payjoin/src/request.rs +++ b/payjoin/src/request.rs @@ -27,10 +27,14 @@ pub struct Request { } impl Request { + // FIXME: could be &Url and clone inside to help caller + // FIXME: could be &[u8] and clone inside to help caller pub fn new_v1(url: Url, body: Vec<u8>) -> Self { Self { url, content_type: V1_REQ_CONTENT_TYPE, body } } + // FIXME: could be &Url and clone inside to help caller + // FIXME: could be &[u8] and clone inside to help caller #[cfg(feature = "v2")] pub fn new_v2(url: Url, body: Vec<u8>) -> Self { Self { url, content_type: V2_REQ_CONTENT_TYPE, body } diff --git a/payjoin/src/send/mod.rs b/payjoin/src/send/mod.rs index f31bb6f0..7e7f3803 100644 --- a/payjoin/src/send/mod.rs +++ b/payjoin/src/send/mod.rs @@ -277,7 +277,7 @@ impl Sender { #[cfg(feature = "v2")] pub fn extract_v2( &self, - ohttp_relay: Url, + ohttp_relay: Url, // FIXME: could be &Url and clone inside to help caller ) -> Result<(Request, V2PostContext), CreateRequestError> { use crate::uri::UrlExt; if let Some(expiry) = self.endpoint.exp() { @@ -342,7 +342,7 @@ pub struct V1Context { impl V1Context { pub fn process_response( self, - response: &mut impl std::io::Read, + response: &mut impl std::io::Read, // FIXME: could be &[u8] bc you need the whole thing ) -> Result<Psbt, ResponseError> { self.psbt_context.process_response(response) } @@ -360,11 +360,9 @@ pub struct V2PostContext { impl V2PostContext { pub fn process_response( self, - response: &mut impl std::io::Read, + response: &[u8], // FIXME: could be &[u8] bc caller doesn't need to track read cursor ) -> Result<V2GetContext, ResponseError> { - let mut res_buf = Vec::new(); - response.read_to_end(&mut res_buf).map_err(InternalValidationError::Io)?; - let response = ohttp_decapsulate(self.ohttp_ctx, &res_buf) + let response = ohttp_decapsulate(self.ohttp_ctx, response) .map_err(InternalValidationError::OhttpEncapsulation)?; match response.status() { http::StatusCode::OK => { @@ -392,7 +390,7 @@ pub struct V2GetContext { impl V2GetContext { pub fn extract_req( &self, - ohttp_relay: Url, + ohttp_relay: Url, // FIXME: could be &Url and clone inside to help caller ) -> Result<(Request, ohttp::ClientResponse), CreateRequestError> { use crate::uri::UrlExt; let mut url = self.endpoint.clone(); @@ -417,12 +415,10 @@ impl V2GetContext { pub fn process_response( &self, - response: &mut impl std::io::Read, + response: &[u8], // FIXME: could be &[u8] bc caller doesn't need to track read cursor ohttp_ctx: ohttp::ClientResponse, ) -> Result<Option<Psbt>, ResponseError> { - let mut res_buf = Vec::new(); - response.read_to_end(&mut res_buf).map_err(InternalValidationError::Io)?; - let response = ohttp_decapsulate(ohttp_ctx, &res_buf) + let response = ohttp_decapsulate(ohttp_ctx, response) .map_err(InternalValidationError::OhttpEncapsulation)?; let body = match response.status() { http::StatusCode::OK => response.body().to_vec(), @@ -465,7 +461,7 @@ struct HpkeContext { #[cfg(feature = "v2")] impl HpkeContext { - pub fn new(receiver: HpkePublicKey) -> Self { + fn new(receiver: HpkePublicKey) -> Self { Self { receiver, reply_pair: HpkeKeyPair::gen_keypair() } } } @@ -496,7 +492,7 @@ impl PsbtContext { #[inline] pub fn process_response( self, - response: &mut impl std::io::Read, + response: &mut impl std::io::Read, // FIXME: could be &[u8] or &str ref IF POSSIBLE ) -> Result<Psbt, ResponseError> { let mut res_str = String::new(); response.read_to_string(&mut res_str).map_err(InternalValidationError::Io)?; @@ -859,7 +855,7 @@ fn serialize_v2_body( } fn serialize_url( - endpoint: Url, + endpoint: Url, // FIXME: could be &Url and clone inside to help caller disable_output_substitution: bool, fee_contribution: Option<(bitcoin::Amount, usize)>, min_fee_rate: FeeRate, diff --git a/payjoin/src/uri/mod.rs b/payjoin/src/uri/mod.rs index 6281446c..ac067807 100644 --- a/payjoin/src/uri/mod.rs +++ b/payjoin/src/uri/mod.rs @@ -61,6 +61,8 @@ pub trait UriExt<'a>: sealed::UriExt { } impl<'a> UriExt<'a> for Uri<'a, NetworkChecked> { + // FIXME custom enum since error is actually a default fallback for pj unsupported + // enumerate reasons why this might fail fn check_pj_supported(self) -> Result<PjUri<'a>, Box<bip21::Uri<'a>>> { match self.extras { MaybePayjoinExtras::Supported(payjoin) => { @@ -116,7 +118,7 @@ impl PjUriBuilder { pub fn new( address: Address, origin: Url, - #[cfg(feature = "v2")] receiver_pubkey: Option<HpkePublicKey>, + #[cfg(feature = "v2")] receiver_pubkey: Option<HpkePublicKey>, // FIXME make Option<(pk, keys, exp)> #[cfg(feature = "v2")] ohttp_keys: Option<OhttpKeys>, #[cfg(feature = "v2")] expiry: Option<std::time::SystemTime>, ) -> Self { @@ -149,6 +151,7 @@ impl PjUriBuilder { } /// Set whether or not payjoin output substitution is allowed. + #[cfg(not(feature = "v2"))] // TODO ensure v2 options are set imply pjos=true pub fn pjos(mut self, pjos: bool) -> Self { self.pjos = pjos; self