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}