Implement DKG Contracts#9
Open
jannikluhn wants to merge 12 commits into
Open
Conversation
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
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.