Skip to content

Implement DKG Contracts#9

Open
jannikluhn wants to merge 12 commits into
mainfrom
dkg-contract
Open

Implement DKG Contracts#9
jannikluhn wants to merge 12 commits into
mainfrom
dkg-contract

Conversation

@jannikluhn
Copy link
Copy Markdown
Contributor

No description provided.

jannikluhn and others added 12 commits May 20, 2026 10:22
Introduces the DKGContract with its four immutables (PHASE_LENGTH,
DKG_LEAD_LENGTH, keyperSetManager, keyBroadcastContract) and a
matching deploy fixture. Phase arithmetic, bulletin-board functions,
and success voting follow in subsequent commits.
Adds Phase enum (None/Dealing/Accusing/Apologizing/Finalizing),
cycleLength(), dkgStart(keyperSetIndex, retryCounter), and
currentPhase(keyperSetIndex, retryCounter) to DKGContract.

DKG instance (k, r) starts at:
  activation_block(k) - DKG_LEAD_LENGTH + r * cycleLength()

All phase windows are derived arithmetically from that start; no
per-instance block is stored. dkgStart returns int256 so it can go
negative when lead exceeds the activation block (edge case handled
without reverting).

Tests cover all phase boundaries for r=0, the r=0→r=1 retry
boundary, dkgStart arithmetic, and the negative-start edge case.
Adds a standalone registry where Keypers register their ECIES
encryption public keys. Membership is verified via KeyperSetManager
at registration time; keys persist across Keyper Set transitions so
a new KeyperSet deployment does not require re-registration.

Key design decisions:
- Uses OZ EnumerableSet.AddressSet so off-chain services can backfill
  the full set of registered keys via getKeyperCount()/getKeyperAt().
- Re-registration (key update) is accepted: add() is idempotent so
  the set size does not grow on re-registration.
- KeyperSetNotFinalized is surfaced from KeyperSet when the set is
  not yet finalized.

Also updates Deploy.gnosh.s.sol and Deploy.s.sol to deploy
DKGContract and ECIESKeyRegistry alongside the existing contracts,
and wires DKGContract as the KeyperSet publisher in Deploy.s.sol.

Tests cover basic registration, key lookup, identity/membership
checks, arbitrary-bytes acceptance, and the iterable enumeration API.
Adds submitDealing (Dealing Phase), submitAccusation (Accusing Phase),
and submitApology (Apologizing Phase) to DKGContract. Each validates
phase, identity, and the succeeded[k] guard, then emits an event with
no on-chain cryptographic verification and no deduplication.

Key decisions:
- submitDealing accepts commitment and polyEval together; callers
  cannot submit them separately.
- Empty accusedIndices reverts with EmptyAccusation. An empty
  accusation has no protocol meaning; clients should skip the call.
- Empty apology arrays (length 0) are accepted as a valid no-op.
  Mismatched array lengths revert with MismatchedArrays.
- succeeded[k] mapping is declared here as a read-only gate for the
  bulletin-board functions; the write logic is owned by issue #4.
- Events use 3 indexed fields (keyperSetIndex, retryCounter,
  keyperIndex) for efficient off-chain filtering.
- A shared _checkMember helper calls
  KeyperSet(manager.getKeyperSetAddress(k)).getMember(keyperIndex)
  and requires msg.sender match — reverts NotKeyperAtIndex.

Tests cover phase/identity/array-validation for all three functions.
Implements submitSuccessVote(keyperSetIndex, retryCounter, keyperIndex,
eonPublicKey) on DKGContract. Accepts votes only during the Finalizing
phase, verifies caller membership, and prevents double-voting per
(keyperSetIndex, retryCounter, voter).

When a key hash reaches KeyperSet.getThreshold():
- succeeded[keyperSetIndex] = true (gates all message functions)
- KeyBroadcastContract.broadcastEonKey is invoked in try/catch so an
  already-broadcast key (or unauthorized publisher) does not block
  recording success.
- DKGSucceeded event is emitted regardless of the broadcast outcome.

Late-arriving votes within the same Finalizing window are accepted
and emit SuccessVoteSubmitted but do not re-trigger success once it
has already been recorded.

State added: voteCount[k][r][keyHash], hasVoted[k][r][voter].
Events added: SuccessVoteSubmitted, DKGSucceeded. Error: AlreadyVoted.

setUp now sets the DKGContract as keyperSet0's publisher before
finalization so the threshold-reached broadcast succeeds in the
default fixture. The silent-revert case constructs its own keyper set
with a different publisher.

Tests cover phase rejection, identity, double-vote, threshold counting
(per-key-hash isolation, no broadcast below threshold), silent
broadcast failure, post-success rejection across all message types,
concurrent keyper-set independence, and the r=0/r=1 retry boundary.
Replace the opaque ABI-encoded bytes blob with a native bytes[] so the Go
binding handles encoding and decoding without bespoke helpers.

Key decisions:
- Empty arrays are accepted for backward compatibility with the existing
  testConcurrentDKGInstancesAreIndependent / retry-dealing tests, which
  previously passed an empty bytes blob.

Files changed:
- src/common/DKGContract.sol: submitDealing argument and DealingSubmitted
  event now use bytes[] for polyEvals
- test/DKGContract.t.sol: all submitDealing call sites and the mirrored
  event signature updated to bytes[]
Add `dkgContract` slot, owner-only pre-finalization setter
`setDKGContract`, and `getDKGContract` view getter to KeyperSet.sol.
Expose `getDKGContract` on the IKeyperSet interface so the Go keyper
can read it through the typed binding when handling KeyperSetAdded.

Tests:
- forge test passes (116 tests, incl. new testDKGContract,
  testSetDKGContractOnlyOwner, and the after-finalized revert case)
