paritytech / paritytech/verifiablejs

Revisit the `one_shot` prover API: is the single-call flow still the best option, and does the name still fit?

Open
#23 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
1
Forks
1
Avg merge
1h 59m
Merged PRs (30d)
4

Description

Summary

one_shot bundles secret derivation + ring open + proof create into a single WASM call. This is a revisit issue: is that still the best (or only) shape, does the original rationale still hold, and does the name still communicate intent?

Short version after digging through the verifiable crate:

  • The "we can't serialize the secret" memory is correct, but that is not what forces a single call.
  • The more interesting question is whether a prover should be able to open a ring once and create many proofs against it — which the crate already supports and we simply don't expose.
  • one_shot names the implementation, not the intent; prove / prove_membership would read better.

Not proposing to rush a change — mostly want to record the findings and decide keep vs. improve.

What one_shot does today

new_secret(entropy) -> member_from_secret -> decode(members) -> open(...) -> create(...)

Bvv::open is re-run on every call. open does the ring setup (prover_key(&pks) over all ring members); create produces the per-context VRF output + ring proof.

The secret-serialization story, checked

  • GenerateVerifiable::Secret is bound only by Clone — unlike Member / Members / Proof / Signature, which are all FullCodec. So the secret genuinely cannot be Encode/Decode'd across the WASM boundary. ✔ the memory holds.
  • But we never need to serialize it. new_secret(entropy) = Secret::from_seed(entropy) is cheap and deterministic, and the 32-byte entropy (already derived from the mnemonic on the caller side) is the serializable stand-in. So the secret constraint does not force bundling open with createcreate can always re-derive the secret from entropy internally.

The thing actually worth revisiting: reusing the ring commitment

  • open(config, member, members) -> Commitment, and Commitment (= ProverState) is FullCodec / serializable — it holds the ring-derived prover_key, the expensive part (cost scales with ring size).
  • create(commitment, secret, ctx, msg) is comparatively cheap per proof.
  • Because one_shot re-runs open every call, a client proving repeatedly against the same (large, People-chain-sized) ring redoes the expensive setup each time.
  • The crate already exposes open / create as separate #[cfg(feature = "prover")] trait methods — the split just isn't surfaced in the WASM binding. Note the verifier side already has the analogous split (members_root + validate_with_commitment); the prover side doesn't.

Options

  • A — status quo. Keep one_shot as the only prover entry point. Fine if provers rarely make >1 proof per ring, or if open is cheap enough at real ring sizes.
  • B — expose a split alongside it:
    • prover_open(ring_exponent, entropy, members) -> commitmentBytes
    • prove_with_commitment(ring_exponent, entropy, commitmentBytes, context, message) -> { proof, alias }
    • keep one_shot as a convenience wrapper. Open once, prove many.
  • C — multi-context. create_multi_context already yields N aliases from one proof against one open, which covers "many proofs, one open" when the contexts are known up front (not when they arrive over time).

Naming

one_shot describes the all-in-one implementation, not what it does. Candidates: prove, prove_membership, create_proof, generate_proof. If we add the split (B), a clean pairing is open + prove with one_shot kept as the convenience form. Leaning prove / prove_membership.

Security notes (so a future split doesn't reintroduce a footgun)

  • The real prover needs good randomness for the proof blinding — the deanonymization footgun (insecure-deterministic-prover zeroes the blinding and is trivially deanonymizable). In WASM this comes from getrandom's js feature; must stay on. A cached commitment (option B) does not change this — randomness lives in create, not open.
  • secret-split (side-channel mitigation) is orthogonal: it's about splitting the secret scalar during multiplication, not serialization.

Questions for the team

  1. At real People-chain ring sizes, how expensive is open vs create? That decides whether B is worth it.
  2. Is there a product flow that makes many proofs against one ring within a session (so caching the opened commitment actually helps)?
  3. If we rename: break now, or add prove as an alias and deprecate one_shot?

Cross-reference

verifiable-dart mirrors this exact API (one_shot, same shape). Whatever we decide should stay consistent across the JS and Dart clients.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the WASM binding entry points for one_shot and the verifiable crate's Bvv::open and create methods; measure or inspect their costs at realistic ring sizes. Compare the existing verifier split and verifiable-dart API. Done means a documented decision on status quo, split exposure, and naming, with consistent client APIs if changes are approved.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, wasm
Domain
api, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.