Skip to content

test: add live mixed-binary legacy signing interop harness - #32

Merged
piotr-roslaniec merged 4 commits into
devfrom
test/mixed-binary-legacy-interop
Sep 30, 2026
Merged

piotr-roslaniec merged 4 commits into
devfrom
test/mixed-binary-legacy-interop

Conversation

@piotr-roslaniec

Copy link
Copy Markdown

Summary

Closes the mixed-binary assurance gap: the existing session-less legacy
Bob/BobWC coverage (crypto/mta/legacy_bob_historical_witness_test.go,
ecdsa/signing/round_3_test.go) pins the shape of a historical witness
using this implementation's own prover equations, which proves the
verifier's bound math but never exercises an actual historical binary.
This PR adds a harness that drives a real, live signing exchange
against a subprocess running the pinned historical
threshold-network/tss-lib@2e712689 commit — the same commit already
qualified by testdata/legacy_transcript/verify.sh's oracle vectors.

Test/oracle files only, all under testdata/legacy_transcript/. No
production code changes.

What's added

  • testdata/legacy_transcript/historical_signer/main.go — a subprocess
    entry point, compiled only inside the pinned historical module (same
    historical/go.mod.fixture/go.sum.fixture pin the oracle already uses).
    Drives one live historical ecdsa/signing.LocalParty (party index 1,
    "Bob") over a bounded, newline-delimited JSON protocol on stdin/stdout
    (init/deliver/quit in, message/signature/error/turn_done
    out). No network access at run time.
  • testdata/legacy_transcript/mixed_interop/main.go — the current-side
    orchestrator (go run from the repo root, same invocation style as the
    oracle). For a fixed 2-of-20 keygen_data_0/1 fixture pair, deterministic
    seeds, and a fixed message, it drives:
    • reject: a current party in ProtocolModeLegacy with the default
      (off) SetLegacyHistoricalBobCompatibility fails closed at round 3
      against the historical peer's real, live-drawn Bob/BobWC witness
      (MtA blinding value sampled below the Paillier modulus N, per the
      historical BobMid/BobMidWC). The witness's T1 is recovered from the
      actual wire bytes received from the subprocess (via
      SignRound2Message.UnmarshalProofBob/UnmarshalProofBobWC, not a
      hand-picked constant) and asserted to exceed the default tight
      N + q^6 bound — so the rejection is proven meaningful, not vacuous.
    • accept: the identical exchange with
      SetLegacyHistoricalBobCompatibility(true) succeeds at round 3 and
      provably progresses: the current party emits its round-3 message, the
      historical peer accepts it, and the live exchange continues through
      round 8 in both directions.
    • homogeneous-control: two current-implementation parties, no
      historical subprocess at all, drive the identical ceremony shape
      through round 8 under the default configuration — proof that "reject"
      is specific to the historical witness range and not a general
      legacy-mode defect, so a homogeneous run is never conflated with a
      mixed-binary one.
  • testdata/legacy_transcript/verify_mixed_interop.sh — the durable script,
    following verify.sh's existing pattern, that sets up the temporary
    pinned module and runs the above end-to-end.
  • testdata/legacy_transcript/README.md — new section documenting the
    harness and its scope.

Why the exchange stops at round 8

This repository's own existing round3Fixture
(ecdsa/signing/round_3_test.go) and historicalBobProofForWitnessY
(crypto/mta/legacy_bob_historical_witness_test.go) already establish the
precedent of driving a 2-of-20 minimal subset of the
test/_ecdsa_fixtures keygen fixtures (threshold 1, not the fixture set's
real threshold 10) for this exact class of round-level interop check. That
minimal subset is sufficient for every per-peer MtA/Schnorr check through
round 8 (each is a property of the two parties' own consistent local
computation), but round 9's final aggregate check (U == T) verifies a
global Shamir reconstruction identity that only holds for a
correctly-sized threshold+1 co-signer set.

Reaching a real, live-exchanged round 8 message already proves the
historical Bob/BobWC witness was accepted and every subsequent round 3–8
verification/decommitment step (Bob_end, the Gamma/Schnorr proofs, and both
decommitments) succeeded against a genuine historical binary. Driving a
full, globally-valid signature to completion is technically possible but
requires testThreshold+1 (11) correctly-thresholded co-signers rather
than an arbitrary 2-of-20 subset — substantially more harness complexity
for a property (global reconstruction validity) that is orthogonal to the
specific Bob/BobWC compatibility mechanism this harness exists to exercise.
This is the "exact inaccessible prerequisite" for full-ceremony completion
within this minimal-fixture design, not a claim that a full ceremony is
impossible.

A note on exact-byte reproducibility

Signing round 2 (ecdsa/signing/round_2.go) draws the Bob and BobWC
witnesses from two goroutines running concurrently against the
process-global crypto/rand.Reader, so which goroutine consumes which
slice of the deterministic keystream is scheduler-dependent — the exact
witness scalar differs slightly run to run (confirmed empirically). Every
run nonetheless deterministically reproduces the qualitative property
under test: a high witness that exceeds the tight bound, a closed-by-default
rejection, and an opt-in acceptance that keeps progressing. That qualitative
property is what verify_mixed_interop.sh asserts and fails on. The
harness's own deterministicReader was made mutex-protected specifically to
avoid the corrupted (non-well-formed) draws a naive shared-stream
implementation would otherwise produce under this concurrency.

Verification

GOTOOLCHAIN=auto ./testdata/legacy_transcript/verify_mixed_interop.sh

Ran clean (exit 0) across 5 consecutive runs. Confirmed the script fails
(non-zero exit, FAIL: opt-in current party did not accept and progress past round 3) when tss.Parameters.LegacyHistoricalBobCompatibility() is
patched to unconditionally return false (simulating a real wiring
regression), and passes again once reverted — proving this is a durable
regression test for the compatibility opt-in wiring, not a tautology.

