Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate vdaf and query type are supported by both aggregators #359

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading