Phase 2 of docs/frost-batch-signing.md. Pure refactor: proposals still carry
one event, the wire is byte-identical, and every test passes unchanged --
344 jvmTest and 217 testDebugUnitTest, none of them edited in this commit.
advance() now loops over FrostSigningItem rows rather than reading the first
one. One nonce per item, one aggregate per item, one Session.create per item,
one partial signature per item, one signature per item. The signer set, the
public shares, the tweak cache and the approval stay shared, because they are
the terms that do not enter e = H(R‖P‖m).
The coordinator's aggregation is the place where that distinction bites: it
builds one AggregatedNonce per item, each from that item's nonce from each
chosen signer. Reusing one across two items would be reusing R across two
messages.
## The payload codec, early
joinPayload/splitPayload land here rather than with the wire change, because at
a batch of one a comma join is the identity -- the payload is the bare value it
has always been. That leaves Phase 3 to the proposal encoding alone.
splitPayload is strict: a payload that is not exactly the batch's length is
dropped rather than truncated or padded. It runs in orderedNonces,
orderedPartialSignatures and splitForSession -- never in record(), which stores
payloads without parsing them so that a nonce can arrive before the proposal
that would give it a length to check against.
## Two short-circuits, and one trap in the first
advance() runs on every arriving message, so at a batch of k it was k native
key generations, k Session.creates and k signs each time, usually to discover
there was nothing left to do.
- Nonces are generated by `lazy`. The obvious version -- a guard computing
`ownNonce == null || (isSigner() && ownPartial == null)` -- is wrong, and
wrong in a way that reads fine and fails every signing test: the coordinator
settles the signer set further down the same pass, so isSigner() at the top
is false on exactly the pass where the coordinator goes on to sign, and the
nonces are never generated. Reproduced as IndexOutOfBounds before switching
to lazy, which has no prediction to make.
- A device that is neither signing nor aggregating leaves before building any
FROST session, rather than building k of them to do nothing with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>