Skip to content

Commit 58ac6ca

Browse files
Make mailbox TTL configurable, default to 1 week
Remove read/unread TTL distinction to close a metadata side channel. TTL is now operator-configurable via Config, defaulting to 1 week. Removes early_removal_count, mark_read(), read_order, read_mailbox_ids, and the now-dead capacity eviction block. Fix is_none handling in prune to warn without logging shortid.
1 parent 64b83af commit 58ac6ca

4 files changed

Lines changed: 58 additions & 55 deletions

File tree

payjoin-mailroom/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub struct Config {
1212
pub storage_dir: PathBuf,
1313
#[serde(deserialize_with = "deserialize_duration_secs")]
1414
pub timeout: Duration,
15+
#[serde(deserialize_with = "deserialize_duration_secs")]
16+
pub mailbox_ttl: Duration,
1517
pub v1: Option<V1Config>,
1618
#[cfg(feature = "telemetry")]
1719
pub telemetry: Option<TelemetryConfig>,
@@ -85,6 +87,7 @@ impl Default for Config {
8587
listener: "[::]:8080".parse().expect("valid default listener address"),
8688
storage_dir: PathBuf::from("./data"),
8789
timeout: Duration::from_secs(30),
90+
mailbox_ttl: Duration::from_secs(60 * 60 * 24 * 7), // 1 week
8891
v1: None,
8992
#[cfg(feature = "telemetry")]
9093
telemetry: None,
@@ -115,6 +118,7 @@ impl Config {
115118
listener,
116119
storage_dir,
117120
timeout,
121+
mailbox_ttl: Duration::from_secs(60 * 60 * 24 * 7), // 1 week
118122
v1,
119123
#[cfg(feature = "telemetry")]
120124
telemetry: None,

payjoin-mailroom/src/db/files.rs

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use rand::RngCore;
1111
use tokio::fs::{self, File};
1212
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
1313
use tokio::sync::{oneshot, Mutex};
14-
use tracing::trace;
14+
use tracing::{trace, warn};
1515

1616
use crate::db::{Db as DbTrait, Error as DbError};
1717

@@ -21,12 +21,6 @@ use crate::db::{Db as DbTrait, Error as DbError};
2121
/// mailboxes/tx, ~4K txs/block, and ~144 blocks/24h.
2222
const DEFAULT_CAPACITY: usize = 1 << (1 + 12 + 8);
2323

24-
/// How long a mailbox is retained before being pruned, regardless of
25-
/// whether it has been read.
26-
/// Matches the default receiver session lifetime
27-
/// (`TWENTY_FOUR_HOURS_DEFAULT_EXPIRATION` in `payjoin::receive::v2`)
28-
const DEFAULT_TTL: Duration = Duration::from_secs(60 * 60 * 24); // 24 hours
29-
3024
#[derive(Debug)]
3125
struct V2WaitMapEntry {
3226
receiver: future::Shared<oneshot::Receiver<Arc<Vec<u8>>>>,
@@ -49,7 +43,6 @@ pub(crate) struct Mailboxes {
4943
pending_v2: HashMap<ShortId, V2WaitMapEntry>,
5044
insert_order: VecDeque<(SystemTime, ShortId)>,
5145
ttl: Duration,
52-
early_removal_count: usize,
5346
}
5447

5548
#[derive(Debug)]
@@ -194,7 +187,7 @@ impl DiskStorage {
194187
}
195188

196189
impl Mailboxes {
197-
async fn init(dir: PathBuf) -> io::Result<Self> {
190+
async fn init(dir: PathBuf, ttl: Duration) -> io::Result<Self> {
198191
let storage = DiskStorage::init(dir).await?;
199192
let insert_order = storage.insert_order().await?.into();
200193
Ok(Self {
@@ -203,8 +196,7 @@ impl Mailboxes {
203196
capacity: DEFAULT_CAPACITY,
204197
pending_v1: HashMap::default(),
205198
pending_v2: HashMap::default(),
206-
ttl: DEFAULT_TTL,
207-
early_removal_count: 0,
199+
ttl,
208200
})
209201
}
210202
}
@@ -216,8 +208,8 @@ pub struct FilesDb {
216208
}
217209

218210
impl FilesDb {
219-
pub async fn init(timeout: Duration, path: PathBuf) -> io::Result<Self> {
220-
Ok(Self { timeout, mailboxes: Arc::new(Mutex::new(Mailboxes::init(path).await?)) })
211+
pub async fn init(timeout: Duration, path: PathBuf, ttl: Duration) -> io::Result<Self> {
212+
Ok(Self { timeout, mailboxes: Arc::new(Mutex::new(Mailboxes::init(path, ttl).await?)) })
221213
}
222214

223215
pub async fn prune(&self) -> io::Result<Duration> { self.mailboxes.lock().await.prune().await }
@@ -437,9 +429,7 @@ impl Mailboxes {
437429
}
438430

439431
fn len(&self) -> usize {
440-
(self.insert_order.len() - self.early_removal_count)
441-
+ self.pending_v1.len()
442-
+ self.pending_v2.len()
432+
self.insert_order.len() + self.pending_v1.len() + self.pending_v2.len()
443433
}
444434

445435
async fn maybe_prune(&mut self) -> io::Result<Duration> {
@@ -467,36 +457,17 @@ impl Mailboxes {
467457
// Prune any fully expired mailboxes
468458
while let Some((created, id)) = self.insert_order.front().cloned() {
469459
if created + self.ttl < now {
470-
debug_assert!(self.insert_order.len() >= self.early_removal_count);
471460
_ = self.insert_order.pop_front();
472461
if self.remove(&id).await?.is_none() {
473-
self.early_removal_count = self
474-
.early_removal_count
475-
.checked_sub(1)
476-
.expect("early removal adjustment should never underflow");
462+
warn!("Mailbox file missing during prune; possible external deletion or disk error");
463+
} else {
464+
trace!("Pruned old mailbox {id}");
477465
}
478-
debug_assert!(self.insert_order.len() >= self.early_removal_count);
479-
trace!("Pruned old mailbox {id}");
480466
} else {
481467
break;
482468
}
483469
}
484470

485-
// If no room was created, try to prune the oldest mailbox if
486-
// it's over the TTL
487-
debug_assert!(self.len() <= self.capacity);
488-
if self.len() == self.capacity {
489-
if let Some((created, id)) = self.insert_order.front().cloned() {
490-
if created + self.ttl < now {
491-
_ = self.insert_order.pop_front();
492-
self.remove(&id).await?;
493-
trace!("Pruned mailbox {id} to make room");
494-
} else {
495-
trace!("Nothing to prune, {} entries remain", self.len());
496-
}
497-
}
498-
}
499-
500471
Ok(self.next_prune())
501472
}
502473

@@ -704,9 +675,13 @@ async fn test_disk_storage_mailboxes() -> std::io::Result<()> {
704675
async fn test_mailbox_storage() -> std::io::Result<()> {
705676
let dir = tempfile::tempdir()?;
706677

707-
let db = FilesDb::init(Duration::from_millis(10), dir.path().to_owned())
708-
.await
709-
.expect("initializing mailbox database should succeed");
678+
let db = FilesDb::init(
679+
Duration::from_millis(10),
680+
dir.path().to_owned(),
681+
Duration::from_secs(60 * 60 * 24 * 7),
682+
)
683+
.await
684+
.expect("initializing mailbox database should succeed");
710685

711686
let id = ShortId([0u8; 8]);
712687
let contents = b"foo bar";
@@ -725,9 +700,13 @@ async fn test_mailbox_storage() -> std::io::Result<()> {
725700
async fn test_v2_wait() -> std::io::Result<()> {
726701
let dir = tempfile::tempdir()?;
727702

728-
let db = FilesDb::init(Duration::from_millis(1), dir.path().to_owned())
729-
.await
730-
.expect("initializing mailbox database should succeed");
703+
let db = FilesDb::init(
704+
Duration::from_millis(1),
705+
dir.path().to_owned(),
706+
Duration::from_secs(60 * 60 * 24 * 7),
707+
)
708+
.await
709+
.expect("initializing mailbox database should succeed");
731710

732711
let id = ShortId([0u8; 8]);
733712
let contents = b"foo bar";
@@ -782,9 +761,13 @@ async fn test_v1_wait() -> std::io::Result<()> {
782761
let dir = tempfile::tempdir()?;
783762

784763
let db = Arc::new(
785-
FilesDb::init(Duration::from_millis(1), dir.path().to_owned())
786-
.await
787-
.expect("initializing mailbox database should succeed"),
764+
FilesDb::init(
765+
Duration::from_millis(1),
766+
dir.path().to_owned(),
767+
Duration::from_secs(60 * 60 * 24 * 7),
768+
)
769+
.await
770+
.expect("initializing mailbox database should succeed"),
788771
);
789772

790773
let id = ShortId([0u8; 8]);
@@ -829,9 +812,13 @@ async fn test_v1_data_minimization() -> std::io::Result<()> {
829812
let dir = tempfile::tempdir()?;
830813

831814
let db = Arc::new(
832-
FilesDb::init(Duration::from_millis(500), dir.path().to_owned())
833-
.await
834-
.expect("initializing mailbox database should succeed"),
815+
FilesDb::init(
816+
Duration::from_millis(500),
817+
dir.path().to_owned(),
818+
Duration::from_secs(60 * 60 * 24 * 7),
819+
)
820+
.await
821+
.expect("initializing mailbox database should succeed"),
835822
);
836823

837824
let id = ShortId([0u8; 8]);
@@ -884,9 +871,13 @@ async fn test_v1_data_minimization() -> std::io::Result<()> {
884871
async fn test_prune() -> std::io::Result<()> {
885872
let dir = tempfile::tempdir()?;
886873

887-
let db = FilesDb::init(Duration::from_millis(2), dir.path().to_owned())
888-
.await
889-
.expect("initializing mailbox database should succeed");
874+
let db = FilesDb::init(
875+
Duration::from_millis(2),
876+
dir.path().to_owned(),
877+
Duration::from_secs(60 * 60 * 24 * 7),
878+
)
879+
.await
880+
.expect("initializing mailbox database should succeed");
890881

891882
let ttl = Duration::from_secs(600);
892883

payjoin-mailroom/src/directory.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,13 @@ mod tests {
591591

592592
async fn test_service(v1: Option<V1>) -> Service<FilesDb> {
593593
let dir = tempfile::tempdir().expect("tempdir");
594-
let db = FilesDb::init(Duration::from_millis(100), dir.keep()).await.expect("db init");
594+
let db = FilesDb::init(
595+
Duration::from_millis(100),
596+
dir.keep(),
597+
Duration::from_secs(60 * 60 * 24 * 7),
598+
)
599+
.await
600+
.expect("db init");
595601
let ohttp: ohttp::Server =
596602
crate::key_config::gen_ohttp_server_config().expect("ohttp config").into();
597603
Service::new(db, ohttp, SentinelTag::new([0u8; 32]), v1)

payjoin-mailroom/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ async fn init_directory(
221221
config: &Config,
222222
sentinel_tag: SentinelTag,
223223
) -> anyhow::Result<crate::directory::Service<crate::db::DbServiceAdapter>> {
224-
let files_db = crate::db::FilesDb::init(config.timeout, config.storage_dir.clone()).await?;
224+
let files_db =
225+
crate::db::FilesDb::init(config.timeout, config.storage_dir.clone(), config.mailbox_ttl)
226+
.await?;
225227
files_db.spawn_background_prune().await;
226228
let db = crate::db::DbServiceAdapter::new(files_db);
227229

0 commit comments

Comments
 (0)