Skip to content

Commit 261ddcc

Browse files
committed
Persist receiver when initialized
This commit introduces a persistence flow for constructing `v2::Receiver`. We enforce that receivers must be persisted before initiating their typestate. To accommodate this, callers will first create a `NewReceiver`, which must be persisted. Upon persistence, they receive a storage token that can be passed to `v2::Receiver::load(...)` to construct a `v2::Receiver`. For cases where persistence is unnecessary (e.g. in testing), implementers can use the `NoopPersister` available in `persist/mod.rs`. Issue: payjoin#336
1 parent 4cd70f1 commit 261ddcc

File tree

6 files changed

+118
-27
lines changed

6 files changed

+118
-27
lines changed

payjoin-cli/src/app/v2.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
44
use payjoin::bitcoin::consensus::encode::serialize_hex;
55
use payjoin::bitcoin::psbt::Psbt;
66
use payjoin::bitcoin::{Amount, FeeRate};
7-
use payjoin::receive::v2::{Receiver, UncheckedProposal};
7+
use payjoin::receive::v2::{NewReceiver, Receiver, UncheckedProposal};
88
use payjoin::receive::{Error, ImplementationError, ReplyableError};
99
use payjoin::send::v2::{Sender, SenderBuilder};
1010
use payjoin::Uri;
@@ -14,6 +14,7 @@ use super::config::Config;
1414
use super::wallet::BitcoindWallet;
1515
use super::App as AppTrait;
1616
use crate::app::{handle_interrupt, http_agent};
17+
use crate::db::v2::ReceiverPersister;
1718
use crate::db::Database;
1819

