Skip to content

Add binary attestation contracts and release build guards - #1262

Closed
prk-Jr wants to merge 167 commits into
mainfrom
spec/1235-release-attestation
Closed

prk-Jr wants to merge 167 commits into
mainfrom
spec/1235-release-attestation

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Publishers and vendors need verifiable release bytes, and operators need to distinguish those bytes from ordinary source builds. This PR contains the binary-attestation contract, implementation plans, release build guards, JS composition capture, and the first private Rust producer utilities. It is an implementation PR with explicitly pending release gates.

  • Select Fastly default WASM and three native ts executables, with provenance, artifact-specific SPDX, immutable membership, mandatory Fastly/shared-JS byte equality, and bounded native rebuild outcomes.
  • Enforce full source identity and checked prebuilt JS in release mode, expose owning package/source/target/variant facts, and preserve the existing development prebuilt flow.
  • Replace the introduced Python tooling with a host-only Rust foundation, add native CI coverage, and reconcile the specification and plans with review 5514253951.

The producer bootstrap remains closed. Source/preflight, authenticated acquisition, actual Rust preparation/boundaries/recipes, hosted qualification, signing/publication, and the consumer verifier are not complete. This checkpoint enables no official release or deployment and does not close #1235.

Changes

Files / surface Result
docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md, feasibility audit, migration addendum and three implementation plans Define the cohort, evidence graph, trust inputs, reproducibility requirements and completion boundaries; distinguish historical prototypes and pending proofs; remove execution journals and private paths.
crates/trusted-server-release/**, workspace manifest/lockfile Add private bounded strict-JSON utilities, held-file reads, raw-byte hashing, atomic output and offline schema evaluation; retain exact SPDX schema/license bytes and committed positive/negative fixtures. No core/CLI/JS dependency or fifth canonical asset.
Core build source helper and build_info.rs; CLI/Fastly build scripts and owning readers Require full source commit in release mode; derive target from Cargo and variant from owning features; report labeled CLI facts and one existing Fastly startup diagnostic; compile out the release CLI checkout fallback.
JS build.rs, inventory/copy/bundle helpers and native fixtures Require checked inventory for release; preserve legacy development copying without inventory; reject leaf-link bypasses, mutations and unsafe opaque integers; use actual behavioral tests and specific stderr assertions.
JS build/composition/Vitest pipeline and scripts/release/collect-js.mjs Record conservative contribution/material evidence without changing bundle bytes; fix portable temporary paths, symlink invocation and environment cleanup; keep the complete release tests mandatory in CI and planned A/B producers.
scripts/release/** Remove introduced Python and validator requirements; retain Node/POSIX adapters, pending qualification data, reviewed seccomp/license bytes and an inert hosted proposal outside registered workflows.
.github/workflows/test.yml, format.yml; crate READMEs and operator guides Add Linux/macOS native utility/prebuilt tests with nonzero-selection guards and lint/docs checks, full JS fixture selection, external .mjs formatting, and current build-fact/control documentation.
CLI domain lint Recognize exact standard schema/RDF identifiers with strict token boundaries; retain domain enforcement for lookalikes and continuations.

The governing index-v1 schema, cohort and consumer-policy bytes were preserved during this review correction. Structural schema acceptance does not authorize staging or another artifact; the selected cohort and planned semantic policy enforcement remain narrower.

The initial deployment choice is the validated manual Fastly production route (AC-9). Integrated EdgeZero context checks, staging and recovery are the later AC-8 milestone. Publisher/configuration authority and actual remote execution remain separate #161 claims.

Linked work

Refs #1235, #1213, #1214, #1292 and #161. The packaging and EdgeZero plans describe later reviewed milestones; they are not implemented by this checkpoint.

Test plan

Local platform: macOS 26.6.2 ARM64, Rust 1.95.0 and Node 24.12.0. These results do not qualify macOS 15, native Intel, the GNU compatibility floor, signing/publication or provider submission.

  • Full assigned Rust gates: 3,690 passed, zero failed, 13 existing ignored; Fastly default/reuse, Axum, Cloudflare, Spin, parity, native build-digest and release utility tests.
  • All adapter/CLI/codegen lint gates, WASM clock lint, Rust formatting, release-tool documentation and the Fastly release-profile development build.
  • Full native CLI suite: 896 passed, 18 existing ignored; actual Cargo release-control CLI facts/resource tests: four plus three passed.
  • Native JS prebuilt tests: 37 passed and one guarded ignored fixture; including that fixture explicitly: 38 passed. Deliberately raising the parser size limit makes its fixed-contract regression fail.
  • Full JS opt-in suite: 51 files, 1,261 passed, no type errors; JS build, lint and formatting. Guard-removal mutations fail their intended regression assertions.
  • Docs format/lint/VitePress build, expanded external Markdown/.mjs formatting, workflow actionlint, whitespace checks, staged and committed PR domain lint.
  • Independent automated spec and quality reviews for the Rust utility, Node, Rust review-fix and docs/CI scopes. These are not maintainer approval or release qualification.
  • Actual supported-host qualification, authenticated bootstrap/acquisition, whole Rust producer recipes and real-artifact SPDX coverage.
  • Same-run artifact transfer, project signing/immutable publication rehearsal, consumer evidence verification and authorized live Fastly submission.

Checklist

  • Preserve the existing branch and PR; introduce no Python release code.
  • Add behavioral tests and retain fail-closed production gates.
  • Use existing runtime logging; stdout directives remain limited to Cargo build scripts.
  • Commit no operator configuration, secrets or credentials.
  • Complete all selected release proofs before official enablement.

@prk-Jr prk-Jr self-assigned this Oct 9, 2026
@prk-Jr
prk-Jr requested review from aram356 and jevansnyc October 9, 2026 07:54

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This spec answers #1235 with attested release artifacts, verified deployment and byte-for-byte reproducible builds, then defines Phase 2 contracts for Cloudflare, Spin and Axum. Its trust boundaries are careful: build provenance, release membership, publisher identity and runtime claims are kept distinct, and the no-configuration rule is stated well. The main problems are scope and fit. It makes reproducibility of every artifact an all-or-nothing release gate and ties #1235's closure to Phase 2. It designs past two in-flight PRs (#1214 and #1213) that already change the same mechanisms. It doesn't account for how releases are branched or what the gh verifier needs. Several tool facts (Fastly packing and deploy, --signer-workflow matching, the Spin ABI) also need correcting.

The architecture I recommend preserves four small ownership boundaries:

flowchart TD
    A[Controlled build and composition evidence] --> B[Signing and immutable publication]
    B --> C[Trusted Server verifier library]
    P[Trusted local policy and source approval] --> C
    C --> D[Opaque VerifiedArtifact with retained bytes]
    D --> E[EdgeZero prepared payload and resolved context]
    E --> F[Observed operation state and recovery record]
Loading

Internally, the verifier should separately establish build evidence, official publication and consumer/action authorization before constructing VerifiedArtifact. EdgeZero should preserve payload integrity and own concrete lifecycle operations. It should not implement IAB release policy.

6 of the inline comments below carry a one-click GitHub suggestion: use Commit suggestion (or Add suggestion to batch for several at once) to apply them. The rest describe the change in prose because it spans several sections or lines outside a single contiguous range.

Blocking

🔧 wrench

  • One reproducibility gate makes every artifact wait for the hardest one: see inline at line 63
  • Strict JS mode should build on #1214, not beside it: see inline at line 265
  • The verifier's gh prerequisites and trust chain aren't specified: see inline at line 357
  • Build facts should be derived where possible, follow the env naming convention, and reconcile with #1213: see inline at line 559
  • Rollout steps 2 and 3 are in an order that can't be executed: see inline at line 787
  • Phase 2 in the acceptance criteria keeps #1235 open until Spin migrates: see inline at line 835

❓ question

  • Which branch are release tags cut from?: see inline at line 320

Non-blocking

♻️ refactor

  • Put the release-index v1 schema in the spec: see inline at line 255
  • Build tsjs once per release and require identical embedded JS across artifacts: see inline at line 278
  • Cargo's own output already separates host and target units; the wrapper and Syft add a component without adding coverage: see inline at line 294
  • Drop RELEASE_SETTINGS_READ_TOKEN and check the published release instead: see inline at line 347
  • --signer-workflow is a prefix match; pin the exact certificate identity: see inline at line 380
  • The pack output doesn't match "exactly one bin/main.wasm member" (suggestion): see inline at lines 528-529
  • State what compute deploy does on the native route (suggestion): see inline at line 553

🤔 thinking

  • reusable-sandbox is runtime-gated, so it may belong in the canonical artifact: see inline at line 61
  • The A/B gate measures nondeterminism, not environment sensitivity: see inline at line 208
  • Separate assurance dimensions from release-cohort selection: see inline at line 208
  • The CLI platform baselines exclude common operator hosts: see inline at line 234
  • Define SBOM coverage before settling the recorder mechanism: see inline at line 294
  • A local reusable workflow ties the signer identity to the source commit: see inline at line 326
  • Make the evidence graph and verifier policy normative: see inline at line 357
  • The release record gives the tag object for annotated tags, not the commit: see inline at line 361
  • The publication record carries the same trust as pinning the artifact digest: see inline at line 473
  • The EdgeZero seam is new surface, not an addition to an existing typed one: see inline at line 514
  • Validate observed Fastly bindings, not only manifest declarations: see inline at line 514
  • Recovery needs retained state and explicit ownership: see inline at line 537
  • Specify Cloudflare's actual configuration handoff: see inline at line 650
  • Spin's locked SDK targets a WASI 0.3 release candidate, not plain WASIp2: see inline at line 658

🏕 camp site

  • Bound verification processes and evidence (suggestion): see inline at line 412
  • The "configuration baked into the wasm" claims go beyond CI comments (suggestion): see inline at line 788

⛏ nitpick

  • The scaffold is the repository-root file (suggestion): see inline at line 163
  • artifact-metadata: write isn't needed for file attestations (suggestion): see inline at line 335

Cross-cutting / body-level findings

  • ♻️ Add an explicit threat model. §1 lists each claim and what it doesn't establish, but there is no adversary table. Add one row per adversary (asset or mirror tampering, a malicious evidence-transfer path, a compromised dependency or build script, a rogue tag pusher, a compromised runner image, a compromised operator host, GitHub itself) with its capability, the check that defends against it, and what remains. Each check in the spec can then cite the threat it defends against, which makes it easier to see which checks earn their cost, for example the settings token at line 347 and the publication record at line 473.
  • ♻️ Write it as a contract. The spec is about 16,000 words with no BCP 14 keywords (42 lowercase "must", no MUST or SHOULD), and paragraphs such as lines 176, 202 and 208 each carry six to ten requirements without IDs. The test plan and acceptance criteria paraphrase each other without traceability. Suggest requirement IDs with BCP 14 keywords, one table mapping requirement → test → acceptance item → #1235 "Done when", and rationale moved to an appendix. Also give the "release manifest" (line 60) and "release-tools manifest" (lines 288 and 412) names and paths; "manifest" already means edgezero.toml and fastly.toml elsewhere in the spec.
  • 🌱 Cloudflare notes for the follow-up spec (§12, lines 642-654):
    • worker-build 0.8.5 downloads the wasm-bindgen CLI (0.2.126), binaryen version_130 and @esbuild/linux-x64 0.28.1 (separate from the JS lockfile's esbuild 0.27.3) with no checksum check. A network-denied recipe needs them pre-seeded with digest checks, through WASM_BINDGEN_BIN, WASM_OPT_BIN and ESBUILD_BIN or its cache layout.
    • Output-changing switches to control include CUSTOM_SHIM, RUN_TO_COMPLETION, COREDUMP/COREDUMP_FLAGS, NO_MINIFY, --no-opt and --panic-unwind. worker-build also adds an sdk workers-rs entry to the wasm producers section.
    • build.sh installs worker-build without --locked and derives its version from Cargo.lock at run time.
    • The .tar.zst canonical archive brings back the archive/member identity question that line 242 avoids for Phase 1. Publishing the members as raw assets plus an attested modules.json inventory keeps every subject a raw file.
    • The versions API silently drops unresolved inherit bindings unless bindings_inherit=strict is set, and inherit/keep_bindings resolve against the latest uploaded version, which may not be the deployed one. Line 650's "fail when unresolved" needs bindings_inherit=strict and an explicit version_id.
  • 🏕 Mark the January spec as superseded. docs/superpowers/specs/2026-01-15-attestation-design.md still says **Status:** Proposal and doesn't point here. #1235 says this spec replaces its binary-attestation sections, so a one-line "superseded in part by" banner in that file would keep readers from following it.

CI Status

  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (python): PASS
  • Analyze (rust): PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • CLAUDE.md symlink guard: PASS
  • CodeQL: PASS
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

@prk-Jr prk-Jr changed the title Design release attestation and reproducible builds for all adapters Define attested Fastly and CLI release contracts Oct 12, 2026
@prk-Jr
prk-Jr requested a review from aram356 October 12, 2026 06:25
Resolve the lockfile while preserving release dependencies and main's upgrades. Adapt the release tool to HMAC 0.13 and Rust 1.98 lint requirements without changing candidate guards or qualification policy.
Add a pure collection::workflow_jobs adapter that turns one strict, bounded
GitHub Actions "jobs for a run attempt" REST body, the upload step outputs and
the collector's own wait clock into the existing Upload and OptionalBuild
inputs. Expected job names, assets, roles and recipes come only from an
explicit ExpectedTopology that must name every fixed canonical and
reproduction job exactly once.

The adapter derives started from executed steps rather than started_at,
maps success/failure/cancelled/skipped/timed_out and rejects every other
job or step conclusion, accepts a conclusion-less job only at the deadline,
binds each expected name to exactly one job of the exact run, attempt and
head, requires mandatory jobs to start, succeed and upload, and passes
upload identity through unchanged. The trusted dependency constructor stays
closed; no HTTP, workflow or command wiring is added.
An optional native reproduction whose job did not succeed now has any
upload output dropped unused and is recorded as NotUploaded with its
derived outcome, so it can neither block collection nor lend its upload
identity. Mandatory behaviour is unchanged.

Document and test the single-fresh-attempt rule: run_attempt is required
on every listed job and a carried-over job from an older attempt rejects
the response. Document that the fetch must request per_page=100 and test
a truncated page. Identify the mandatory reproduction by its Wasm target
instead of its fixed-artifact position. Cover waiting and requested
mandatory jobs and completed jobs without a conclusion, and split the
fixtures and transfer integration out of the adapter test module.
prk-Jr added a commit that referenced this pull request Oct 26, 2026
Port the five remaining slice 2.3 comment/documentation paths from #1262 at 122b786. Runtime configuration and release-source facts already landed in Phase 1; no executable behavior changes.
prk-Jr added a commit that referenced this pull request Oct 26, 2026
Reuse the reviewed ZIP32 primitive and negative fixtures from #1262 at 122b786 without moving collection authority into Phase 2. Enforce exact build/observation member sets and source-owned bounds before the original full integrity checks.

Verify red-green profile admission, 71 audited pure tests, strict clippy and an exact-ID read-only probe of the current Fastly archive against its recorded GitHub digest. Independent/security review approves this structural unit; transfer authentication remains pending.
@aram356 aram356 added this to the 202611 milestone Oct 29, 2026
@aram356
aram356 marked this pull request as ready for review October 29, 2026 15:56
@aram356
aram356 marked this pull request as draft October 29, 2026 15:56
@aram356

aram356 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Closing PR since split into phases See #1435 and #1449

@aram356 aram356 closed this Oct 29, 2026
prk-Jr added a commit that referenced this pull request Nov 2, 2026
- Move the release publisher into scripts/release/publish.mjs with a
  release-notes template; tests import it instead of scraping YAML.
- Accept release-attestation records that carry only bundle_url, verify
  them through gh release verify output, and retry the list read and
  failed proof fetches within the availability checks.
- Install a checksummed GitHub CLI 2.102.0 and pinned Node for the
  publisher and checks job; refuse non-SemVer tags before building.
- Fix the signing-input symlink guard and bundle check, which set -e
  ignored inside && lists.
- Run release builds on main pushes and tags only; run the Node release
  suite on pull requests in format.yml. Reference actions by major tag.
- Leave REPO, TAG and COMMIT blank in release notes, with a guard that
  also works in bash 3.2.
- Extend the source audit to byte/C raw strings, astral chars and build
  helpers; drop the test-only CLI cfg and duplicate release validation.
- Remove carried #1262 data and docs (frozen at 122b786), frame Axum
  binaries as dev/evaluation builds, map Phase 1 spec scope by R-ID,
  and correct the guide, plan, compatibility record and changelog.
prk-Jr added a commit that referenced this pull request Nov 11, 2026
* Phase the reviewed binary attestation contract

* Map binary attestation phases to review boundaries

* Build and attest phased release binaries

* Document binary verification and release deployment

* Align Phase 1 signing and publication with the reviewed contract

* Allow the exact GitHub Actions build type reference host

* Document Phase 1 compatibility and verification boundaries

* Avoid stale API connections during release verification

* Clarify rehearsed release status and CLI verification

* Retry transient post-publication verification reads

* Document offline verification and completed release rehearsal

* Add production Axum assets and release startup checks

* Disable rustup self-updates in temporary build homes

* Expose pinned compiler binaries in the GNU builder

* Supply runtime trust data to Axum startup checks

* Record the seven-binary release rehearsal

* Derive immutable release build facts

* Allow runtime Axum bind address overrides

* Harden release eligibility and artifact validation

* Align release documentation and reference-host policy

* Reduce startup logging and align runtime port aliases

* Harden release verification and preflight boundaries

* Record rehearsal failure and release recovery checks

* Record the successful binary attestation rehearsal

* Handle attestation availability and verify published bytes

* Record the successful ninth attestation rehearsal

* Clarify final release verification notes (docs-only)

* Address self-review findings in release attestations

- Print Axum bind-address refusals to stderr so RUST_LOG=off
  cannot hide them; add a binary-level regression test
- Log and ignore other tags' release attestations instead of
  verifying them after publication
- Time-bound every release API read; mutations stay single-shot
- Run the CLI build-facts test in release mode in CI and compare
  the embedded source with the exact supplied commit
- Assert the v* tag gate and action pinning in both workflows
- Define COMMIT in the maintainer checklist; describe both
  attestation subjects accurately
- Make the Fastly BUILD_INFO constant private

* Strengthen release build-fact and gate tests

- Supply a fictional commit to the release-mode CLI test so a
  fallback to local git cannot pass, and assert release mode
  matches the supplied control
- Anchor the publication gate assertion to the job-level if
- Document the read timeout and job-bounded mutations

* Record the main-merge Phase 1 rehearsal

Docs-only since rehearsed 68eef90 (v0.0.0-rehearsal.15).

* Record two independent Phase 1 spec review rounds

Docs-only since rehearsed 68eef90. Append actual fresh review outcomes and clarify historical adapter investigation inputs.

* Address maintainer review of Phase 1 attested releases

- Move the release publisher into scripts/release/publish.mjs with a
  release-notes template; tests import it instead of scraping YAML.
- Accept release-attestation records that carry only bundle_url, verify
  them through gh release verify output, and retry the list read and
  failed proof fetches within the availability checks.
- Install a checksummed GitHub CLI 2.102.0 and pinned Node for the
  publisher and checks job; refuse non-SemVer tags before building.
- Fix the signing-input symlink guard and bundle check, which set -e
  ignored inside && lists.
- Run release builds on main pushes and tags only; run the Node release
  suite on pull requests in format.yml. Reference actions by major tag.
- Leave REPO, TAG and COMMIT blank in release notes, with a guard that
  also works in bash 3.2.
- Extend the source audit to byte/C raw strings, astral chars and build
  helpers; drop the test-only CLI cfg and duplicate release validation.
- Remove carried #1262 data and docs (frozen at 122b786), frame Axum
  binaries as dev/evaluation builds, map Phase 1 spec scope by R-ID,
  and correct the guide, plan, compatibility record and changelog.

* Record Phase 1 fork rehearsal .22 at the current head

* Bound release-attestation availability checks by their deadline

The availability loop checked its 60-second deadline only at the start of
each round, while the attestation list read and `gh release verify` proof
fetch retried under the 180-second post-publish window. A round could
therefore retry and accept a proof after the advertised deadline. Both now
use a retrier and command timeouts bound to the availability deadline.

Also correct the rehearsal attribution: fork rehearsal .22 covers source
5f0139b only, not the later main merge or this publisher change, and
distinguish Axum's Ctrl-C request draining from missing SIGTERM handling.

* Address second maintainer review of Phase 1 attested releases

Find build-script helpers that a non-mod.rs file loads from its own
directory, refuse custom package.build scripts and #[path] modules
outside the audited sources, and pin the producer and publisher checks
the review showed could be deleted without a failing test, including the
availability deadline bounds on list reads and proof fetches.

Keep the cause of a retry the availability deadline cut short and name
that window in the error. Refuse annotated tags before building, upload
build artifacts only for tags, run the release suite in its own job and
strip debug info from native release binaries.

Correct the spec, plan, record and guide where they described CLI and
Fastly build-script validation, tagging and retries that do not hold.

* Address third maintainer review of Phase 1 attested releases

Narrow Phase 1 to the Fastly wasm and the three ts CLI binaries
(17 release assets). Drop the Axum assets, the Rust source audit, the
post-publish re-verification, the descriptive cohort file and the
Fastly build-facts line. Replace the destination spec, plan, slice map
and compatibility record with a Phase 1 spec that links the frozen
design, and trim the operator guide to one signer check per predicate.

The publisher now verifies every signing bundle before creating a
draft, logs the release ID before checking it, and keeps the SBOM root
at Cargo's package version. Build-script guards exit explicitly under
bash 3.2, every member's phase1.json is refused and cleaned, and the
Linux format check requires an x86-64 ELF.

Tests pin the create and publish bodies, the producer step order and
container passthrough, the workflow permissions and gates, the
post-publish deadlines and the checksum format. CI now checks that a
release build refuses a branch source.

* Raise the bundle check output limit and pin its real output size

Rehearsal v0.0.0-rehearsal.24 failed before creating anything: the
SBOM verification result embeds the whole SPDX document, which exceeds
Node's default 1 MiB execFileSync buffer, so gh was killed with
ENOBUFS. Allow up to 256 MiB, and add a publisher mode that sends a
2 MiB result through a real child process with the publisher's options.

Also test the CLI version smoke check, the build checks' annotated-tag
comparison and the publisher step's pinned-Node assertion, which could
previously be removed without a failing test.

* Test the remaining publisher and SPDX guards

Cover three checks that could be removed without a failing test: API
calls closing their connection after a gh command leaves pooled sockets
idle, refusing a published asset whose name is not in the expected set,
and refusing an SPDX package that stands in for the described root.
Reword the bash 3.2 comment to state only what the guard needs.

* Refuse release tags on commits that are on no branch

In fork rehearsals, draft creation returned HTTP 403 when the tagged
commit was reachable only through its tag, after every build and
signing job had run and written public attestations. The checks job now
fetches full history and refuses such a tag before anything is built or
signed. Which branches may be released stays a maintainer decision.

* Address fourth maintainer review of Phase 1 attested releases

Fix the operator guide's recovery rules and manual Fastly deploy store
links. Replace the publisher's post-publish availability machinery with
one bounded poll over the release read and gh release verify. Inline
the CLI build facts, drop the unused CycloneDX dependency merge and
redundant SPDX re-checks, and pin the producer, workflow and publisher
behaviours the review found untested.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish attested release binaries with build provenance and SBOMs

3 participants