Skip to content

Commit 6193026

Browse files
committed
fix: gate v2 std-only code behind cfg features
1 parent f870052 commit 6193026

8 files changed

Lines changed: 614 additions & 226 deletions

File tree

payjoin/src/core/persist.rs

Lines changed: 168 additions & 71 deletions
Large diffs are not rendered by default.

payjoin/src/core/receive/v2/error.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use core::fmt;
2-
use std::error;
1+
use core::{error, fmt};
32

3+
#[cfg(feature = "v2-ohttp")]
44
use crate::hpke::HpkeError;
5+
#[cfg(feature = "v2-ohttp")]
56
use crate::ohttp::{DirectoryResponseError, OhttpEncapsulationError};
67
use crate::time::Time;
78

payjoin/src/core/receive/v2/mod.rs

Lines changed: 126 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@
2424
//! Note: Even fresh requests may be linkable via metadata (e.g. client IP, request timing),
2525
//! but request reuse makes correlation trivial for the relay.
2626
27-
use std::str::FromStr;
27+
use alloc::boxed::Box;
28+
use alloc::string::{String, ToString};
29+
use alloc::vec::Vec;
30+
#[cfg(not(feature = "std"))]
31+
use alloc::{format, vec};
32+
use core::str::FromStr;
2833
#[cfg(not(target_arch = "wasm32"))]
29-
use std::time::Duration;
34+
use core::time::Duration;
3035

3136
use bitcoin::hashes::{sha256, Hash};
3237
use bitcoin::psbt::Psbt;
@@ -35,10 +40,16 @@ pub use error::{CreateRequestError, SessionError};
3540
pub(crate) use error::{InternalCreateRequestError, InternalSessionError};
3641
use serde::de::Deserializer;
3742
use serde::{Deserialize, Serialize};
38-
pub use session::{
39-
replay_event_log, replay_event_log_async, SessionEvent, SessionHistory, SessionOutcome,
40-
SessionStatus,
41-
};
43+
#[cfg(feature = "std")]
44+
pub use session::replay_event_log_async;
45+
pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionOutcome, SessionStatus};
46+
47+
#[cfg(feature = "std")]
48+
pub use super::JsonReply as ErrorReply;
49+
50+
#[cfg(not(feature = "std"))]
51+
#[derive(Debug, Clone, PartialEq, Eq)]
52+
pub struct ErrorReply;
4253
#[cfg(target_arch = "wasm32")]
4354
use web_time::Duration;
4455