- go build ./... passes for the regenerated keyperset binding

Files:
- src/common/KeyperSet.sol
- src/common/intf/IKeyperSet.sol
- test/KeyperSet.t.sol
- bindings/keyperset/keyperset.go (regenerated)

Notes for next iteration:
- E2E tests in mise-test-setup were not run: the foundry/anvil docker
  image is linux/amd64 and the sandbox is arm64, so the ethereum
  container fails with `exec format error`. Acceptance criteria of this
  task only require forge test + go build.
- Unblocks docs/dkg-module-refactor/contract-updates/003 (eons per-
  keyperset phase params), which will call GetDKGContract from Go and
  store phase_length/lead_length on the eons row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forge test contract `DKGBenchmark` that measures total on-chain gas for a
clean DKG run (n submitDealing calls + ⌈2n/3⌉ submitSuccessVote calls) at
n = 3, 5, 10, 20, 50. Setup gas — contract deployment and keyper set
creation — is excluded; only protocol operation cost is accumulated via
gasleft() deltas around each contract call.

Key decisions:
- Realistic, non-zero (0xAB) payload sizes from the PRD: commitment =
  4 + threshold × 96 bytes, each PolyEvals entry = 129 bytes (ECIES),
  EonPublicKey = 96 bytes. Non-zero bytes cost 16 gas/byte in calldata
  and match real BLS/ECIES data.
- Threshold via integer ⌈2n/3⌉ = (2n + 2) / 3.
- Fresh KeyperSetManager / KeyBroadcastContract / DKGContract per test
  so warm-storage artefacts from earlier runs do not contaminate
  results.
- Per-test console.log line emits scenario, n, threshold, and gas total
  for easy parsing.

Measured gas (monotonic by design — quadratic calldata dominates):
  n=3   →   293,144
  n=5   →   457,151
  n=10  →   901,165
  n=20  → 2,368,268
  n=50  → 10,962,537

Files changed:
- test/DKGBenchmark.t.sol (new)

Notes for next iteration:
- 002-worst-case-benchmark extends this with Accusing + Apologizing
  phases, accumulating into the same totalGas counter and adding five
  test_worstCase_n* entry points.
- Rolling-shutter e2e suite could not be exercised in this sandbox
  (postgres/sqlc downloads firewall-blocked); the change is purely
  additive Forge test code so it cannot regress Go paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor _runHappyPath into a parameterized _run(n, worstCase) and
extend it with accusing/apologizing phases for the worst-case scenario.
Add test_worstCase_n{3,5,10,20,50} entry points.

The worst case has every keyper accuse all n-1 others in one tx, then
respond to all n-1 accusers in one apology tx with plaintext 32-byte
BLS12-381 scalars (NOT 129-byte ECIES ciphertext — the accused keyper
reveals the eval in the clear).

Key decisions:
- Single _run function (parameterized) over duplicating happy-path code.
- Apology polyEvalData entries are 32 bytes per PRD: plaintext scalar.
- Accusing block = dealingBlock + PHASE_LENGTH;
  apologizing block = dealingBlock + 2 * PHASE_LENGTH;
  finalizing block unchanged.

Files: contracts/test/DKGBenchmark.t.sol.

Verified all 10 tests pass; worst-case gas strictly exceeds happy-path
at each n (n=50: 22M worst-case vs 11M happy-path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…AddKeyperSet

Service deployment now also deploys DKGContract and ECIESKeyRegistry, so the
e2e tests can exercise the full DKG cycle against the shutterservice keyper.
AddKeyperSet (both common and gnosh) accept an optional DKG_CONTRACT_ADDRESS
env var and call setDKGContract on the new KeyperSet when provided.

Key decisions:
- Reuse the existing deployDKGContract / deployECIESKeyRegistry helpers
  (already shared with Deploy.gnosh.s.sol) instead of forking. Reads the
  same DKG_PHASE_LENGTH / DKG_LEAD_LENGTH env defaults.
- AddKeyperSet treats DKG_CONTRACT_ADDRESS as optional: if unset, no
  setDKGContract call is made. This keeps backward compatibility with
  callers that haven't been updated, and lets the keyper's eons-row code
  fall back to its config-supplied global DKG contract.

Files changed:
- script/Deploy.service.s.sol: deploys DKGContract + ECIESKeyRegistry.
- script/AddKeyperSet.s.sol, script/AddKeyperSet.gnosh.s.sol: accept
  DKG_CONTRACT_ADDRESS env var.

Notes for next iteration:
- mise-test-setup's contracts container is built from
  https://github.com/shutter-network/contracts.git#docker so these script
  changes will only reach the e2e test runs once that branch is pushed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added `error WrongDKGContract` and `_checkDKGContract(uint64 keyperSetIndex)`
helper that reverts if the Keyper Set's `dkgContract` field does not match
`address(this)` (address(0) is treated as a mismatch — no special case).
Called before `_checkMember` in `submitDealing`, `submitAccusation`,
`submitApology`, and `submitSuccessVote`.

Files changed:
- src/common/DKGContract.sol: error + helper + 4 call sites
- test/DKGContract.t.sol: setUp wires setDKGContract; 5 new revert tests

Decision: check runs after phase/success guards so misconfigured keypers
get a clear WrongDKGContract error rather than a phase error.
All ten tests were reverting with WrongDKGContract(). _setup() finalized
the KeyperSet without calling ks.setDKGContract(address(dkg)), so
_checkDKGContract saw address(0) instead of address(this).

Added ks.setDKGContract(address(dkg)) between setPublisher and setFinalized,
mirroring DKGContractTest.t.sol.

Files changed: test/DKGBenchmark.t.sol
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