Skip to content

Commit

Permalink
validate vdaf and query type are supported by both aggregators (#359)
Browse files Browse the repository at this point in the history
* validate vdaf and query type are supported by both aggregators

* hide vdafs and query types that aren't supported
  • Loading branch information
jbr authored Aug 1, 2023
1 parent 850a809 commit 905e2a5
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 12 deletions.
2 changes: 2 additions & 0 deletions app/src/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export interface Aggregator {
role: Role;
name: string;
is_first_party: boolean;
vdafs: string[];
query_types: string[];
}

export interface NewAggregator {
Expand Down
79 changes: 71 additions & 8 deletions app/src/tasks/TaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ function LeaderAggregator(props: Props) {
const { aggregators } = useLoaderData() as {
aggregators: Promise<Aggregator[]>;
};
let { helper_aggregator_id } = props.values;

return (
<TaskFormGroup controlId="leader_aggregator_id">
Expand All @@ -444,9 +445,13 @@ function LeaderAggregator(props: Props) {
<Await resolve={aggregators}>
{(aggregators: Aggregator[]) =>
aggregators
.filter((a) => a.role === "Leader" || a.role === "Either")
.filter(({ role }) => role === "Leader" || role === "Either")
.map((aggregator) => (
<option key={aggregator.id} value={aggregator.id}>
<option
key={aggregator.id}
value={aggregator.id}
disabled={aggregator.id === helper_aggregator_id}
>
{aggregator.name}
</option>
))
Expand All @@ -465,6 +470,7 @@ function HelperAggregator(props: Props) {
const { aggregators } = useLoaderData() as {
aggregators: Promise<Aggregator[]>;
};
let { leader_aggregator_id } = props.values;
return (
<TaskFormGroup>
<ShortHelpAndLabel
Expand All @@ -484,9 +490,13 @@ function HelperAggregator(props: Props) {
<Await resolve={aggregators}>
{(aggregators: Aggregator[]) =>
aggregators
.filter((a) => a.role === "Helper" || a.role === "Either")
.filter(({ role }) => role === "Helper" || role === "Either")
.map((aggregator) => (
<option key={aggregator.id} value={aggregator.id}>
<option
key={aggregator.id}
value={aggregator.id}
disabled={aggregator.id === leader_aggregator_id}
>
{aggregator.name}
</option>
))
Expand All @@ -506,7 +516,30 @@ function QueryType(props: Props) {
setFieldValue,
values: { max_batch_size, min_batch_size },
} = props;
const timeInterval = typeof max_batch_size !== "number";
const { aggregators } = useLoaderData() as {
aggregators: Promise<Aggregator[]>;
};

const { leader_aggregator_id, helper_aggregator_id } = props.values;
const [aggregatorsResolved, setAggregatorsResolved] = React.useState<
Aggregator[]
>([]);
React.useEffect(() => {
aggregators.then((a) => setAggregatorsResolved(a));
}, [setAggregatorsResolved, aggregators]);
const leader = leader_aggregator_id
? aggregatorsResolved.find(({ id }) => id === leader_aggregator_id) || null
: null;
const helper = helper_aggregator_id
? aggregatorsResolved.find(({ id }) => id === helper_aggregator_id) || null
: null;
const queryTypes =
leader && helper
? leader.query_types.filter((qt) => helper.query_types.includes(qt))
: ["TimeInterval", "FixedSize"];

const timeInterval =
queryTypes.includes("TimeInterval") && typeof max_batch_size !== "number";

const checkboxChange = React.useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -536,6 +569,7 @@ function QueryType(props: Props) {
checked={timeInterval}
onChange={checkboxChange}
label="Time Interval"
disabled={!queryTypes.includes("TimeInterval")}
value="time"
/>
<FormCheck
Expand All @@ -545,6 +579,7 @@ function QueryType(props: Props) {
checked={!timeInterval}
onChange={checkboxChange}
label="Fixed Size"
disabled={!queryTypes.includes("FixedSize")}
value="fixed"
/>
<MaxBatchSize {...props} />
Expand Down Expand Up @@ -745,6 +780,32 @@ function TimePrecisionSeconds(props: Props) {
}

function VdafType(props: Props) {
const { aggregators } = useLoaderData() as {
aggregators: Promise<Aggregator[]>;
};

const { leader_aggregator_id, helper_aggregator_id } = props.values;
const [aggregatorsResolved, setAggregatorsResolved] = React.useState<
Aggregator[]
>([]);
React.useEffect(() => {
aggregators.then((a) => setAggregatorsResolved(a));
}, [setAggregatorsResolved, aggregators]);
const leader = leader_aggregator_id
? aggregatorsResolved.find(({ id }) => id === leader_aggregator_id) || null
: null;
const helper = helper_aggregator_id
? aggregatorsResolved.find(({ id }) => id === helper_aggregator_id) || null
: null;

let vdafs = new Set(
leader && helper
? leader.vdafs
.filter((vdaf) => helper.vdafs.includes(vdaf))
.map((vdaf) => vdaf.replace(/^Prio3/, "").toLowerCase())
: ["sum", "histogram", "count"]
);

return (
<TaskFormGroup controlId="vdaf.type">
<ShortHelpAndLabel
Expand All @@ -759,9 +820,11 @@ function VdafType(props: Props) {
isInvalid={typeof props.errors.vdaf === "string"}
>
<option></option>
<option value="sum">sum</option>
<option value="histogram">histogram</option>
<option value="count">count</option>
{["sum", "histogram", "count"].map((vdaf) => (
<option key={vdaf} value={vdaf} disabled={!vdafs.has(vdaf)}>
{vdaf}
</option>
))}
</FormSelect>
<FormControl.Feedback type="invalid">
{typeof props.errors.vdaf === "string" ? props.errors.vdaf : null}
Expand Down
11 changes: 10 additions & 1 deletion src/clients/aggregator_client/api_types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
entity::{
aggregator::{QueryTypeNameSet, Role as AggregatorRole, VdafNameSet},
aggregator::{QueryTypeName, QueryTypeNameSet, Role as AggregatorRole, VdafNameSet},
task::vdaf::{CountVec, Histogram, Sum, SumVec, Vdaf},
Aggregator, ProvisionableTask,
},
Expand Down Expand Up @@ -73,6 +73,15 @@ pub enum QueryType {
FixedSize { max_batch_size: u64 },
}

impl QueryType {
pub fn name(&self) -> QueryTypeName {
match self {
QueryType::TimeInterval => QueryTypeName::TimeInterval,
QueryType::FixedSize { .. } => QueryTypeName::FixedSize,
}
}
}

impl From<QueryType> for Option<i64> {
fn from(value: QueryType) -> Self {
Option::<u64>::from(value).map(|u| u.try_into().unwrap())
Expand Down
4 changes: 4 additions & 0 deletions src/entity/aggregator/query_type_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,8 @@ impl QueryTypeNameSet {
pub fn intersect(&self, other: &QueryTypeNameSet) -> QueryTypeNameSet {
self.0.intersection(&other.0).cloned().collect()
}

pub fn contains(&self, name: &QueryTypeName) -> bool {
self.0.contains(name)
}
}
4 changes: 4 additions & 0 deletions src/entity/aggregator/vdaf_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,8 @@ impl VdafNameSet {
pub fn intersect(&self, other: &VdafNameSet) -> VdafNameSet {
self.0.intersection(&other.0).cloned().collect()
}

pub fn contains(&self, name: &VdafName) -> bool {
self.0.contains(name)
}
}
42 changes: 40 additions & 2 deletions src/entity/task/new_task.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;
use crate::{
clients::aggregator_client::api_types::{Decode, HpkeConfig},
clients::aggregator_client::api_types::{Decode, HpkeConfig, QueryType},
entity::{aggregator::Role, Account, Aggregator, Aggregators},
handler::Error,
};
Expand All @@ -11,7 +11,7 @@ use base64::{
use rand::Rng;
use sha2::{Digest, Sha256};
use std::io::Cursor;
use validator::ValidationErrors;
use validator::{ValidationErrors, ValidationErrorsKind};

fn in_the_future(time: &OffsetDateTime) -> Result<(), ValidationError> {
if time < &OffsetDateTime::now_utc() {
Expand Down Expand Up @@ -184,6 +184,40 @@ impl NewTask {
}
}

fn validate_vdaf_is_supported(
&self,
leader: &Aggregator,
helper: &Aggregator,
errors: &mut ValidationErrors,
) {
let Some(vdaf) = self.vdaf.as_ref() else {
return;
};
let name = vdaf.name();
if leader.vdafs.contains(&name) && helper.vdafs.contains(&name) {
return;
}
if let ValidationErrorsKind::Struct(errors) = errors
.errors_mut()
.entry("vdaf")
.or_insert_with(|| ValidationErrorsKind::Struct(Box::new(ValidationErrors::new())))
{
errors.add("type", ValidationError::new("not-supported"));
}
}

fn validate_query_type_is_supported(
&self,
leader: &Aggregator,
helper: &Aggregator,
errors: &mut ValidationErrors,
) {
let name = QueryType::from(self.max_batch_size).name();
if !leader.query_types.contains(&name) || !helper.query_types.contains(&name) {
errors.add("max_batch_size", ValidationError::new("not-supported"));
}
}

pub async fn validate(
&self,
account: Account,
Expand All @@ -193,6 +227,10 @@ impl NewTask {
self.validate_min_lte_max(&mut errors);
let hpke_config = self.validate_hpke_config(&mut errors);
let aggregators = self.validate_aggregators(&account, db, &mut errors).await;
if let Some((leader, helper)) = aggregators.as_ref() {
self.validate_vdaf_is_supported(leader, helper, &mut errors);
self.validate_query_type_is_supported(leader, helper, &mut errors);
}

if errors.is_empty() {
// Unwrap safety: All of these unwraps below have previously
Expand Down
15 changes: 14 additions & 1 deletion src/entity/task/vdaf.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::json_newtype;
use crate::{entity::aggregator::VdafName, json_newtype};
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationError, ValidationErrors};

Expand Down Expand Up @@ -72,6 +72,19 @@ pub enum Vdaf {
Unrecognized,
}

impl Vdaf {
pub fn name(&self) -> VdafName {
match self {
Vdaf::Count => VdafName::Prio3Count,
Vdaf::Histogram(_) => VdafName::Prio3Histogram,
Vdaf::Sum(_) => VdafName::Prio3Sum,
Vdaf::CountVec(_) => VdafName::Prio3Count,
Vdaf::SumVec(_) => VdafName::Prio3SumVec,
Vdaf::Unrecognized => VdafName::Other("unsupported".into()),
}
}
}

json_newtype!(Vdaf);

impl Validate for Vdaf {
Expand Down

0 comments on commit 905e2a5

Please sign in to comment.