1920
#[derive(Clone)]
@@ -65,13 +66,18 @@ impl AppTrait for App {
6566
async fn receive_payjoin(&self, amount: Amount) -> Result<()> {
6667
let address = self.wallet().get_new_address()?;
6768
let ohttp_keys = unwrap_ohttp_keys_or_else_fetch(&self.config).await?;
68-
let session = Receiver::new(
69+
let mut persister = ReceiverPersister::new(self.db.clone());
70+
let new_receiver = NewReceiver::new(
6971
address,
7072
self.config.v2()?.pj_directory.clone(),
7173
ohttp_keys.clone(),
7274
None,
7375
)?;
74-
self.db.insert_recv_session(session.clone())?;
76+
let storage_token = new_receiver
77+
.persist(&mut persister)
78+
.map_err(|e| anyhow!("Failed to persist receiver: {}", e))?;
79+
let session = Receiver::load(storage_token, &persister)
80+
.map_err(|e| anyhow!("Failed to load receiver: {}", e))?;
7581
self.spawn_payjoin_receiver(session, Some(amount)).await
7682
}
7783

payjoin-cli/src/db/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub(crate) enum Error {
1313
Serialize(serde_json::Error),
1414
#[cfg(feature = "v2")]
1515
Deserialize(serde_json::Error),
16+
#[cfg(feature = "v2")]
17+
NotFound(String),
1618
}
1719

1820
impl fmt::Display for Error {
@@ -23,6 +25,8 @@ impl fmt::Display for Error {
2325
Error::Serialize(e) => write!(f, "Serialization failed: {}", e),
2426
#[cfg(feature = "v2")]
2527
Error::Deserialize(e) => write!(f, "Deserialization failed: {}", e),
28+
#[cfg(feature = "v2")]
29+
Error::NotFound(key) => write!(f, "Key not found: {}", key),
2630
}
2731
}
2832
}

payjoin-cli/src/db/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ impl Database {
2727
}
2828

2929
#[cfg(feature = "v2")]
30-
mod v2;
30+
pub(crate) mod v2;

payjoin-cli/src/db/v2.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
1+
use std::sync::Arc;
2+
13
use bitcoincore_rpc::jsonrpc::serde_json;
2-
use payjoin::receive::v2::Receiver;
4+
use payjoin::persist::{Persister, Value};
5+
use payjoin::receive::v2::{Receiver, ReceiverToken};
36
use payjoin::send::v2::Sender;
4-
use sled::{IVec, Tree};
7+
use sled::Tree;
58
use url::Url;
69

710
use super::*;
811

9-
impl Database {
10-
pub(crate) fn insert_recv_session(&self, session: Receiver) -> Result<()> {
11-
let recv_tree = self.0.open_tree("recv_sessions")?;
12-
let key = &session.id();
13-
let value = serde_json::to_string(&session).map_err(Error::Serialize)?;
14-
recv_tree.insert(key.as_slice(), IVec::from(value.as_str()))?;
12+
pub(crate) struct ReceiverPersister(Arc<Database>);
13+
impl ReceiverPersister {
14+
pub fn new(db: Arc<Database>) -> Self { Self(db) }
15+
}
16+
17+
impl Persister<Receiver> for ReceiverPersister {
18+
type Token = ReceiverToken;
19+
type Error = crate::db::error::Error;
20+
fn save(&mut self, value: Receiver) -> std::result::Result<ReceiverToken, Self::Error> {
21+
let recv_tree = self.0 .0.open_tree("recv_sessions")?;
22+
let key = value.key();
23+
let value = serde_json::to_vec(&value).map_err(Error::Serialize)?;
24+
recv_tree.insert(key.clone(), value.as_slice())?;
1525
recv_tree.flush()?;
16-
Ok(())
26+
Ok(key)
27+
}
28+
fn load(&self, key: ReceiverToken) -> std::result::Result<Receiver, Self::Error> {
29+
let recv_tree = self.0 .0.open_tree("recv_sessions")?;
30+
let value = recv_tree.get(key.as_ref())?.ok_or(Error::NotFound(key.to_string()))?;
31+
serde_json::from_slice(&value).map_err(Error::Deserialize)
1732
}
33+
}
1834

35+
impl Database {
1936
pub(crate) fn get_recv_sessions(&self) -> Result<Vec<Receiver>> {
2037
let recv_tree = self.0.open_tree("recv_sessions")?;
2138
let mut sessions = Vec::new();

payjoin/src/receive/v2/mod.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Receive BIP 77 Payjoin v2
2+
use std::fmt::{self, Display};
23
use std::str::FromStr;
34
use std::time::{Duration, SystemTime};
45

@@ -19,6 +20,7 @@ use super::{
1920
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
2021
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys};
2122
use crate::output_substitution::OutputSubstitution;
23+
use crate::persist::{self, Persister};
2224
use crate::receive::{parse_payload, InputPair};
2325
use crate::uri::ShortId;
2426
use crate::{IntoUrl, IntoUrlError, Request};
@@ -71,12 +73,12 @@ fn subdir_path_from_pubkey(pubkey: &HpkePublicKey) -> ShortId {
7173

7274
/// A payjoin V2 receiver, allowing for polled requests to the
7375
/// payjoin directory and response processing.
74-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75-
pub struct Receiver {
76+
#[derive(Debug)]
77+
pub struct NewReceiver {
7678
context: SessionContext,
7779
}
7880

79-
impl Receiver {
81+
impl NewReceiver {
8082
/// Creates a new `Receiver` with the provided parameters.
8183
///
8284
/// # Parameters
@@ -96,7 +98,7 @@ impl Receiver {
9698
ohttp_keys: OhttpKeys,
9799
expire_after: Option<Duration>,
98100
) -> Result<Self, IntoUrlError> {
99-
Ok(Self {
101+
let receiver = Self {
100102
context: SessionContext {
101103
address,
102104
directory: directory.into_url()?,
@@ -107,9 +109,53 @@ impl Receiver {
107109
s: HpkeKeyPair::gen_keypair(),
108110
e: None,
109111
},
110-
})
112+
};
113+
Ok(receiver)
114+
}
115+
116+
pub fn persist<P: Persister<Receiver>>(
117+
&self,
118+
persister: &mut P,
119+
) -> Result<P::Token, ImplementationError> {
120+
let receiver = Receiver { context: self.context.clone() };
121+
Ok(persister.save(receiver)?)
111122
}
123+
}
124+
125+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126+
pub struct Receiver {
127+
context: SessionContext,
128+
}
112129

130+
/// Opaque key type for the receiver
131+
#[derive(Debug, Clone, PartialEq, Eq)]
132+
pub struct ReceiverToken(ShortId);
133+
134+
impl Display for ReceiverToken {
135+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) }
136+
}
137+
138+
impl From<Receiver> for ReceiverToken {
139+
fn from(receiver: Receiver) -> Self { ReceiverToken(id(&receiver.context.s)) }
140+
}
141+
142+
impl AsRef<[u8]> for ReceiverToken {
143+
fn as_ref(&self) -> &[u8] { self.0.as_bytes() }
144+
}
145+
146+
impl persist::Value for Receiver {
147+
type Key = ReceiverToken;
148+
149+
fn key(&self) -> Self::Key { ReceiverToken(id(&self.context.s)) }
150+
}
151+
152+
impl Receiver {
153+
pub fn load<P: Persister<Receiver>>(
154+
token: P::Token,
155+
persister: &P,
156+
) -> Result<Self, ImplementationError> {
157+
persister.load(token).map_err(ImplementationError::from)
158+
}
113159
/// Extract an OHTTP Encapsulated HTTP GET request for the Original PSBT
114160
pub fn extract_req(
115161
&mut self,

payjoin/tests/integration.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ mod integration {
171171

172172
use bitcoin::Address;
173173
use http::StatusCode;
174-
use payjoin::receive::v2::{PayjoinProposal, Receiver, UncheckedProposal};
174+
use payjoin::persist::NoopPersister;
175+
use payjoin::receive::v2::{NewReceiver, PayjoinProposal, Receiver, UncheckedProposal};
175176
use payjoin::send::v2::SenderBuilder;
176177
use payjoin::{OhttpKeys, PjUri, UriExt};
177178
use payjoin_test_utils::{BoxSendSyncError, TestServices};
@@ -205,8 +206,9 @@ mod integration {
205206
let ohttp_relay = services.ohttp_relay_url();
206207
let mock_address = Address::from_str("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4")?
207208
.assume_checked();
208-
let mut bad_initializer =
209-
Receiver::new(mock_address, directory, bad_ohttp_keys, None)?;
209+
let new_receiver = NewReceiver::new(mock_address, directory, bad_ohttp_keys, None)?;
210+
let storage_token = new_receiver.persist(&mut NoopPersister)?;
211+
let mut bad_initializer = Receiver::load(storage_token, &NoopPersister)?;
210212
let (req, _ctx) = bad_initializer.extract_req(&ohttp_relay)?;
211213
agent
212214
.post(req.url)
@@ -242,12 +244,16 @@ mod integration {
242244
// Inside the Receiver:
243245
let address = receiver.get_new_address(None, None)?.assume_checked();
244246
// test session with expiry in the past
245-
let mut expired_receiver = Receiver::new(
247+
let new_receiver = NewReceiver::new(
246248
address.clone(),
247249
directory.clone(),
248250
ohttp_keys.clone(),
249251
Some(Duration::from_secs(0)),
250252
)?;
253+
let storage_token =
254+
new_receiver.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
255+
let mut expired_receiver =
256+
Receiver::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
251257
match expired_receiver.extract_req(&ohttp_relay) {
252258
// Internal error types are private, so check against a string
253259
Err(err) => assert!(err.to_string().contains("expired")),
@@ -294,8 +300,12 @@ mod integration {
294300
let address = receiver.get_new_address(None, None)?.assume_checked();
295301

296302
// test session with expiry in the future
303+
let new_receiver =
304+
NewReceiver::new(address.clone(), directory.clone(), ohttp_keys.clone(), None)?;
305+
let storage_token =
306+
new_receiver.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
297307
let mut session =
298-
Receiver::new(address.clone(), directory.clone(), ohttp_keys.clone(), None)?;
308+
Receiver::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
299309
println!("session: {:#?}", &session);
300310
// Poll receive request
301311
let ohttp_relay = services.ohttp_relay_url();
@@ -465,9 +475,12 @@ mod integration {
465475
let directory = services.directory_url();
466476
let ohttp_keys = services.fetch_ohttp_keys().await?;
467477
let address = receiver.get_new_address(None, None)?.assume_checked();
468-
478+
let new_receiver =
479+
NewReceiver::new(address, directory.clone(), ohttp_keys.clone(), None)?;
480+
let storage_token =
481+
new_receiver.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
469482
let mut session =
470-
Receiver::new(address, directory.clone(), ohttp_keys.clone(), None)?;
483+
Receiver::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
471484

472485
// **********************
473486
// Inside the V1 Sender:
@@ -690,7 +703,8 @@ mod integration {
690703
#[cfg(feature = "_multiparty")]
691704
mod multiparty {
692705
use bitcoin::ScriptBuf;
693-
use payjoin::receive::v2::Receiver;
706+
use payjoin::persist::NoopPersister;
707+
use payjoin::receive::v2::{NewReceiver, Receiver};
694708
use payjoin::send::multiparty::{
695709
GetContext as MultiPartyGetContext, SenderBuilder as MultiPartySenderBuilder,
696710
};
@@ -738,12 +752,16 @@ mod integration {
738752
// Senders will generate a sweep psbt and send PSBT to receiver subdir
739753
for sender in senders.iter() {
740754
let address = receiver.get_new_address(None, None)?.assume_checked();
741-
let receiver_session = Receiver::new(
755+
let new_receiver = NewReceiver::new(
742756
address.clone(),
743757
directory.clone(),
744758
ohttp_keys.clone(),
745759
None,
746760
)?;
761+
let storage_token =
762+
new_receiver.persist(&mut NoopPersister).map_err(|e| e.to_string())?;
763+
let receiver_session =
764+
Receiver::load(storage_token, &NoopPersister).map_err(|e| e.to_string())?;
747765
let pj_uri = receiver_session.pj_uri();
748766
let psbt = build_sweep_psbt(sender, &pj_uri)?;
749767
let sender_ctx = MultiPartySenderBuilder::new(psbt.clone(), pj_uri.clone())

0 commit comments

Comments
 (0)