Skip to content

fix: chunk the registration to the registry per-entry content-CID cap - #946

Merged
FSM1 merged 4 commits into
mainfrom
feat/920-chunk-register-content-cids
Aug 2, 2026
Merged

fix: chunk the registration to the registry per-entry content-CID cap#946
FSM1 merged 4 commits into
mainfrom
feat/920-chunk-register-content-cids

Conversation

@FSM1

@FSM1 FSM1 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Problem

POST /registry/register caps contentCids at 1000 per entry and refuses a larger array fail-closed. The engine's register caller was unbounded: PublishRequest::registration() built a single entry carrying the version root plus every leaf. At the production framing any file past ~1 GiB frames to more than 1000 leaves, so its registration was refused — and the refusal did not dead-letter. A register 400 became Halt::Unclassified, which is charged nothing and retried every tick, so the op held the strict-FIFO queue head forever while every pass re-uploaded and re-registered.

Change

Engine. A new crates/engine/src/net/register.rs is the bounded path to the endpoint, the sibling of net::retire. It owns both of the registry's bounds: an entry past the per-entry contentCids cap splits into several entries under the same ipnsName (the head rides the first, continuations omit headCid so the stored head survives), and the batch itself chunks to REGISTRY_BATCH_MAX. publish calls it, so register-first still holds — every chunk lands before the record PUT. The cap constant moved out of retire.rs into net::REGISTRY_BATCH_MAX, shared by both chunkers rather than copied, and is now what the tests and the contract suite assert against instead of literal 1000s.