@@ -51,9 +62,9 @@ use super::{
5162
use crate::core::Url;
5263
use crate::error::{InternalReplayError, ReplayError};
5364
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
54-
use crate::ohttp::{
55-
ohttp_encapsulate, process_get_res, process_post_res, OhttpEncapsulationError, OhttpKeys,
56-
};
65+
#[cfg(all(feature = "std", feature = "v2-ohttp"))]
66+
use crate::ohttp::process_get_res;
67+
use crate::ohttp::{ohttp_encapsulate, process_post_res, OhttpEncapsulationError, OhttpKeys};
5768
use crate::output_substitution::OutputSubstitution;
5869
use crate::persist::{
5970
MaybeFatalOrSuccessTransition, MaybeFatalTransition, MaybeFatalTransitionWithNoResults,
@@ -64,7 +75,6 @@ use crate::receive::{parse_payload, InputPair, OriginalPayload, PsbtContext};
6475
use crate::time::Time;
6576
use crate::uri::ShortId;
6677
use crate::{ImplementationError, IntoUrl, IntoUrlError, Request, Version};
67-
6878
mod error;
6979
mod session;
7080

@@ -143,6 +153,7 @@ pub enum ReceiveSession {
143153
WantsFeeRange(Receiver<WantsFeeRange>),
144154
ProvisionalProposal(Receiver<ProvisionalProposal>),
145155
PayjoinProposal(Receiver<PayjoinProposal>),
156+
#[cfg(feature = "std")]
146157
HasReplyableError(Receiver<HasReplyableError>),
147158
Monitor(Receiver<Monitor>),
148159
PendingFallback(Receiver<PendingFallback>),
@@ -220,6 +231,7 @@ impl ReceiveSession {
220231
(_, SessionEvent::Closed(session_outcome)) =>
221232
Ok(ReceiveSession::Closed(session_outcome)),
222233

234+
#[cfg(feature = "std")]
223235
(session, SessionEvent::GotReplyableError(error)) => {
224236
let (session_context, fallback_tx) = match session {
225237
ReceiveSession::Initialized(r) => (r.session_context, None),
@@ -240,8 +252,7 @@ impl ReceiveSession {
240252
(r.session_context, Some(r.state.fallback_tx())),
241253
ReceiveSession::PayjoinProposal(r) =>
242254
(r.session_context, Some(r.state.fallback_tx())),
243-
ReceiveSession::HasReplyableError(r) =>
244-
(r.session_context, r.state.fallback_tx.clone()),
255+
ReceiveSession::HasReplyableError(r) => (r.session_context, None),
245256
ReceiveSession::Monitor(r) => (r.session_context, Some(r.state.fallback_tx())),
246257
ReceiveSession::PendingFallback(r) => {
247258
let fallback_tx = r.fallback_tx().clone();
@@ -319,6 +330,7 @@ mod sealed {
319330
impl State for super::WantsFeeRange {}
320331
impl State for super::ProvisionalProposal {}
321332
impl State for super::PayjoinProposal {}
333+
#[cfg(feature = "std")]
322334
impl State for super::HasReplyableError {}
323335
impl State for super::Monitor {}
324336
impl State for super::PendingFallback {}
@@ -610,21 +622,34 @@ impl Receiver<Initialized> {
610622
body: &[u8],
611623
context: ohttp::ClientResponse,
612624
) -> Result<Option<(OriginalPayload, Option<HpkePublicKey>)>, ProtocolError> {
613-
let body = match process_get_res(body, context)
614-
.map_err(|e| ProtocolError::V2(InternalSessionError::DirectoryResponse(e).into()))?
625+
#[cfg(all(feature = "std", feature = "v2-ohttp"))]
615626
{
616-
Some(body) => body,
617-
None => return Ok(None),
618-
};
619-
match std::str::from_utf8(&body) {
620-
// V1 response bodies are utf8 plaintext
621-
Ok(response) =>
622-
Ok(Some(self.extract_proposal_from_v1(response).map(|original| (original, None))?)),
623-
// V2 response bodies are encrypted binary
624-
Err(_) => Ok(Some(
625-
self.extract_proposal_from_v2(body)
626-
.map(|(original, reply_key)| (original, Some(reply_key)))?,
627-
)),
627+
let body: Vec<u8> = match process_get_res(body, context)
628+
.map_err(|e| ProtocolError::V2(InternalSessionError::DirectoryResponse(e).into()))?
629+
{
630+
Some(body) => body,
631+
None => return Ok(None),
632+
};
633+
634+
match core::str::from_utf8(&body) {
635+
// V1 response bodies are utf8 plaintext
636+
Ok(response) => Ok(Some(
637+
self.extract_proposal_from_v1(response).map(|original| (original, None))?,
638+
)),
639+
// V2 response bodies are encrypted binary
640+
Err(_) => Ok(Some(
641+
self.extract_proposal_from_v2(body)
642+
.map(|(original, reply_key)| (original, Some(reply_key)))?,
643+
)),
644+
}
645+
}
646+
647+
#[cfg(not(all(feature = "std", feature = "v2-ohttp")))]
648+
{
649+
let _ = (body, context);
650+
Err(ProtocolError::V2(
651+
InternalSessionError::Implementation(ImplementationError::std_required()).into(),
652+
))
628653
}
629654
}
630655

@@ -652,7 +677,7 @@ impl Receiver<Initialized> {
652677
let (payload_bytes, reply_key) =
653678
decrypt_message_a(&response, self.session_context.receiver_key.secret_key())
654679
.map_err(|e| ProtocolError::V2(InternalSessionError::Hpke(e).into()))?;
655-
let payload = std::str::from_utf8(&payload_bytes)
680+
let payload = core::str::from_utf8(&payload_bytes)
656681
.map_err(|e| ProtocolError::OriginalPayload(InternalPayloadError::Utf8(e).into()))?;
657682
self.unchecked_from_payload(payload).map(|p| (p, reply_key))
658683
}
@@ -745,6 +770,7 @@ impl Receiver<UncheckedOriginalPayload> {
745770
/// This can be used to further prevent probing attacks since the attacker would now need to probe the receiver
746771
/// with transactions which are both broadcastable and pay high fee. Unrelated to the probing attack scenario,
747772
/// this parameter also makes operating in a high fee environment easier for the receiver.
773+
#[cfg(feature = "std")]
748774
pub fn check_broadcast_suitability(
749775
self,
750776
min_fee_rate: Option<FeeRate>,
@@ -776,6 +802,24 @@ impl Receiver<UncheckedOriginalPayload> {
776802
}
777803
}
778804

805+
#[cfg(not(feature = "std"))]
806+
pub fn check_broadcast_suitability(
807+
self,
808+
min_fee_rate: Option<FeeRate>,
809+
can_broadcast: impl Fn(&bitcoin::Transaction) -> Result<bool, ImplementationError>,
810+
) -> MaybeFatalTransition<SessionEvent, Receiver<MaybeInputsOwned>, Error> {
811+
match self.state.original.check_broadcast_suitability(min_fee_rate, can_broadcast) {
812+
Ok(()) => MaybeFatalTransition::success(
813+
SessionEvent::CheckedBroadcastSuitability(),
814+
Receiver {
815+
state: MaybeInputsOwned { original: self.original.clone() },
816+
session_context: self.session_context,
817+
},
818+
),
819+
Err(e) => MaybeFatalTransition::transient(e),
820+
}
821+
}
822+
779823
/// Moves on to the next typestate without any of the current typestate's validations.
780824
///
781825
/// Use this for interactive payment receivers, where there is no risk of a probing attack since the
@@ -1362,7 +1406,15 @@ impl Receiver<PayjoinProposal> {
13621406
}
13631407
}
13641408

1409+
#[cfg(feature = "std")]
13651410
#[derive(Debug, Clone, PartialEq)]
1411+
pub struct HasReplyableError {
1412+
error_reply: super::JsonReply,
1413+
fallback_tx: Option<bitcoin::Transaction>,
1414+
}
1415+
1416+
#[cfg(not(feature = "std"))]
1417+
#[derive(Debug, Clone, PartialEq, Eq)]
13661418
pub struct HasReplyableError {
13671419
error_reply: JsonReply,
13681420
fallback_tx: Option<bitcoin::Transaction>,
@@ -1371,14 +1423,17 @@ pub struct HasReplyableError {
13711423
impl Receiver<HasReplyableError> {
13721424
/// Cancel without sending the error response.
13731425
pub fn cancel(self) -> MaybeTerminalTransition<SessionEvent, Receiver<PendingFallback>> {
1374-
let Receiver { state: HasReplyableError { fallback_tx, .. }, session_context } = self;
1375-
match fallback_tx {
1376-
Some(fallback_tx) => MaybeTerminalTransition::advance(
1377-
SessionEvent::Cancelled,
1378-
Receiver { state: PendingFallback { fallback_tx }, session_context },
1379-
),
1380-
None =>
1381-
MaybeTerminalTransition::terminate(SessionEvent::Closed(SessionOutcome::Aborted)),
1426+
{
1427+
let Receiver { state: HasReplyableError { fallback_tx, .. }, session_context } = self;
1428+
match fallback_tx {
1429+
Some(fallback_tx) => MaybeTerminalTransition::advance(
1430+
SessionEvent::Cancelled,
1431+
Receiver { state: PendingFallback { fallback_tx }, session_context },
1432+
),
1433+
None => MaybeTerminalTransition::terminate(SessionEvent::Closed(
1434+
SessionOutcome::Aborted,
1435+
)),
1436+
}
13821437
}
13831438
}
13841439

@@ -1389,6 +1444,7 @@ impl Receiver<HasReplyableError> {
13891444
ohttp_relay: impl IntoUrl,
13901445
) -> Result<(Request, ohttp::ClientResponse), SessionError> {
13911446
let session_context = &self.session_context;
1447+
#[cfg(feature = "std")]
13921448
if session_context.expiration.elapsed() {
13931449
return Err(InternalSessionError::Expired(session_context.expiration).into());
13941450
}
@@ -1836,6 +1892,7 @@ pub mod test {
18361892
Ok(())
18371893
}
18381894

1895+
#[cfg(feature = "v1")]
18391896
#[test]
18401897
fn test_unchecked_proposal_fatal_error() -> Result<(), BoxError> {
18411898
let persister = InMemoryPersister::default();
@@ -1947,6 +2004,7 @@ pub mod test {
19472004
Ok(())
19482005
}
19492006

2007+
#[cfg(all(feature = "v1", not(feature = "std")))]
19502008
#[test]
19512009
fn test_create_error_request() -> Result<(), BoxError> {
19522010
let mock_err = mock_err();
@@ -1970,6 +2028,8 @@ pub mod test {
19702028
Ok(())
19712029
}
19722030

2031+
#[cfg(feature = "v1")]
2032+
#[cfg(not(feature = "std"))]
19732033
#[test]
19742034
fn test_create_error_request_expiration() -> Result<(), BoxError> {
19752035
let now = crate::time::Time::now();
@@ -1994,6 +2054,7 @@ pub mod test {
19942054
Ok(())
19952055
}
19962056

2057+
#[cfg(not(feature = "std"))]
19972058
#[test]
19982059
fn process_error_response_success_with_fallback_enters_pending_fallback() -> Result<(), BoxError>
19992060
{
@@ -2015,7 +2076,7 @@ pub mod test {
20152076
assert_events(&persister, &[SessionEvent::ProtocolFailed], false);
20162077
Ok(())
20172078
}
2018-
2079+
#[cfg(not(feature = "std"))]
20192080
#[test]
20202081
fn process_error_response_success_without_fallback_closes_session() -> Result<(), BoxError> {
20212082
let receiver = receiver(HasReplyableError { error_reply: mock_err(), fallback_tx: None });
@@ -2030,6 +2091,7 @@ pub mod test {
20302091
Ok(())
20312092
}
20322093

2094+
#[cfg(not(feature = "std"))]
20332095
#[test]
20342096
fn process_error_response_fatal_with_fallback_enters_pending_fallback() -> Result<(), BoxError>
20352097
{
@@ -2053,7 +2115,7 @@ pub mod test {
20532115
assert_events(&persister, &[SessionEvent::ProtocolFailed], false);
20542116
Ok(())
20552117
}
2056-
2118+
#[cfg(not(feature = "std"))]
20572119
#[test]
20582120
fn process_error_response_fatal_without_fallback_closes_session() -> Result<(), BoxError> {
20592121
let receiver = receiver(HasReplyableError { error_reply: mock_err(), fallback_tx: None });
@@ -2071,6 +2133,7 @@ pub mod test {
20712133
Ok(())
20722134
}
20732135

2136+
#[cfg(not(feature = "std"))]
20742137
#[test]
20752138
fn process_error_response_transient_leaves_session_open() -> Result<(), BoxError> {
20762139
let receiver = receiver(HasReplyableError {
@@ -2271,6 +2334,7 @@ pub mod test {
22712334
assert_events(&persister, &[SessionEvent::Cancelled], false);
22722335
}
22732336

2337+
#[cfg(not(feature = "std"))]
22742338
#[test]
22752339
fn cancel_replyable_error_with_fallback_enters_pending_fallback() {
22762340
let expected_tx = mock_fallback_tx();
@@ -2288,6 +2352,7 @@ pub mod test {
22882352
assert_events(&persister, &[SessionEvent::Cancelled], false);
22892353
}
22902354

2355+
#[cfg(not(feature = "std"))]
22912356
#[test]
22922357
fn cancel_replyable_error_without_fallback_closes_session() {
22932358
let persister = InMemoryPersister::<SessionEvent>::default();
@@ -2436,6 +2501,7 @@ pub mod test {
24362501
}
24372502
}
24382503

2504+
#[cfg(not(feature = "std"))]
24392505
#[test]
24402506
fn replaying_replyable_error_from_unchecked_captures_no_fallback() {
24412507
let state = unchecked_proposal_v2_from_test_vector();
@@ -2458,6 +2524,7 @@ pub mod test {
24582524
}
24592525
}
24602526

2527+
#[cfg(not(feature = "std"))]
24612528
#[test]
24622529
fn replaying_replyable_error_from_initialized_captures_no_fallback() {
24632530
let error = mock_err();
@@ -2479,6 +2546,7 @@ pub mod test {
24792546
}
24802547
}
24812548

2549+
#[cfg(not(feature = "std"))]
24822550
#[test]
24832551
fn replaying_replyable_error_from_replyable_error_carries_some_fallback() {
24842552
let expected_fallback = mock_fallback_tx();
@@ -2504,6 +2572,7 @@ pub mod test {
25042572
}
25052573
}
25062574

2575+
#[cfg(not(feature = "std"))]
25072576
#[test]
25082577
fn replaying_replyable_error_from_replyable_error_carries_no_fallback() {
25092578
let error = mock_err();
@@ -2524,4 +2593,24 @@ pub mod test {
25242593
other => panic!("Expected HasReplyableError, got {other:?}"),
25252594
}
25262595
}
2596+
2597+
#[cfg(not(feature = "std"))]
2598+
#[cfg(test)]
2599+
mod json_reply_placeholder_tests {
2600+
use super::json_reply_placeholder::JsonReply;
2601+
use crate::error_codes::ErrorCode;
2602+
2603+
#[test]
2604+
fn test_json_reply_new() {
2605+
let reply = JsonReply::new(ErrorCode::Unavailable, "test");
2606+
assert_eq!(reply.to_json(), "{}");
2607+
}
2608+
2609+
#[test]
2610+
fn test_json_reply_from() {
2611+
let val = 42u32;
2612+
let reply = JsonReply::from(&val);
2613+
assert_eq!(reply.to_json(), "{}");
2614+
}
2615+
}
25272616
}

0 commit comments

Comments
 (0)