star_constellation/lib.rs
1//! The `star-constellation` crate implements the Constellation
2//! aggregation mechanism: a modification of the original
3//! [STAR](https://github.com/brave/sta-rs) protocol to allow
4//! clients to submit ordered, granular data at the highest
5//! resolution that is possible, whilst maintaining crowd-based
6//! anonymity.
7//!
8//! Constellation provides both higher utility for data aggregation
9//! than STAR alone (revealing partial measurements where possible),
10//! and better privacy for fine-grained client data.
11//!
12//! ## Background
13//!
14//! Specifically, Constellation 'nests' or 'layers' an ordered vector of
15//! measurements into associated STAR messages, such that each message
16//! can only be accessed if the STAR message at the previous layer was
17//! included in a successful recovery. The privacy of unrevealed layers
18//! is provided using symmetric encryption, that can only be decrypted
19//! using a key enclosed in the previous STAR message.
20//!
21//! ## Example API usage
22//!
23//! ### Client
24//!
25//! The Client produces a message for threshold aggregation using the
26//! Constellation format.
27//!
28//! ```
29//! # use star_constellation::api::*;
30//! # use star_constellation::randomness::*;
31//! # use star_constellation::randomness::testing::{LocalFetcher as RandomnessFetcher};
32//! # use star_constellation::consts::*;
33//! # use star_constellation::format::*;
34//! #
35//! let threshold = 10;
36//! let epoch = 0u8;
37//! let random_fetcher = RandomnessFetcher::new();
38//!
39//! // setup randomness server information
40//! let example_aux = vec![1u8; 3];
41//!
42//! let measurements = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
43//! let rrs = client::prepare_measurement(&measurements, epoch).unwrap();
44//! let req = client::construct_randomness_request(&rrs);
45//!
46//! let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
47//! let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();
48//!
49//! let points_slice_vec: Vec<&[u8]> =
50//! resp.serialized_points.iter().map(|v| v.as_slice()).collect();
51//! let proofs_slice_vec: Vec<&[u8]> =
52//! resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
53//! client::construct_message(
54//! &points_slice_vec,
55//! Some(&proofs_slice_vec),
56//! &rrs,
57//! &Some(random_fetcher.get_server().get_public_key()),
58//! &example_aux,
59//! threshold
60//! ).unwrap();
61//! ```
62//!
63//! ### Server
64//!
65//! Server aggregation takes a number of client messages as input, and
66//! outputs those measurements that were received from at least
67//! `threshold` clients. It also reveals prefixes of full measurements
68//! that were received by greater than `threshold` clients.
69//!
70//! #### Full recovery
71//!
72//! After receiving at least `threshold` client messages of the same
73//! full measurement, the server can run aggregation and reveal the
74//! client measurement.
75//!
76//! ```
77//! # use star_constellation::api::*;
78//! # use star_constellation::randomness::*;
79//! # use star_constellation::randomness::testing::{LocalFetcher as RandomnessFetcher};
80//! # use star_constellation::consts::*;
81//! # use star_constellation::format::*;
82//! #
83//! let threshold = 10;
84//! let epoch = 0u8;
85//! let random_fetcher = RandomnessFetcher::new();
86//!
87//! // construct at least `threshold` client messages with the same measurement
88//! let measurements_1 = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
89//! let client_messages_to_reveal: Vec<Vec<u8>> = (0..threshold).into_iter().map(|i| {
90//! let example_aux = vec![i as u8; 3];
91//! let rrs = client::prepare_measurement(&measurements_1, epoch).unwrap();
92//! let req = client::construct_randomness_request(&rrs);
93//!
94//! let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
95//! let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();
96//!
97//! let points_slice_vec: Vec<&[u8]> =
98//! resp.serialized_points.iter().map(|v| v.as_slice()).collect();
99//! let proofs_slice_vec: Vec<&[u8]> =
100//! resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
101//! client::construct_message(
102//! &points_slice_vec,
103//! Some(&proofs_slice_vec),
104//! &rrs,
105//! &Some(random_fetcher.get_server().get_public_key()),
106//! &example_aux,
107//! threshold
108//! ).unwrap()
109//! }).collect();
110//!
111//! // construct a low number client messages with a different measurement
112//! let measurements_2 = vec!["something".as_bytes().to_vec(), "else".as_bytes().to_vec()];
113//! let client_messages_to_hide: Vec<Vec<u8>> = (0..2).into_iter().map(|i| {
114//! let example_aux = vec![i as u8; 3];
115//! let rrs = client::prepare_measurement(&measurements_2, epoch).unwrap();
116//! let req = client::construct_randomness_request(&rrs);
117//!
118//! let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
119//! let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();
120//!
121//! let points_slice_vec: Vec<&[u8]> =
122//! resp.serialized_points.iter().map(|v| v.as_slice()).collect();
123//! let proofs_slice_vec: Vec<&[u8]> =
124//! resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
125//! client::construct_message(
126//! &points_slice_vec,
127//! Some(&proofs_slice_vec),
128//! &rrs,
129//! &Some(random_fetcher.get_server().get_public_key()),
130//! &example_aux,
131//! threshold
132//! ).unwrap()
133//! }).collect();
134//!
135//! // aggregation reveals the client measurement that reaches the
136//! // threshold, the other measurement stays hidden
137//! let agg_res = server::aggregate(
138//! &[client_messages_to_reveal, client_messages_to_hide].concat(),
139//! threshold,
140//! epoch,
141//! measurements_1.len()
142//! );
143//! let output = agg_res.outputs();
144//! assert_eq!(output.len(), 1);
145//! let revealed_output = output.iter().find(|v| v.value() == vec!["world"]).unwrap();
146//! assert_eq!(revealed_output.value(), vec!["world"]);
147//! assert_eq!(revealed_output.occurrences(), 10);
148//! (0..10).into_iter().for_each(|i| {
149//! assert_eq!(revealed_output.auxiliary_data()[i], vec![i as u8; 3]);
150//! });
151//! ```
152//!
153//! #### Partial recovery
154//!
155//! Partial recovery allows revealing prefixes of full measurements that
156//! are received by enough clients, even when the full measurements
157//! themselves stay hidden.
158//!
159//! ```
160//! # use star_constellation::api::*;
161//! # use star_constellation::randomness::*;
162//! # use star_constellation::randomness::testing::{LocalFetcher as RandomnessFetcher};
163//! # use star_constellation::consts::*;
164//! # use star_constellation::format::*;
165//! #
166//! let threshold = 10;
167//! let epoch = 0u8;
168//! let random_fetcher = RandomnessFetcher::new();
169//!
170//! // construct a low number client messages with the same measurement
171//! let measurements_1 = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
172//! let client_messages_1: Vec<Vec<u8>> = (0..5).into_iter().map(|i| {
173//! let example_aux = vec![i as u8; 3];
174//! let rrs = client::prepare_measurement(&measurements_1, epoch).unwrap();
175//! let req = client::construct_randomness_request(&rrs);
176//!
177//! let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
178//! let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();
179//!
180//! let points_slice_vec: Vec<&[u8]> =
181//! resp.serialized_points.iter().map(|v| v.as_slice()).collect();
182//! let proofs_slice_vec: Vec<&[u8]> =
183//! resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
184//! client::construct_message(
185//! &points_slice_vec,
186//! Some(&proofs_slice_vec),
187//! &rrs,
188//! &Some(random_fetcher.get_server().get_public_key()),
189//! &example_aux,
190//! threshold
191//! ).unwrap()
192//! }).collect();
193//!
194//! // construct a low number of measurements that also share a prefix
195//! let measurements_2 = vec!["hello".as_bytes().to_vec(), "goodbye".as_bytes().to_vec()];
196//! let client_messages_2: Vec<Vec<u8>> = (0..5).into_iter().map(|i| {
197//! let example_aux = vec![i as u8; 3];
198//! let rrs = client::prepare_measurement(&measurements_2, epoch).unwrap();
199//! let req = client::construct_randomness_request(&rrs);
200//!
201//! let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
202//! let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();
203//!
204//! let points_slice_vec: Vec<&[u8]> =
205//! resp.serialized_points.iter().map(|v| v.as_slice()).collect();
206//! let proofs_slice_vec: Vec<&[u8]> =
207//! resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
208//! client::construct_message(
209//! &points_slice_vec,
210//! Some(&proofs_slice_vec),
211//! &rrs,
212//! &Some(random_fetcher.get_server().get_public_key()),
213//! &example_aux,
214//! threshold
215//! ).unwrap()
216//! }).collect();
217//!
218//! // aggregation reveals the partial client measurement `vec!["hello"]`,
219//! // but the full measurements stay hidden
220//! let agg_res = server::aggregate(
221//! &[client_messages_1, client_messages_2].concat(),
222//! threshold,
223//! epoch,
224//! measurements_1.len()
225//! );
226//! let output = agg_res.outputs();
227//! assert_eq!(output.len(), 1);
228//! assert_eq!(output[0].value(), vec!["hello"]);
229//! assert_eq!(output[0].occurrences(), 10);
230//! (0..10).into_iter().for_each(|i| {
231//! let val = i % 5;
232//! assert_eq!(output[0].auxiliary_data()[i], vec![val as u8; 3]);
233//! });
234//! ```
235mod internal;
236
237pub mod api;
238pub mod format;
239pub mod randomness;
240
241pub mod consts {
242 pub const RANDOMNESS_LEN: usize = 32;
243}
244
245#[derive(Debug, Clone, Eq, PartialEq)]
246pub enum Error {
247 ShareRecovery,
248 ClientMeasurementMismatch(String, String),
249 LayerEncryptionKeys(usize, usize),
250 NumMeasurementLayers(usize, usize),
251 Serialization(String),
252 RandomnessSampling(String),
253 MessageGeneration(String),
254 MessageParse,
255 ProofMissing,
256 MissingVerificationParams,
257}
258
259impl std::error::Error for Error {}
260
261impl std::fmt::Display for Error {
262 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
263 match self {
264 Self::ShareRecovery => write!(f, "Internal share recovery failed"),
265 Self::ClientMeasurementMismatch(original, received) => write!(f, "Clients sent differing measurement for identical share sets, original: {original}, received: {received}"),
266 Self::LayerEncryptionKeys(nkeys, nlayers) => write!(f, "Number of encryption keys ({nkeys}) provided for nested encryptions is not compatible with number of layers specified ({nlayers})."),
267 Self::NumMeasurementLayers(current, expected) => write!(f, "Number of inferred measurement layers is {current}, but expected is {expected}."),
268 Self::Serialization(err_string) => write!(f, "An error occurred during serialization/deserialization: {err_string}."),
269 Self::RandomnessSampling(err_string) => write!(f, "An error occurred during the sampling of randomness: {err_string}."),
270 Self::MessageGeneration(err_string) => write!(f, "An error when attempting to generate the message: {err_string}."),
271 Self::MessageParse => write!(f, "An error when attempting to parse the message."),
272 Self::ProofMissing => write!(f, "Proof missing for randomness point."),
273 Self::MissingVerificationParams => write!(f, "Verification key or proofs missing, must supply both or none.")
274 }
275 }
276}