No error-wording/source-text assertions: all pass/fail signals are
behavioral (error non-nil/nil, message round-type via protobuf content type
switch, T1 field comparison against the computed bound).

Findings addressed: mixed-binary assurance gap for legacy signing
Bob/BobWC compatibility — closes the gap between the existing session-less,
same-binary-only proof-shape tests and a genuine live cross-version
exchange.

Closes the mixed-binary assurance gap left by the existing session-less
legacy Bob/BobWC coverage (crypto/mta/legacy_bob_historical_witness_test.go,
ecdsa/signing/round_3_test.go), which pins the shape of a historical witness
using this implementation's own prover equations but never exercises an
actual historical binary.

Adds testdata/legacy_transcript/historical_signer, a subprocess entry point
compiled only inside the pinned historical (threshold-network/tss-lib@
2e71268) module (same pin as the existing oracle), driven over a bounded
newline-delimited JSON protocol on stdin/stdout.

Adds testdata/legacy_transcript/mixed_interop, the current-side orchestrator
that drives a live 2-of-2 ECDSA signing ceremony against that subprocess and
asserts:
  - reject: a default-configured current party (ProtocolModeLegacy, compat
    off) fails closed at round 3 against the historical peer's real,
    live-drawn Bob/BobWC witness, whose T1 (recovered from the actual wire
    bytes, not a hand-picked value) is asserted to exceed the default tight
    N+q^6 bound.
  - accept: the identical exchange with SetLegacyHistoricalBobCompatibility
    (true) succeeds at round 3 and provably progresses through round 8 in
    both directions.
  - homogeneous-control: two current-only parties complete the identical
    ceremony shape through round 8, proving reject is specific to the
    historical witness range and not a general legacy-mode defect.

Adds verify_mixed_interop.sh, following verify.sh's existing pattern, as the
durable script CI/developers run to catch regressions in historical/current
round interop or the compatibility opt-in wiring; confirmed to fail when the
opt-in wiring is deliberately broken (LegacyHistoricalBobCompatibility()
forced to return false) and to pass again once restored.

No production code changes; test/oracle files only under
testdata/legacy_transcript.
…file split

Addresses PR review findings on the mixed-binary legacy signing interop
harness:

1. Independent Bob/BobWC per-proof verification: previously the reject
   scenario only checked an aggregate round-3 pass/fail, which cannot
   distinguish a regression where only one of Bob/BobWC is correctly
   gated by the compatibility flag (e.g. Bob accepts the widened bound
   but BobWC stays tight, or vice versa) from a fully-correct
   implementation, as long as the OTHER proof still legitimately fails.
   Both the reject and accept scenarios now independently re-verify Bob's
   and BobWC's proofs via ProofBob.VerifyLegacy/ProofBobWC.VerifyLegacy,
   using the actual captured wire proof and reconstructed public inputs
   (Alice's own Paillier key and Ring-Pedersen parameters per
   ecdsa/signing/round_3.go's exact calling convention, her round-1
   ciphertext, Bob's round-2 response, and Bob's PrepareForSigning-derived
   EC contribution — correctly resliced via
   keygen.BuildLocalSaveDataSubset to the 2-party signing session, not the
   fixture's underlying 20-party keygen ceremony), asserting each proof
   independently rejects at the tight bound and accepts at the loose
   bound.
2. Structural (non-text) rejection constraints: the reject scenario's
   round-3 failure is now constrained to tss.Error.Round() == 3, the
   historical peer's party ID present in tss.Error.Culprits(), and no
   round-3 message ever emitted by the current party — replacing an
   AliceProgressed-based proxy, and never parsing the error string.
3. Split mixed_interop/main.go (was 583 lines, over the repository's
   500-line file cap) into main.go (entry point), peer.go (subprocess
   wire protocol), scenarios.go (scenario logic and per-proof
   verification), and homogeneous.go (the control scenario). All four
   files are now under 500 lines.

Also fixed two bugs surfaced while implementing the above:
  - runHomogeneousControl delivered a message back to its own sender
    instead of routing it to the other party (a latent bug from an
    earlier pass), and stopped after forwarding a round-8-or-later
    message instead of before, cascading into an expected-but-unhandled
    round-9 reconstruction error for this minimal fixture subset.
  - The per-proof verification call was only reachable from the
    non-rejecting exit path of runMixedScenario, so it silently never ran
    for the reject scenario (leaving all four fields at their zero value)
    until extracted into a shared helper called from every exit path.

Verified: 15 consecutive clean runs of the full harness after the fix;
confirmed the harness still fails when
LegacyHistoricalBobCompatibility() is patched to force-return false and
passes again once reverted.
@piotr-roslaniec

Copy link
Copy Markdown
Author

Final integration evidence at fb7569146bced2803ccd7a2241715a38d9c01c0b:

  • Live mixed-binary script passed repeatedly against the pinned historical binary.
  • Default path rejected in round 3 with both production Bob/BobWC verifier failures attributed to the historical peer (rejection_peer_culprit_count=2) and no round-3 output.
  • Opt-in path accepted both independently captured proofs and exchanged live messages through round 8.
  • Independent final re-review found both rejection-oracle issues resolved.
  • Go-fmt run 36656436843 and full Go Test run 36656436901 passed.

Scope: the harness intentionally stops after round 8 because its two fixture shares come from a 2-of-20 key set with threshold 10; it does not claim final signature completion.

@piotr-roslaniec
piotr-roslaniec merged commit 54b1e39 into dev Sep 30, 2026
4 checks passed
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