Skip to content

perf(transcript): verify each record once and reuse the decoded frame - #2

Open
thangvmt wants to merge 2 commits into
PASdeco:fix/transcript-timestamp-authfrom
thangvmt:patch/97-verify-once
Open

thangvmt wants to merge 2 commits into
PASdeco:fix/transcript-timestamp-authfrom
thangvmt:patch/97-verify-once

Conversation

@thangvmt

Copy link
Copy Markdown

Addresses @khenzarr's measurement on flop-labs#97: the warning pass verifies every record three times.

foldTranscript walks the records three times and calls verifyTranscriptRecord in each pass — once to build the per-room map for gap analysis, once inside the hasDeadlineFrame probe, and once in the fold itself. tryDecodeFrame runs twice for the same reason. Ed25519 verification dominates the cost of a large retained export, so the warning feature roughly triples the price of a fold even for lines that never decode as TCLK frames.

This patch builds on the verified-row gap analysis from flop-labs#94/flop-labs#97. Composing that pass with deadline detection and folding repeated signature verification, so this refactor caches verification and decoding once per record and shares the result across all three consumers.

Measurement

Same machine, same input, timing one foldTranscript call after building 2000 deterministic signed records. The unpatched number comes from stashing only src/transcript.ts and rebuilding, so both rows are measured under identical conditions:

before   6792.3 ms / 6788.9 ms
after    2257.7 ms / 2259.2 ms

That is 3.01x, consistent with the 2.93x reported on the issue.

The test pins structure, not time

tests/transcript-cost.test.ts asserts one verification per record rather than a duration, because a timing assertion is fragile on shared CI.

ed25519 is a frozen object, so vi.spyOn fails with Cannot redefine property: verify. The module is mocked through vi.hoisted in an isolated file, and the real implementation is called behind a counter. Keeping it in its own file means that if the mock ever breaks it does not take the existing suites with it, and no injection seam is added to production code for the sake of a test.

The test REDs on the unpatched base with expected 12 to be 5. The 12 is itself informative: the hasDeadlineFrame probe uses records.some, so it short-circuits at the second record rather than walking all five.

Behavior

No protocol or state-machine behavior change; one warning index is corrected for aliased duplicate objects. Carrying the original index on each checked entry also removes the records.indexOf scan inside the gap loop, which was O(N²) on the number of verified rows.

Gates

Per AGENTS.md, recursive with the workspace root:

pnpm -r --include-workspace-root build
pnpm -r --include-workspace-root test

root 107/107, mcp 40/40, worker 33/33 — 180/180.

One conflict to flag

My other open patch against this branch, #1, appends the derived-room firstSeq anchor to the same for (const [room, list] of byRoom) loop. After this refactor the loop iterates checked entries rather than raw records, so that block needs one adjustment when both land:

-      const firstSeq = list.reduce((min, r) => (r.seq < min ? r.seq : min), list[0]!.seq);
+      const firstSeq = list.reduce((min, r) => (r.record.seq < min ? r.record.seq : min), list[0]!.record.seq);

Merge in either order and I will rebase whichever lands second — say which you prefer and I will send it.

foldTranscript walked the records three times and called
verifyTranscriptRecord in each pass: once to build the per-room map for gap
analysis, once inside the hasDeadlineFrame probe, and once in the fold itself.
tryDecodeFrame ran twice for the same reason. Ed25519 verification dominates
the cost of a large retained export, so the warning feature roughly tripled
the price of a fold even for lines that never decode as TCLK frames.

This patch builds on the verified-row gap analysis from flop-labs#94/flop-labs#97. Composing
that pass with deadline detection and folding repeated signature
verification, so this refactor caches verification and decoding once per
record and shares the result across all three consumers.

No protocol or state-machine behavior change; one warning index is corrected
for aliased duplicate objects. Carrying the original index on each checked
entry also removes the records.indexOf scan inside the gap loop, which was
O(N^2) on the number of verified rows.

Measured on one machine with 2000 deterministic signed records, timing a
single foldTranscript call after building the input:

  before  6792.3 ms / 6788.9 ms
  after   2257.7 ms / 2259.2 ms

tests/transcript-cost.test.ts pins the invariant structurally rather than by
timing, which would be fragile on shared CI. ed25519 is a frozen object, so
vi.spyOn cannot wrap verify; the module is mocked through vi.hoisted in an
isolated file and the real implementation is called behind a counter. The
test REDs on the unpatched base (12 verifications for 5 records, because the
hasDeadlineFrame probe short-circuits) and GREENs here.

Gates, per AGENTS.md:

  pnpm -r --include-workspace-root build
  pnpm -r --include-workspace-root test

  root 107/107, mcp 40/40, worker 33/33 — 180/180

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fold

@stupeterwilliams-ui instrumented the call sites on flop-labs#115 and found the case the
original test missed: when every record fails verification, the unpatched fold
still pays the full 3.00x. Each pass verifies before it can skip, and the
hasDeadlineFrame probe's `some` returns false for an unverified record rather
than short-circuiting, so it walks the entire array. Decode work drops to zero
while the Ed25519 cost does not, which is why a wall-clock benchmark understates
this corpus rather than exposing it.

That is the input foldTranscript exists to handle, since its job is
authenticating records from a supplier the caller does not trust, so it is worth
pinning rather than leaving to the happy-path test.

RED on base aedd1c0, GREEN here:

  verifies each record's signature exactly once                  10 -> 4
  verifies once per record even when every record fails          60 -> 20
  does not re-verify a record whose line is not a TCLK frame     12 -> 5

Gates unchanged: pnpm -r --include-workspace-root build clean,
root 108/108, mcp 40/40, worker 33/33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant