docs(frost): record batch signing as built, and what rollout needs

Phase 7 of docs/frost-batch-signing.md, which is the phase with no code in it.

Nothing needs a feature flag. k=1 is the entire behaviour of the app as shipped
-- no caller batches anything yet -- and at k=1 every message is byte-identical
to the app before Phase 1: encodeProposal returns the bare event object,
joinPayload of one value is that value, and every plural branch in the
transcript is only taken above one. The doc now tabulates that rather than
asserting it in prose, since it is the claim the whole rollout rests on.

The one rollout constraint stands: before a caller batches, the group has to be
on a build that understands array proposals. There is no negotiation for it and
adding one is not worth it -- an old device refuses an array proposal outright,
so the failure mode is a batch that never reaches threshold and is abandoned,
visible in the transcript and costing a retry.

Also records what is left, which is nothing in the protocol: deciding what to
batch is a product question, bounded only by "a batch is only as available as
its worst item" and "GroupKeyStateManager.propose must never batch".

The phases are kept as written rather than rewritten into a description of the
result -- the code reads better against the argument it came from -- with the
two places the implementation chose differently (itemIndex over index,
DROP COLUMN over a table rebuild) marked in their own sections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-06 05:00:21 +02:00
parent 2309879153
commit 1448ed5ad8
2 changed files with 32 additions and 9 deletions

View File

@@ -20,6 +20,6 @@ Start with the ceremony if you are new to this area; the Marmot notes all assume
Read the skipped-keys note before debugging any "the other device never got it"
report — it is silent, and it looks like every other kind of delivery failure. The
sync note stands alone, and the dead-code inventory reads as a follow-up to it. The
batch-signing note is a plan rather than a description of what is there: read it
after the derivation note, whose one rule is the same one it is built around. The
batch-signing note is a phased plan that has been built: read it after the
derivation note, whose one rule is the same one it is built around. The
jvm-target note is unrelated to all of them: it is a build and packaging story.

View File

@@ -4,6 +4,14 @@ How to have a group sign several events in one ceremony instead of one at a
time, phased, with the cryptographic constraint that shapes every phase stated
first.
**Built.** All seven phases are in, one commit each, and the phases below are
kept as written -- they are the reasoning, and the code is easier to read
against the argument it came from than against a summary of itself. Where the
implementation chose differently from the first draft the section says so.
`FrostSigningManager.proposeSigningBatch` and
`FrostSigningRepository.proposeSigningBatch` are the entry points; nothing in
the app calls them yet, which is Phase 7's point.
The headline: **there is no such thing as one FROST signature over many
messages, and no way to reuse a nonce across them.** What can be batched is the
ceremony — the rounds, the group events, and the approval a human is asked for.
@@ -452,18 +460,33 @@ either alone is enough to be relied on by accident).
**No code.**
Nothing here needs a feature flag. `k = 1` is the entire behaviour of the app
after Phase 4, byte-identical on the wire to the app before Phase 1, and no
caller batches anything until one is written to. Phases 1 and 2 are shippable on
their own and worth shipping on their own — they are a schema move and a
refactor, and landing them apart from the wire change means a bisect over a
signing bug lands on one or the other rather than on all of it.
Nothing here needs a feature flag. `k = 1` is the entire behaviour of the app as
shipped — no caller batches anything yet — and at `k = 1` every message is
byte-identical to the app before Phase 1:
| message | at `k = 1` |
|---|---|
| proposal | `encodeProposal` returns the bare event object (asserted in `FrostProposalCodecTest`) |
| nonce, signer set, partial, signature | `joinPayload` of one value is that value |
| chat transcript | every line's plural branch is only taken above 1 |
Phases 1 and 2 are shippable on their own and were worth landing on their own —
a schema move and a refactor — because a bisect over a signing bug then lands on
one or the other rather than on all of it.
Before the first caller batches, confirm the group is on a build that
understands array proposals. There is no negotiation for this and adding one is
not worth it; the failure mode is a batch that never reaches threshold and is
not worth it: the failure mode is a batch that never reaches threshold and is
abandoned, which is visible in the transcript and costs nothing but a retry.
### What is left, when a caller wants it
Nothing in the protocol. The remaining work is deciding *what* to batch, which
is a product question this document deliberately does not answer — beyond the
one rule that a batch is only as available as its worst item, so events that do
not belong together should not travel together, and the one prohibition that
`GroupKeyStateManager.propose` must never batch.
---
## Appendix — what was considered and rejected