Skip to content

fix(service): sign resharing justifications before returning them to the CL#88

Merged
0xHansLee merged 3 commits into
mainfrom
dkg/fix-sign-resharing-justifications
Jul 23, 2026
Merged

fix(service): sign resharing justifications before returning them to the CL#88
0xHansLee merged 3 commits into
mainfrom
dkg/fix-sign-resharing-justifications

Conversation

@0xHansLee

Copy link
Copy Markdown
Collaborator

Stacked on #87 — base is dkg/fix-process-deals-reemit, not main. Review/merge #87 first.

Problem

In every resharing round, a dealer's justification is unsigned, so the CL drops it (schnorr: signature of invalid length 0) and complaint recovery is structurally dead. Keys rotate per round, so a resharing dealer is never in the new committee (newPresent=false); kyber's processResharingResponse then hand-builds the justification without the Schnorr signature the normal dealing path applies (vss.Dealer.ProcessResponse). Fixed upstream in kyber v4.0.2; we are on v4.0.0-pre2.

Fix

  • Sign the inner vss.Justification with the dealer's prev-round longterm key over j.Justification.Hash(suite) — the exact bytes the CL verifies against the dealer's prev-round dkgPubKey. Only when unsigned, so the initial-DKG path (already kyber-signed) is untouched.
  • Re-emit on retry (same class as fix(service): re-emit stored responses when ProcessDeals dedups retried deals #87, for the response→justification path): persist the signed justification with the response (EmittedJustifications), and on an idempotent retried complaint re-emit the stored copy so it is not lost.

Tests

  • TestSignResharingJustification_ResharingSignsAndVerifies
  • TestSignResharingJustification_PassesStoryClientVerification
  • TestSignResharingJustification_InitialPathUntouched
  • TestApplyResponses_ResharingJustificationSignedViaWiring
  • TestApplyResponses_KeyLoadFailureDropsJustification
  • TestApplyResponses_ReEmitsEmittedJustificationOnRetry
  • TestApplyResponses_SeparatesEmittedJustifications

issue: #85

@yingyangxu2026

Copy link
Copy Markdown
Contributor

Verified the signing end-to-end against a manufactured resharing complaint on a SGX devnet: the dealer's justification is signed, passes the CL's Schnorr check (zero drops across all nodes), gets processed, and the round recovers. Confirmed the signed bytes match what the CL verifies — kernel signs j.Justification.Hash(suite) (dkg_process_responses.go:451), CL verifies the same hash against the dealer's prev-round DkgPubKey (story dkg_justification.go:88-111). The previous silent Debug-level drops are gone. Looks correct.

Two minor notes: the signing path (dkg_process_responses.go:360-384, :446) doesn't log that it signed or with which round's key, so it's not observable on-chain whether a given justification was signed. And a key-load failure at :366 drops the justification for that round with no retry/restart recovery, so the dealer would be invalidated that round — it's the safe direction, but worth being aware it has no self-heal.

@jinn-agent

jinn-agent Bot commented Jul 21, 2026

Copy link
Copy Markdown

The core fix (signing unsigned resharing justifications and re-emitting stored ones on idempotent retries) is well-reasoned and the test coverage is thorough. Two issues worth addressing before merge:

  1. Potential nil-pointer panic in signJustificationIfNeeded: The function dereferences j.Justification.Signature without first confirming j.Justification != nil, unlike the downstream signResharingJustification which guards it explicitly.

  2. Idempotent responses accumulate duplicates in st.Responses on retries: An idempotent response sets res.processed = true, so it is appended to resps and re-persisted on every retry call. This was pre-existing, but the new AddProcessedResponses contract now also appends emittedJusts in the same atomic write — meaning on a retry the emitted justification slice is nil but the duplicate response is still written, growing st.Responses unboundedly across retries. Since the state file is replayed on rebuild, duplicate responses are fed back into kyber generators repeatedly; whether kyber is idempotent for all of those is an untested assumption.

No other high-confidence issues found. The mutex scope extension across process+persist, the lazy key loading, and the shouldReEmitJustification guard (idempotent AND no fresh justification AND complaint) are all correct.


Review iteration 3 · Commit 6b14a37 · 2026-07-23T06:30:30Z

@0xHansLee
0xHansLee changed the base branch from dkg/fix-process-deals-reemit to main July 23, 2026 06:16
…the CL

In resharing, kernel keys rotate per round so a dealer is never present in the
new committee (newPresent=false). Kyber's processResharingResponse then builds
the justification without a Schnorr signature, so the CL dropped every resharing
justification ("signature of invalid length 0") and complaint recovery was dead.
Sign the inner vss.Justification with the dealer's prev-round longterm key over
the exact hash the CL verifies; the initial-DKG path stays kyber-signed and is
never re-signed.
@0xHansLee
0xHansLee force-pushed the dkg/fix-sign-resharing-justifications branch from 59c45b6 to 6b14a37 Compare July 23, 2026 06:26
@0xHansLee
0xHansLee merged commit c7eb27e into main Jul 23, 2026
13 checks passed
@0xHansLee
0xHansLee deleted the dkg/fix-sign-resharing-justifications branch July 23, 2026 06:42
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.

2 participants