Failure valve. classify_register in sync/drain.rs dead-letters a refused registration instead of looping at the queue head — but on positive evidence only, the discipline classify_upload already applies to a 413 (#848). The registry's batch gate now stamps code: REGISTRY_BATCH_REFUSED on its own 400s, published in the OpenAPI document; a 400 the gate did not stamp (a proxy, a body cap) is Halt::UploadAttempt and abandons only once the attempt budget runs out. Without that split, any 400 would destroy every queued write on sight — retiring its rows and releasing its staged ciphertext.

API. registry-error-codes.ts mirrors the existing upload-error-codes.ts. The pipe exceptionFactory flattens to constraint strings, so the refusal keeps the uniform error envelope instead of echoing the caller's ipnsName and every contentCid back in the body. An explicit headCid: null is now refused rather than clearing the stored head, since a chunked registration's continuations omit the field and the invariant has no way to express the opposite.

blueprint/api.md "Batch bounds" documents the split shape and the new code.

Tests

Reverting the fix turns each of these red — verified:

  • crates/engine/tests/write_plane.rs::a_version_past_the_registration_cap_registers_in_chunks_and_publishes — 1001 leaves under ContentProfile::CI. The Blocks fake now enforces the server's bounds, so an unchunked registration really is refused: revert the split in net::register and the op dead-letters instead of publishing.
  • ...::a_registration_the_registry_refuses_dead_letters_instead_of_holding_the_queue_head — a stamped 400 dead-letters on pass 1 with PayloadRefused and retires what its chunks charged. Red without classify_register.
  • ...::a_registration_400_from_an_intermediary_is_charged_not_permanent — an unstamped 400 survives to AttemptsExhausted. Red if the classifier branches on the status alone.
  • crates/engine/src/net/register.rs unit tests — empty batch, in-bounds passthrough, over-cap entry split, bare name+head entry, over-cap batch split.
  • crates/contract/tests/contract.rs::an_oversize_register_entry_is_refused_fail_closed — the live leg mirroring an_oversize_retire_batch_is_refused_fail_closed, asserting the code the valve branches on and that the engine's exact chunk shape is accepted.
  • apps/api/src/registry/registry.http.integration.test.ts — three legs against real Postgres: the over-cap refusal stamps the code and does not echo the entry, the two-entry chunk shape keeps the head on one name row, and a null headCid is refused.

Verification

All exit 0 in the worktree:

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo check --workspace --all-targets
  • cargo check -p cipherbox-wasm --target wasm32-unknown-unknown --all-targets
  • cargo test --workspace — no failures; write_plane 48 passed
  • pnpm --filter @cipherbox/api run typecheck, test 177 passed, test:integration 136 passed against the local Postgres
  • pnpm -r --if-present run typecheck, eslint ., markdownlint-cli2 blueprint/api.md, prettier --check
  • pnpm --filter @cipherbox/api openapi:generate — the regenerated openapi.json is committed, so the CI staleness gate stays green

Gates: /security-review and /crypto-privacy-review findings are folded into commits 2 and 3 (the code discriminator, and the error-body flattening plus the null-head refusal). /simplify moved the chunker to net::register and deduplicated the test helpers.

Parallel work

#921 is being implemented concurrently in the same registration/retire area of the engine and also extends #916. This PR moves RETIRE_BATCH_MAX out of net/retire.rs to net::REGISTRY_BATCH_MAX and touches classify_publish, Halt::UploadAttempt's doc, upload_failure, and DeadLetterReason::PayloadRefused's doc in sync/drain.rs / sync/rebase.rs. Whichever merges second should re-check those.

Closes #920

Summary by CodeRabbit

  • New Features

    • Large registry registrations and retirements are automatically split into compliant batches.
    • Entries exceeding content CID limits are chunked while preserving the existing head when omitted.
    • Registry batch-limit failures now return the stable REGISTRY_BATCH_REFUSED code.
  • Bug Fixes

    • Explicitly null head CIDs are rejected.
    • Refused batches no longer expose submitted contents.
    • Permanent registry refusals are distinguished from retryable failures.
  • Documentation

    • Updated registry API specifications and error response documentation.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FSM1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b67b37d2-efae-426c-80a6-45edebaef7e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6842706 and 2d27cbd.

📒 Files selected for processing (17)
  • apps/api/openapi.json
  • apps/api/src/registry/dto/registry.dto.ts
  • apps/api/src/registry/registry-error-codes.ts
  • apps/api/src/registry/registry.controller.ts
  • apps/api/src/registry/registry.http.integration.test.ts
  • apps/api/src/registry/registry.pipes.ts
  • blueprint/api.md
  • crates/contract/tests/contract.rs
  • crates/engine/src/api/error.rs
  • crates/engine/src/api/mod.rs
  • crates/engine/src/net/mod.rs
  • crates/engine/src/net/publish.rs
  • crates/engine/src/net/register.rs
  • crates/engine/src/net/retire.rs
  • crates/engine/src/sync/drain.rs
  • crates/engine/src/sync/rebase.rs
  • crates/engine/tests/write_plane.rs

Walkthrough

The registry API now returns REGISTRY_BATCH_REFUSED for malformed or over-cap batches. The engine chunks oversized registrations, shares the batch limit with retirement, and classifies registry refusals during publish synchronization.

Changes

Registry batch contract and validation

Layer / File(s) Summary
Registry validation and refusal contract
apps/api/openapi.json, apps/api/src/registry/..., blueprint/api.md, crates/contract/tests/contract.rs
The API standardizes 400 responses with REGISTRY_BATCH_REFUSED, rejects explicit null head CIDs, and documents batch limits and chunking behavior. Tests cover refusal, head preservation, and accepted split entries.

Engine registration flow

Layer / File(s) Summary
Registration chunking and shared limits
crates/engine/src/net/..., crates/engine/src/api/...
The engine exports a shared limit, splits oversized contentCids lists, preserves the head on the first entry, sends bounded batches, and routes publishing through the helper. Retirement uses the shared limit.

Publish refusal handling

Layer / File(s) Summary
Registration refusal classification
crates/engine/src/sync/..., crates/engine/tests/write_plane.rs
Stamped registry refusals become PayloadRefused dead letters. Other register 400 responses consume retry attempts. Test fixtures cover both outcomes and successful chunked publication.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PublishPipeline
  participant RegisterHelper
  participant RegistryAPI
  participant SyncDrain
  PublishPipeline->>RegisterHelper: registration entries
  RegisterHelper->>RegisterHelper: split oversized entries and batches
  RegisterHelper->>RegistryAPI: bounded registration requests
  RegistryAPI-->>RegisterHelper: success or registry refusal
  RegisterHelper-->>PublishPipeline: registration result
  PublishPipeline->>SyncDrain: register failure
  SyncDrain-->>PublishPipeline: PayloadRefused or retry classification
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: chunking registry registrations at the per-entry content-CID cap.
Linked Issues check ✅ Passed The changes satisfy #920 by chunking registrations, preserving ordering, classifying confirmed refusals, sharing the batch limit, and adding required tests.
Out of Scope Changes check ✅ Passed The API, documentation, error handling, and test changes directly support the registration chunking and refusal requirements in #920.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/920-chunk-register-content-cids
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/920-chunk-register-content-cids

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@FSM1

FSM1 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Merge-order constraint with #944

Verified by building the pairwise merge: this PR and #944 are each green against main, but their merge does not compile.

This PR deletes pub(crate) const RETIRE_BATCH_MAX from crates/engine/src/net/retire.rs (moved to net/mod.rs as REGISTRY_BATCH_MAX). #944 adds a new consumer of the deleted name in crates/engine/src/sync/drain.rs. Git conflicts only the definition — the use line in drain.rs auto-merges silently, giving E0432: unresolved import after the retire.rs conflict is resolved the obviously-correct way.

There is also a test-fake collision: both branches add refuse_register to the Blocks fake in crates/engine/tests/write_plane.rs with incompatible signatures (bool here vs Vec<u8> there). Not overloadable in Rust; one needs renaming.

Land this PR first, then rebase #944 — that keeps the constant move owned end-to-end here, and limits #944's rebase to its own new code (one import, two usages, one test-helper rename). After a correct hand-resolution the pair is fully green: cargo test --workspace 1151 passed / 0 failed.

Do not merge the two back-to-back on green checkmarks alone.

@FSM1
FSM1 marked this pull request as ready for review August 1, 2026 21:09
@FSM1
FSM1 marked this pull request as draft August 1, 2026 21:14
@FSM1
FSM1 marked this pull request as ready for review August 1, 2026 21:40
@FSM1
FSM1 marked this pull request as draft August 1, 2026 21:48
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 07:19
@FSM1
FSM1 marked this pull request as draft August 2, 2026 07:19
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 08:07
@FSM1
FSM1 marked this pull request as draft August 2, 2026 08:07
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 09:07
@FSM1
FSM1 marked this pull request as draft August 2, 2026 09:07
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 10:08
@FSM1
FSM1 marked this pull request as draft August 2, 2026 10:08
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 11:07
@FSM1
FSM1 marked this pull request as draft August 2, 2026 11:07
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 12:08
@FSM1
FSM1 marked this pull request as draft August 2, 2026 12:08
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 13:28
@FSM1
FSM1 marked this pull request as draft August 2, 2026 13:28
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 14:10
@FSM1
FSM1 marked this pull request as draft August 2, 2026 14:10
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 15:08
@FSM1
FSM1 marked this pull request as draft August 2, 2026 15:08
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 16:07
@FSM1
FSM1 marked this pull request as draft August 2, 2026 16:08
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 17:08
@FSM1
FSM1 marked this pull request as draft August 2, 2026 17:08
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 17:52
@FSM1

FSM1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

@FSM1: I will perform a full review of #946. I will also check the documented integration risk with #944.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/api/src/registry/registry-error-codes.ts (1)

1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shorten the new comments.

Keep the permanent-refusal rationale and the blueprint reference. Remove the Nest implementation detail and issue-history narrative.

As per coding guidelines, keep comments short and explain why rather than what.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/registry/registry-error-codes.ts` around lines 1 - 16, Shorten
the comments above REGISTRY_BATCH_REFUSED and batchRefusedBody: retain the
permanent-refusal rationale, the stable client-facing code purpose, and the
blueprint reference, while removing the Nest exception-object implementation
details and issue-history narrative.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/openapi.json`:
- Line 455: Define a reusable JSON refusal-error schema with required
statusCode, message, error, and code fields, constraining code to
REGISTRY_BATCH_REFUSED. Apply it to both 400 responses in apps/api/openapi.json
at lines 455-455 and 509-509, and update the register and retirement response
decorators in apps/api/src/registry/registry.controller.ts at lines 55-58 and
83-86 so generated OpenAPI includes the same schema.

In `@apps/api/src/registry/registry.http.integration.test.ts`:
- Around line 153-169: Extend the over-cap batch test around the existing
namesFor assertion to verify that no PinnedCid records are created for the
refused request and that response.body does not contain any submitted content
CID, such as a representative value from contentCids. Keep the existing
refusal-code, constraint-message, IPNS-name, and empty NameInventory assertions
unchanged.

In `@apps/api/src/registry/registry.pipes.ts`:
- Line 63: Update the ParseArrayPipe configuration in the registry pipe to
preserve the original DTO ValidationError objects when invoking refuse, rather
than passing its collected string messages. Ensure malformed batches
consistently receive REGISTRY_BATCH_REFUSED with headCid null, and update the
corresponding null-head test to assert response.body.code equals
REGISTRY_BATCH_REFUSED.

---

Nitpick comments:
In `@apps/api/src/registry/registry-error-codes.ts`:
- Around line 1-16: Shorten the comments above REGISTRY_BATCH_REFUSED and
batchRefusedBody: retain the permanent-refusal rationale, the stable
client-facing code purpose, and the blueprint reference, while removing the Nest
exception-object implementation details and issue-history narrative.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 40f452bd-cb16-4d0b-8f51-16566d8fbadd

📥 Commits

Reviewing files that changed from the base of the PR and between afb3887 and 6842706.

📒 Files selected for processing (17)
  • apps/api/openapi.json
  • apps/api/src/registry/dto/registry.dto.ts
  • apps/api/src/registry/registry-error-codes.ts
  • apps/api/src/registry/registry.controller.ts
  • apps/api/src/registry/registry.http.integration.test.ts
  • apps/api/src/registry/registry.pipes.ts
  • blueprint/api.md
  • crates/contract/tests/contract.rs
  • crates/engine/src/api/error.rs
  • crates/engine/src/api/mod.rs
  • crates/engine/src/net/mod.rs
  • crates/engine/src/net/publish.rs
  • crates/engine/src/net/register.rs
  • crates/engine/src/net/retire.rs
  • crates/engine/src/sync/drain.rs
  • crates/engine/src/sync/rebase.rs
  • crates/engine/tests/write_plane.rs

Comment thread apps/api/openapi.json Outdated
Comment thread apps/api/src/registry/registry.http.integration.test.ts
Comment thread apps/api/src/registry/registry.pipes.ts
@FSM1
FSM1 marked this pull request as draft August 2, 2026 18:03
FSM1 added 4 commits August 2, 2026 20:07
…-CID cap

The registry caps contentCids at 1000 per register entry and refuses a larger
array fail-closed. PublishRequest built a single entry carrying the version
root plus every leaf, so at the production framing any file past ~1 GiB was
refused - and a register 400 classified as Halt::Unclassified, which is charged
nothing and retried every tick, so the op held the strict-FIFO queue head
forever while every pass re-uploaded and re-registered.

The registration now splits at the cap into several entries under one ipnsName,
the head riding the first so the name and its pointer land ahead of any
content-only entry; the server collapses them to one name row and a bare
re-register leaves the stored head untouched. Register-first still holds -
every chunk lands before the record PUT.

A register 400 is now classified permanent, so a registration no retry can
satisfy dead-letters instead of looping at the queue head.

The retire chunker's cap constant moves to net::REGISTRY_BATCH_MAX, shared by
both chunkers rather than copied.

Closes #920
The review gates found that treating any 400 as the registry's verdict breaks
the positive-evidence rule classify_upload enforces for a 413: a proxy or a
version-skewed deploy answering 400 would dead-letter every queued write on
sight, retiring its rows and releasing its staged blocks.

The registry's batch gate now stamps code REGISTRY_BATCH_REFUSED on its own
refusals, published in the OpenAPI document, and the valve branches on that
code. A 400 the gate did not stamp is charged like any other pre-PUT refusal
and abandons only once the attempt budget runs out.

The chunker also moves out of PublishRequest into net::register, the sibling
of net::retire, so both of the registry's bounds are enforced on one bounded
path no future caller of the raw client can bypass.
… entry

The crypto/privacy gate found that routing ParseArrayPipe's exceptionFactory
straight into the error body replaced Nest's flattened constraint strings with
the raw ValidationError objects - which carry `target` and `value`, so an
over-cap register 400 echoed the caller's ipnsName and every contentCid back,
twice, into a body that request logging and intermediaries capture.

The factory now flattens to constraint strings, restoring the uniform envelope
the rest of the surface emits.

An explicit `headCid: null` is also refused rather than clearing the stored
head: continuation entries of a chunked registration omit the field, and the
invariant blueprint/api.md now states has no way to express the opposite.
…tract

The 400 responses promised a stable `code` in prose but carried no schema,
so a client had no documented shape to classify on. Both register and retire
now answer `BatchRefusedDto`, and every refusal normalizes its message to a
string list so the documented shape is the only one emitted.

Cover the code on the DTO-validation and target-length paths, and assert a
refused batch leaves no pin rows and echoes no submitted content CID.
@FSM1
FSM1 force-pushed the feat/920-chunk-register-content-cids branch from 6842706 to 2d27cbd Compare August 2, 2026 18:12
@FSM1

FSM1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto `origin/main` (76c9e52, picking up #941/#943/#945) and resolved the CodeRabbit review. Branch is now 2d27cbd.

Rebase. One conflict, in `crates/engine/tests/write_plane.rs` — an import-line collision only: main added `AuthoredHead` to the `net::author` import while this branch added `REGISTRY_BATCH_MAX` to the `net` import. Resolved as a union of both. Test count went 52 (main) + 3 (this branch) = 55, and `cargo test -p cipherbox-engine` reports `55 passed` for the suite, so nothing was dropped from either side.

This branch moves `RETIRE_BATCH_MAX` out of `net/retire.rs` and reintroduces it as `REGISTRY_BATCH_MAX` in `net/mod.rs`, which auto-merge would silently break if main had gained a consumer of the old name. Swept the tree post-rebase: zero occurrences of `RETIRE_BATCH_MAX` remain, and all nine `REGISTRY_BATCH_MAX` references resolve. Clean.

Review body nitpick (no thread to reply on): the comments in `registry-error-codes.ts` were trimmed as suggested. The permanent-refusal rationale, the stable client-facing purpose of the code, and the blueprint reference are kept; the issue-history narrative is gone. The Nest note was kept but cut to one line — it is the non-obvious reason the envelope is hand-built rather than left to Nest, and the file now also carries `BatchRefusedDto`, so that rationale explains why the two must stay in lockstep.

Verification — all green on the rebased tree, with a per-worktree `CARGO_TARGET_DIR`:

  • `cargo clippy --workspace --all-targets -- -D warnings` — exit 0
  • `cargo test -p cipherbox-engine` — exit 0, write-plane 55 passed
  • `cargo check -p cipherbox-wasm --target wasm32-unknown-unknown` — exit 0
  • `cargo fmt --all --check` — clean
  • `pnpm typecheck` — exit 0 across all packages
  • `apps/api` integration suite vs real Postgres — 13 files, 136 tests passed
  • eslint + prettier on the touched paths — clean

@FSM1 FSM1 mentioned this pull request Aug 2, 2026
@FSM1

FSM1 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Follow-up filed as #965 — shorten the API test-tier filename suffixes.

The *.http.integration.test.ts / *.service.integration.test.ts names are a repo-wide convention, not something this PR introduced, so the rename is out of scope here. #965 is ordered to land after this PR, since apps/api/src/registry/registry.http.integration.test.ts is the only file in that set touched by an open PR.

@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 18:21
@FSM1
FSM1 merged commit 4162154 into main Aug 2, 2026
23 checks passed
@FSM1
FSM1 deleted the feat/920-chunk-register-content-cids branch August 2, 2026 18:21
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.

engine: chunk the registration to the registry per-entry content-CID cap

1 participant