Skip to content

Commit 3fa1c66

Browse files
authored
Merge pull request #7 from columbia/histogram_and_requests
Histogram and requests
2 parents 2d9e384 + 3449b25 commit 3fa1c66

20 files changed

+703
-145
lines changed

.github/workflows/rust.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ jobs:
2121
- name: Run tests
2222
run: cargo test --verbose
2323
- name: Run integration tests
24-
run: cargo test --package pdslib --test demo -- --nocapture
24+
run: |
25+
cargo test --package pdslib --test ara_demo -- --nocapture
26+
cargo test --package pdslib --test simple_events_demo -- --nocapture

justfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ test:
77
cargo test
88

99
demo:
10-
cargo test --package pdslib --test demo -- --nocapture
10+
cargo test --package pdslib --test simple_events_demo -- --nocapture
11+
cargo test --package pdslib --test ara_demo -- --nocapture
1112

1213
format:
1314
cargo +nightly fmt

src/budget/hashmap_filter_storage.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::budget::traits::{Filter, FilterStorage, FilterStorageError};
1+
use crate::budget::traits::{
2+
Budget, Filter, FilterStorage, FilterStorageError,
3+
};
24
use std::collections::HashMap;
35
use std::marker::PhantomData;
46

@@ -19,18 +21,19 @@ impl<K, F, Budget> HashMapFilterStorage<K, F, Budget> {
1921
}
2022
}
2123

22-
impl<K, F, Budget> FilterStorage for HashMapFilterStorage<K, F, Budget>
24+
impl<K, F, B> FilterStorage for HashMapFilterStorage<K, F, B>
2325
where
24-
F: Filter<Budget>,
26+
B: Budget,
27+
F: Filter<B>,
2528
K: Eq + std::hash::Hash,
2629
{
2730
type FilterId = K;
28-
type Budget = Budget;
31+
type Budget = B;
2932

3033
fn new_filter(
3134
&mut self,
3235
filter_id: K,
33-
capacity: Budget,
36+
capacity: B,
3437
) -> Result<(), FilterStorageError> {
3538
let filter = F::new(capacity);
3639
self.filters.insert(filter_id, filter);
@@ -45,7 +48,7 @@ where
4548
fn try_consume(
4649
&mut self,
4750
filter_id: &K,
48-
budget: &Budget,
51+
budget: &B,
4952
) -> Result<(), FilterStorageError> {
5053
let filter = self
5154
.filters
@@ -54,6 +57,17 @@ where
5457
filter.try_consume(budget)?;
5558
Ok(())
5659
}
60+
61+
fn get_remaining_budget(
62+
&self,
63+
filter_id: &Self::FilterId,
64+
) -> Result<Self::Budget, FilterStorageError> {
65+
let filter = self
66+
.filters
67+
.get(filter_id)
68+
.ok_or(FilterStorageError::FilterDoesNotExist)?;
69+
Ok(filter.get_remaining_budget())
70+
}
5771
}
5872

5973
#[cfg(test)]

src/budget/pure_dp_filter.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
pub use crate::budget::traits::{Filter, FilterError};
1+
pub use crate::budget::traits::{Budget, Filter, FilterError};
22

33
#[derive(Debug, Clone)]
44
pub struct PureDPBudget {
55
pub epsilon: f64,
66
}
77

8+
impl Budget for PureDPBudget {}
9+
810
// TODO: Check whether we can reuse the OpenDP accountant if we want to use RDP/zCDP, without having to execute a measurement on real data. Check out the `compose` function here: https://docs.rs/opendp/latest/opendp/measures/struct.ZeroConcentratedDivergence.html, check if they offer filters directly.
911

1012
#[derive(Debug)]
@@ -31,6 +33,10 @@ impl Filter<PureDPBudget> for PureDPBudgetFilter {
3133
Err(FilterError::OutOfBudget)
3234
}
3335
}
36+
37+
fn get_remaining_budget(&self) -> PureDPBudget {
38+
self.remaining_budget.clone()
39+
}
3440
}
3541

3642
#[cfg(test)]

src/budget/traits.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
// TODO: maybe Budget trait, and Filter<T: Budget> if we need?
2-
31
use thiserror::Error;
42

