scripts/janus_smoke.sh runs a Prio3Count aggregation across two independent
DAP implementations: the Janus interop
containers play Client, Leader, and Collector, and dap-go plays the Helper. The
only cross-implementation boundary is Leader to Helper, which is what the smoke
exercises.
This document records what has actually been observed on two dates, and how to reproduce either run.
20 June 2026, Janus c1531764: complete, aggregate 3. The whole path
worked end to end, including the aggregate share. At that commit Janus
advertised DAP_VERSION_IDENTIFIER = "dap-18" while implementing a wire format
that differed from the published draft-18 in five places, and dap-go spoke to it
through wire.VariantJanus.
28 August 2026, Janus d5d523d: aggregation works, collection does not. All
four reports decrypt, verify, and are accepted by the Leader. The run then stops
at the aggregate-share step. Details below.
The version identifier was "dap-18" on both dates. What changed underneath it
was the entire message layer.
dap-go has since added draft-19 and draft-20 as further variants, but the smoke
still runs draft-18: Janus advertises dap-18, and the version tag is baked into
every domain-separation string, so a draft-19 or draft-20 Helper and a draft-18
Leader cannot open each other's input shares at all. Interop on the newer drafts
waits on a peer that speaks them.
Janus has converged on the published draft-18 messages: the aggregation-job
initialization request carries verification_key_id and aggregation-job
extensions in place of the partial batch selector, and the input-share AAD is the
four-field form including the task configuration.
It has not converged on the resource model. Aggregation jobs are still created
with PUT to a Leader-chosen URL, where the published draft uses POST to the
collection URL with a server-selected id. So Janus is a hybrid: draft-18
messages over the older HTTP model.
The practical consequence for an implementer: you cannot infer the message format from the HTTP method. dap-go used to pick the AAD variant from the method, which broke here; it now tries both variants and keeps whichever the sender actually sealed under.
The Leader requests the aggregate share and dap-go rejects it with 400. Draft-18 restructured that exchange:
| Message | Janus today | dap-go |
|---|---|---|
AggregateShareReq |
{collection_job_req, batch_selector, report_count, checksum} |
{batch_selector, agg_param, report_count, checksum} |
AggregateShareAad |
{task_id, task_configuration, collection_job_req} |
{task_id, agg_param, batch_selector} |
BatchSelector |
{batch_identifier} |
{batch_mode, identifier} |
Closing this means implementing CollectionJobReq and the reworked
aggregate-share messages. That is collection-path work, which this project
deliberately has not built (see the status table in the
README), so the August run is expected to stop there. It is a
scope boundary, not a defect.
Draft-18 binds the task configuration into the input-share AAD, so every party
has to construct identical bytes. The interop test API has no task_info field,
so every Janus interop binary uses the fixed placeholder task-info, and
anything else makes every HPKE open fail with a decryption error that looks like
a key problem but is not. dap-go uses the same placeholder in
cmd/dap-helper.
The same applies to the endpoints, the time precision, the minimum batch size, and the batch configuration: the collector needs them too, which is why it is registered with the helper endpoint even though it never talks to the Helper.
docker,jq,python3- Go toolchain (1.26+) to build the Helper
- A divviup/janus checkout
Building the Janus images needs roughly 8 GiB available to Docker. With less,
rustc is killed while compiling janus_aggregator and the bake fails with an
allocation error.
# 1. Build the Janus interop images. Use the default (release) cargo profile:
# PROFILE=dev writes to target/debug while the Dockerfile copies from
# target/$PROFILE, which breaks the bake.
cd /path/to/janus
git checkout c1531764d2fc0a05c64b722117004fd516e33a43 # for the complete June run
docker buildx bake janus_interop_aggregator janus_interop_client janus_interop_collector --load
# 2. Run the smoke from this repository.
cd /path/to/dap-go
scripts/janus_smoke.shOmit the git checkout to run against current Janus, which reaches aggregation
and stops at the aggregate share as described above.
Ports are overridable when something else already holds them:
HELPER_PORT=18080 LEADER_PORT=19001 CLIENT_PORT=19002 COLLECTOR_PORT=19003 \
scripts/janus_smoke.shOn failure the script dumps the Leader and Collector logs, and the Helper logs every request with its response code.
One Prio3Count task, one aggregation job, a single batch, and the Helper role. Not covered: the Leader role, the general collection path, the other Prio3 instances, multi-round VDAFs, and runs against other implementations. It is a smoke against one peer, not a conformance suite.
Against Janus at c1531764, these differed from the published
draft-ietf-ppm-dap-18. dap-go encodes both through wire.Variant; the caller
pins the variant, since nothing on the wire announces it.
| Structure | Janus c1531764 |
Published draft-18 |
|---|---|---|
AggregationJobInitReq |
{agg_param, partial_batch_selector, verify_inits} |
{verification_key_id, agg_param, extensions, verify_inits} |
verification_key_id |
absent | uint8, first field |
PartialBatchSelector |
present | replaced by aggregation-job extensions |
InputShareAad |
{task_id, metadata, public_share} |
{task_id, task_configuration, metadata, public_share} |
verify_inits / verify_resps |
uint32 byte-length prefix |
implicit-length remainder |
| Aggregation-job init | PUT to a Leader-chosen resource URL |
POST to the collection URL |
Janus has since adopted every row except the last. Golden-byte tests pin both
encodings and assert the difference stays confined to the documented header and
length-prefix positions: see pkg/dap/wire/golden_test.go.
Identical across both and exercised live: the vdaf-18 Prio3Count crypto, the
"dap-18 input share" and "dap-18 aggregate share" HPKE info strings, the
"dap-18" || task_id VDAF context, ReportShare, VerifyInit, VerifyResp,
and HpkeConfig / HpkeConfigList.
The harness API is not stable between Janus versions. As of d5d523d:
add_taskrequirestask_startandtask_end. They are optional values but the keys must be present, and Janus rejects a task where one is set and the other is not.uploadrequiresbatch_modeandmin_batch_size.- The collector's
add_taskrequires the helper endpoint andmin_batch_size. - A Prio3 measurement is a JSON string (
"1", not1).