use crate::{
hpke::HpkeReceiverConfig,
messages::{
BatchSelector, CollectResp, Duration, HpkeConfig, Id, Interval, PartialBatchSelector,
ReportId, ReportMetadata, Time, TransitionFailure,
},
vdaf::{
prio2::prio2_decode_prepare_state,
prio3::{prio3_append_prepare_state, prio3_decode_prepare_state},
VdafAggregateShare, VdafError, VdafMessage, VdafState, VdafVerifyKey,
},
};
use messages::HpkeKemId;
use prio::{
codec::{CodecError, Decode, Encode},
vdaf::Aggregatable as AggregatableTrait,
};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
};
use url::Url;
#[derive(Debug, thiserror::Error)]
pub enum DapError {
#[error("fatal error: {0}")]
Fatal(String),
#[error("abort: {0}")]
Abort(DapAbort),
#[error("transition error: {0}")]
Transition(TransitionFailure),
}
impl DapError {
pub fn fatal(s: &'static str) -> Self {
Self::Fatal(s.into())
}
}
impl From<serde_json::Error> for DapError {
fn from(e: serde_json::Error) -> Self {
Self::Fatal(format!("serde_json: {}", e))
}
}
impl From<hex::FromHexError> for DapError {
fn from(e: hex::FromHexError) -> Self {
Self::Fatal(format!("from hex: {}", e))
}
}
impl From<CodecError> for DapError {
fn from(e: CodecError) -> Self {
Self::Fatal(format!("codec: {}", e))
}
}
impl From<VdafError> for DapError {
fn from(e: VdafError) -> Self {
match e {
VdafError::Codec(..) | VdafError::Vdaf(..) => {
Self::Transition(TransitionFailure::VdafPrepError)
}
}
}
}
impl From<::hpke::HpkeError> for DapError {
fn from(_e: ::hpke::HpkeError) -> Self {
Self::Transition(TransitionFailure::HpkeDecryptError)
}
}
#[derive(Debug, thiserror::Error)]
pub enum DapAbort {
#[error("badRequest")]
BadRequest(String),
#[error("batchInvalid")]
BatchInvalid,
#[error("batchMismatch")]
BatchMismatch,
#[error("batchOverlap")]
BatchOverlap,
#[error("{0}")]
Internal(#[source] Box<dyn std::error::Error + 'static + Send + Sync>),
#[error("invalidProtocolVersion")]
InvalidProtocolVersion,
#[error("invalidBatchSize")]
InvalidBatchSize,
#[error("missingTaskID")]
MissingTaskId,
#[error("queryMismatch")]
QueryMismatch,
#[error("replayedReport")]
ReplayedReport,
#[error("reportTooLate")]
ReportTooLate,
#[error("staleReport")]
StaleReport,
#[error("unauthorizedRequest")]
UnauthorizedRequest,
#[error("unrecognizedAggregationJob")]
UnrecognizedAggregationJob,
#[error("unrecognizedHpkeConfig")]
UnrecognizedHpkeConfig,
#[error("unrecognizedMessage")]
UnrecognizedMessage,
#[error("unrecognizedTask")]
UnrecognizedTask,
}
impl DapAbort {
pub fn to_problem_details(&self) -> ProblemDetails {
let (typ, detail) = match self {
Self::BatchInvalid
| Self::BatchMismatch
| Self::BatchOverlap
| Self::InvalidProtocolVersion
| Self::InvalidBatchSize
| Self::QueryMismatch
| Self::MissingTaskId
| Self::ReplayedReport
| Self::ReportTooLate
| Self::StaleReport
| Self::UnauthorizedRequest
| Self::UnrecognizedAggregationJob
| Self::UnrecognizedHpkeConfig
| Self::UnrecognizedMessage
| Self::UnrecognizedTask => (self.to_string(), None),
Self::BadRequest(s) => ("badRequest".to_string(), Some(s.clone())),
Self::Internal(e) => ("internalError".to_string(), Some(e.to_string())),
};
ProblemDetails {
typ: format!("urn:ietf:params:ppm:dap:error:{}", typ),
taskid: None, instance: None, detail,
}
}
}
impl From<DapError> for DapAbort {
fn from(e: DapError) -> Self {
match e {
e @ DapError::Fatal(..) => Self::Internal(Box::new(e)),
DapError::Abort(e) => e,
DapError::Transition(t) => Self::from(t),
}
}
}
impl From<CodecError> for DapAbort {
fn from(_e: CodecError) -> Self {
Self::UnrecognizedMessage
}
}
impl From<TransitionFailure> for DapAbort {
fn from(failure_reason: TransitionFailure) -> Self {
match failure_reason {
TransitionFailure::BatchCollected => Self::StaleReport,
TransitionFailure::ReportReplayed => Self::ReplayedReport,
_ => DapError::fatal("unhandled transition failure").into(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ProblemDetails {
#[serde(rename = "type")]
pub typ: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) taskid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) instance: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum DapVersion {
#[serde(rename = "v02")]
Draft02,
#[serde(other)]
Unknown,
}
impl From<&str> for DapVersion {
fn from(version: &str) -> Self {
match version {
"v02" => DapVersion::Draft02,
_ => DapVersion::Unknown,
}
}
}
impl AsRef<str> for DapVersion {
fn as_ref(&self) -> &str {
match self {
DapVersion::Draft02 => "v02",
_ => panic!("tried to construct string from unknown DAP version"),
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct DapGlobalConfig {
pub report_storage_epoch_duration: Duration,
pub max_batch_duration: Duration,
pub min_batch_interval_start: Duration,
pub max_batch_interval_end: Duration,
pub supported_hpke_kems: Vec<HpkeKemId>,
}
impl DapGlobalConfig {
pub fn gen_hpke_receiver_config_list(
&self,
first_config_id: u8,
) -> impl IntoIterator<Item = HpkeReceiverConfig> {
assert!(self.supported_hpke_kems.len() <= 256);
let kem_ids = self.supported_hpke_kems.clone();
kem_ids.into_iter().enumerate().map(move |(i, kem_id)| {
let (config_id, _overflowed) = first_config_id.overflowing_add(i as u8);
HpkeReceiverConfig::gen(config_id, kem_id)
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DapQueryConfig {
TimeInterval,
FixedSize { max_batch_size: u64 },
}
impl DapQueryConfig {
pub(crate) fn is_valid_part_batch_sel(&self, part_batch_sel: &PartialBatchSelector) -> bool {
matches!(
(&self, part_batch_sel),
(
Self::TimeInterval { .. },
PartialBatchSelector::TimeInterval
) | (
Self::FixedSize { .. },
PartialBatchSelector::FixedSize { .. }
)
)
}
pub(crate) fn is_valid_batch_sel(&self, batch_sel: &BatchSelector) -> bool {
matches!(
(&self, batch_sel),
(
Self::TimeInterval { .. },
BatchSelector::TimeInterval { .. }
) | (Self::FixedSize { .. }, BatchSelector::FixedSize { .. })
)
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub enum DapBatchBucket<'a> {
FixedSize { batch_id: &'a Id },
TimeInterval { batch_window: Time },
}
#[derive(Clone, Deserialize, Serialize)]
pub struct DapTaskConfig {
pub version: DapVersion,
pub leader_url: Url,
pub helper_url: Url,
pub time_precision: Duration,
pub expiration: Time,
pub min_batch_size: u64,
pub query: DapQueryConfig,
pub vdaf: VdafConfig,
pub vdaf_verify_key: VdafVerifyKey,
pub collector_hpke_config: HpkeConfig,
}
impl DapTaskConfig {
#[cfg(test)]
pub fn query_for_current_batch_window(&self, now: u64) -> crate::messages::Query {
let start = now - (now % self.time_precision);
crate::messages::Query::TimeInterval {
batch_interval: crate::messages::Interval {
start,
duration: self.time_precision,
},
}
}
pub(crate) fn truncate_time(&self, time: Time) -> Time {
time - (time % self.time_precision)
}
pub fn batch_span_for_out_shares<'a>(
&self,
part_batch_sel: &'a PartialBatchSelector,
out_shares: Vec<DapOutputShare>,
) -> Result<HashMap<DapBatchBucket<'a>, DapAggregateShare>, DapError> {
if !self.query.is_valid_part_batch_sel(part_batch_sel) {
return Err(DapError::fatal(
"partial batch selector not compatible with task",
));
}
let mut span: HashMap<DapBatchBucket<'a>, DapAggregateShare> = HashMap::new();
for out_share in out_shares.into_iter() {
let bucket = match part_batch_sel {
PartialBatchSelector::TimeInterval => DapBatchBucket::TimeInterval {
batch_window: self.truncate_time(out_share.time),
},
PartialBatchSelector::FixedSize { batch_id } => {
DapBatchBucket::FixedSize { batch_id }
}
};
let agg_share = span.entry(bucket).or_default();
agg_share.merge(DapAggregateShare {
report_count: 1,
checksum: out_share.checksum,
data: Some(out_share.data),
})?;
}
Ok(span)
}
pub fn batch_span_for_sel<'a>(
&self,
batch_sel: &'a BatchSelector,
) -> Result<HashSet<DapBatchBucket<'a>>, DapError> {
if !self.query.is_valid_batch_sel(batch_sel) {
return Err(DapError::fatal("batch selector not compatible with task"));
}
match batch_sel {
BatchSelector::TimeInterval {
batch_interval: Interval { start, duration },
} => {
let windows = duration / self.time_precision;
let mut span = HashSet::with_capacity(windows as usize);
for i in 0..windows {
span.insert(DapBatchBucket::TimeInterval {
batch_window: start + i * self.time_precision,
});
}
Ok(span)
}
BatchSelector::FixedSize { batch_id } => {
Ok(HashSet::from([DapBatchBucket::FixedSize { batch_id }]))
}
}
}
pub fn batch_span_for_meta<'a>(
&self,
part_batch_sel: &'a PartialBatchSelector,
report_meta: impl Iterator<Item = &'a ReportMetadata>,
) -> Result<HashMap<DapBatchBucket<'a>, Vec<&'a ReportMetadata>>, DapError> {
if !self.query.is_valid_part_batch_sel(part_batch_sel) {
return Err(DapError::fatal(
"partial batch selector not compatible with task",
));
}
let mut span: HashMap<_, Vec<_>> = HashMap::new();
for metadata in report_meta {
let bucket = match part_batch_sel {
PartialBatchSelector::TimeInterval => DapBatchBucket::TimeInterval {
batch_window: self.truncate_time(metadata.time),
},
PartialBatchSelector::FixedSize { batch_id } => {
DapBatchBucket::FixedSize { batch_id }
}
};
let report_ids = span.entry(bucket).or_default();
report_ids.push(metadata);
}
Ok(span)
}
pub(crate) fn is_report_count_compatible(&self, report_count: u64) -> Result<bool, DapAbort> {
match self.query {
DapQueryConfig::TimeInterval => (),
DapQueryConfig::FixedSize { max_batch_size } => {
if report_count > max_batch_size {
return Err(DapAbort::InvalidBatchSize);
}
}
};
Ok(report_count >= self.min_batch_size)
}
}
impl AsRef<DapTaskConfig> for DapTaskConfig {
fn as_ref(&self) -> &Self {
self
}
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DapMeasurement {
U64(u64),
U32Vec(Vec<u32>),
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DapAggregateResult {
U32Vec(Vec<u32>),
U64(u64),
U128(u128),
U128Vec(Vec<u128>),
}
#[derive(Debug)]
pub struct DapLeaderState {
pub(crate) seq: Vec<(VdafState, VdafMessage, Time, ReportId)>,
}
#[derive(Debug)]
pub struct DapLeaderUncommitted {
pub(crate) seq: Vec<(DapOutputShare, ReportId)>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DapHelperState {
pub(crate) part_batch_sel: PartialBatchSelector,
pub(crate) seq: Vec<(VdafState, Time, ReportId)>,
}
impl DapHelperState {
pub fn get_encoded(&self, vdaf_config: &VdafConfig) -> Result<Vec<u8>, DapError> {
let mut bytes = vec![];
self.part_batch_sel.encode(&mut bytes);
for (state, time, report_id) in self.seq.iter() {
match (vdaf_config, state) {
(VdafConfig::Prio3(prio3_config), _) => {
prio3_append_prepare_state(&mut bytes, prio3_config, state)?;
}
(VdafConfig::Prio2 { .. }, VdafState::Prio2(state)) => {
state.encode(&mut bytes);
}
_ => return Err(DapError::fatal("VDAF config and prep state mismatch")),
}
time.encode(&mut bytes);
report_id.encode(&mut bytes);
}
Ok(bytes)
}
pub fn get_decoded(vdaf_config: &VdafConfig, data: &[u8]) -> Result<Self, DapError> {
let mut r = std::io::Cursor::new(data);
let part_batch_sel = PartialBatchSelector::decode(&mut r)?;
let mut seq = vec![];
while (r.position() as usize) < data.len() {
let state = match vdaf_config {
VdafConfig::Prio3(ref prio3_config) => {
prio3_decode_prepare_state(prio3_config, 1, &mut r)?
}
VdafConfig::Prio2 { dimension } => {
prio2_decode_prepare_state(*dimension, 1, &mut r)?
}
};
let time = Time::decode(&mut r)?;
let report_id = ReportId::decode(&mut r)?;
seq.push((state, time, report_id))
}
Ok(DapHelperState {
part_batch_sel,
seq,
})
}
}
#[derive(Debug)]
pub struct DapOutputShare {
pub(crate) time: u64, pub(crate) checksum: [u8; 32],
pub(crate) data: VdafAggregateShare,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct DapAggregateShare {
pub(crate) report_count: u64,
pub(crate) checksum: [u8; 32],
pub(crate) data: Option<VdafAggregateShare>,
}
impl DapAggregateShare {
pub fn merge(&mut self, other: DapAggregateShare) -> Result<(), DapError> {
match (self.data.as_mut(), other.data) {
(_, None) => (),
(None, Some(data)) => {
self.data = Some(data);
}
(Some(VdafAggregateShare::Field64(left)), Some(VdafAggregateShare::Field64(right))) => {
left.merge(&right)
.map_err(|e| DapError::Fatal(e.to_string()))?;
}
(
Some(VdafAggregateShare::Field128(left)),
Some(VdafAggregateShare::Field128(right)),
) => {
left.merge(&right)
.map_err(|e| DapError::Fatal(e.to_string()))?;
}
(
Some(VdafAggregateShare::FieldPrio2(left)),
Some(VdafAggregateShare::FieldPrio2(right)),
) => {
left.merge(&right)
.map_err(|e| DapError::Fatal(e.to_string()))?;
}
_ => return Err(DapError::fatal("invalid aggregate share merge")),
};
self.report_count += other.report_count;
for (x, y) in self.checksum.iter_mut().zip(other.checksum) {
*x ^= y;
}
Ok(())
}
pub fn empty(&self) -> bool {
self.report_count == 0
}
pub fn reset(&mut self) {
self.report_count = 0;
self.checksum = [0; 32];
self.data = None;
}
#[cfg(test)]
pub(crate) fn try_from_out_shares(
out_shares: impl IntoIterator<Item = DapOutputShare>,
) -> Result<Self, DapError> {
let mut agg_share = Self::default();
for out_share in out_shares.into_iter() {
agg_share.merge(DapAggregateShare {
report_count: 1,
checksum: out_share.checksum,
data: Some(out_share.data),
})?;
}
Ok(agg_share)
}
}
#[derive(Debug)]
pub enum DapLeaderTransition<M: Debug> {
Continue(DapLeaderState, M),
Uncommitted(DapLeaderUncommitted, M),
Skip,
}
#[derive(Debug)]
pub enum DapHelperTransition<M: Debug> {
Continue(DapHelperState, M),
Finish(Vec<DapOutputShare>, M),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VdafConfig {
Prio3(Prio3Config),
Prio2 { dimension: u32 },
}
impl std::str::FromStr for VdafConfig {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Prio3Config {
Count,
Histogram { buckets: Vec<u64> },
Sum { bits: u32 },
}
#[derive(Debug)]
pub struct DapRequest<S> {
pub version: DapVersion,
pub media_type: Option<&'static str>,
pub task_id: Option<Id>,
pub payload: Vec<u8>,
pub url: Url,
pub sender_auth: Option<S>,
}
impl<S> DapRequest<S> {
pub(crate) fn task_id(&self) -> Result<&Id, DapAbort> {
if let Some(ref id) = self.task_id {
Ok(id)
} else {
Err(DapAbort::UnrecognizedMessage)
}
}
}
#[derive(Debug)]
pub struct DapResponse {
pub media_type: Option<&'static str>,
pub payload: Vec<u8>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DapCollectJob {
Done(CollectResp),
Pending,
Unknown,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct DapLeaderProcessTelemetry {
pub reports_collected: u64,
pub reports_aggregated: u64,
pub reports_processed: u64,
}
pub mod auth;
pub mod constants;
pub mod hpke;
#[cfg(test)]
mod hpke_test;
pub mod messages;
pub mod roles;
#[cfg(test)]
mod roles_test;
pub mod testing;
pub mod vdaf;