3+
/// Trait for privacy budgets
4+
pub trait Budget: Clone {
5+
// For now just a marker trait requiring Clone
6+
}
7+
58
/// Error returned when trying to consume from a filter.
69
#[derive(Error, Debug)]
710
pub enum FilterError {
@@ -10,12 +13,18 @@ pub enum FilterError {
1013
}
1114

1215
/// Trait for a privacy filter.
13-
pub trait Filter<T> {
16+
pub trait Filter<T: Budget> {
1417
/// Initializes a new filter with a given capacity.
1518
fn new(capacity: T) -> Self;
1619

17-
/// Tries to consume a given budget from the filter. In the formalism from https://arxiv.org/abs/1605.08294, Ok(()) corresponds to CONTINUE, and Err(FilterError::OutOfBudget) corresponds to HALT..
20+
/// Tries to consume a given budget from the filter.
21+
/// In the formalism from https://arxiv.org/abs/1605.08294, Ok(()) corresponds to CONTINUE, and Err(FilterError::OutOfBudget) corresponds to HALT.
1822
fn try_consume(&mut self, budget: &T) -> Result<(), FilterError>;
23+
24+
/// Gets the remaining budget for this filter.
25+
/// WARNING: this method is for local visualization only.
26+
/// Its output should not be shared outside the device.
27+
fn get_remaining_budget(&self) -> T;
1928
}
2029

2130
/// Error returned when trying to interact with a filter storage.
@@ -32,7 +41,7 @@ pub enum FilterStorageError {
3241
/// Trait for an interface or object that maintains a collection of filters.
3342
pub trait FilterStorage {
3443
type FilterId;
35-
type Budget;
44+
type Budget: Budget;
3645

3746
/// Initializes a new filter with an associated filter ID and capacity.
3847
fn new_filter(
@@ -50,4 +59,10 @@ pub trait FilterStorage {
5059
filter_id: &Self::FilterId,
5160
budget: &Self::Budget,
5261
) -> Result<(), FilterStorageError>;
62+
63+
/// Gets the remaining budget for a filter.
64+
fn get_remaining_budget(
65+
&self,
66+
filter_id: &Self::FilterId,
67+
) -> Result<Self::Budget, FilterStorageError>;
5368
}

src/events/ara_event.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use std::collections::HashMap;
2+
3+
use crate::events::traits::Event;
4+
5+
/// Source event for ARA-style callers such as Chromium.
6+
/// Mimics the fields from https://source.chromium.org/chromium/chromium/src/+/main:content/browser/attribution_reporting/attribution_reporting.proto.
7+
/// TODO: add other fields as needed by callers.
8+
/// TODO: replace String by usize if that simplifies FFI?
9+
#[derive(Debug, Clone)]
10+
pub struct AraEvent {
11+
pub id: usize,
12+
pub epoch_number: usize,
13+
pub aggregatable_sources: HashMap<String, usize>,
14+
// TODO: add filters here
15+
}
16+
17+
impl Event for AraEvent {
18+
type EpochId = usize;
19+
20+
fn get_epoch_id(&self) -> Self::EpochId {
21+
self.epoch_number
22+
}
23+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use crate::events::traits::EventStorage;
2+
use crate::events::traits::RelevantEventSelector;
3+
use crate::events::traits::{EpochEvents, Event};
4+
5+
use std::collections::HashMap;
6+
use std::marker::PhantomData;
7+
8+
pub type VecEpochEvents<E> = Vec<E>;
9+
10+
impl<E: Event> EpochEvents for VecEpochEvents<E> {
11+
fn is_empty(&self) -> bool {
12+
self.is_empty()
13+
}
14+
}
15+
16+
/// A simple in-memory event storage. Stores a mapping of epoch id to epoch
17+
/// events, where each epoch events is just a vec of events.
18+
/// Clones events when asked to retrieve events for an epoch.
19+
#[derive(Debug)]
20+
pub struct HashMapEventStorage<E: Event, RES: RelevantEventSelector<Event = E>>
21+
{
22+
epochs: HashMap<E::EpochId, VecEpochEvents<E>>,
23+
_phantom: PhantomData<RES>,
24+
}
25+
26+
impl<E: Event, RES: RelevantEventSelector<Event = E>>
27+
HashMapEventStorage<E, RES>
28+
{
29+
pub fn new() -> Self {
30+
Self {
31+
epochs: HashMap::new(),
32+
_phantom: PhantomData,
33+
}
34+
}
35+
}
36+
37+
impl<E, RES> EventStorage for HashMapEventStorage<E, RES>
38+
where
39+
E: Event + Clone,
40+
RES: RelevantEventSelector<Event = E>,
41+
{
42+
type Event = E;
43+
type EpochEvents = VecEpochEvents<E>;
44+
type RelevantEventSelector = RES;
45+
46+
fn add_event(&mut self, event: E) -> Result<(), ()> {
47+
let epoch_id = event.get_epoch_id();
48+
let epoch = self.epochs.entry(epoch_id).or_default();
49+
epoch.push(event);
50+
Ok(())
51+
}
52+
53+
fn get_epoch_events(
54+
&self,
55+
epoch_id: &E::EpochId,
56+
selector: &RES,
57+
) -> Option<VecEpochEvents<E>> {
58+
// Return relevant events for a given epoch_id
59+
self.epochs.get(&epoch_id).map(|events| {
60+
events
61+
.iter()
62+
.filter(|event| selector.is_relevant_event(event))
63+
.cloned()
64+
.collect()
65+
})
66+
}
67+
}

src/events/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
pub mod simple_events;
1+
pub mod ara_event;
2+
pub mod hashmap_event_storage;
3+
pub mod simple_event;
24
pub mod traits;

src/events/simple_event.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use crate::events::traits::Event;
2+
3+
/// TODO: add enough things to run basic queries and filter by attributes.
4+
/// use https://github.com/patcg/meetings/blob/main/2024/09/27-tpac/Privacy-Preserving%20Attribution%20Proposed%20Roadmap.pdf
5+
#[derive(Debug, Clone)]
6+
pub struct SimpleEvent {
7+
pub id: usize,
8+
pub epoch_number: usize,
9+
pub event_key: usize,
10+
// TODO: consider adding timestamp
11+
}
12+
13+
impl Event for SimpleEvent {
14+
type EpochId = usize;
15+
16+
fn get_epoch_id(&self) -> Self::EpochId {
17+
self.epoch_number
18+
}
19+
}
20+
21+
#[cfg(test)]
22+
mod tests {
23+
use super::*;
24+
25+
#[test]
26+
fn test_simple_event() {
27+
let event = SimpleEvent {
28+
id: 1,
29+
epoch_number: 1,
30+
event_key: 3,
31+
};
32+
assert_eq!(event.id, 1);
33+
}
34+
}

src/events/simple_events.rs

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)