# DKG permanently stuck for validator sets outside {4,5,6}: wrong coefficient-count check in register\_round1\_package

**URL:** https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99
**Category:** Submitted
**Tags:** exodus-pallet
**Created:** [October 2, 2026, 11:16am UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99 "2026-10-02T11:16:03Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Rusdiwireng](https://ghostforum.org/letter_avatar_proxy/v4/letter/r/9de0a6/32.png) [@Rusdiwireng](https://ghostforum.org/u/Rusdiwireng)
#### Post date: [October 2, 2026, 11:16am UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99/1 "2026-10-02T11:16:03Z")

</div>

**Component:** `ghost-node` @ `0162da4604d9931c16d12a0cec40dd85951368c5` — `pallets/exodus/src/lib.rs:982-996` (`register_round1_package`), `pallets/exodus/src/lib.rs:3745-3786` (`dkg_run_round1` offchain worker), `pallets/helpers/src/lib.rs:37-50` (`get_byzantium_quorum_threshold`). The production runtime allows up to 300 validators (`runtime/casper/src/lib.rs:348`, `MaxActiveValidators = 300`).

**Description**

The offchain worker generates DKG round-1 packages with `min_signers = get_byzantium_quorum_threshold(max_signers)` (lib.rs:3750), where the threshold is `n - floor((n-1)/3)`. The VSS commitment in the package therefore carries exactly `min_signers` coefficients (frost-core 2.2.0 `keys::dkg::part1`).

The on-chain check in `register_round1_package` requires something different:

```rust
// pallets/exodus/src/lib.rs:991-996
state.count_ones::<AuthIndex>().checked_sub(1)
    .map(|needed_commitment_count| {
        estimated_commitment_count
            .eq(&needed_commitment_count)
            .then(|| ())
    })
    .ok_or(Error::<T>::Round1PackageInvalidLength)?;

```

i.e. it demands `coefficient_count == (round0 registrants) - 1`. And `dkg_verify_proof_of_knowledge` (pallets/exodus/src/impls/dkg.rs:109-111) returns `commitment.coefficients().len()`, which is `min_signers` by construction. The two formulas agree only for n in {4,5,6}: n=4: 3==3; n=5: 4==4; n=6: 5==5; n=7: 5!=6; n=8: 6!=7; n=10: 7!=9.

Ironic detail: `validate_round1_package_size`, used by `validate_unsigned`, uses the correct formula `get_byzantium_quorum_threshold(authorities.len())` (lib.rs:2729). So the unsigned transactions pass the transaction pool and then fail at dispatch with `Round1PackageInvalidLength`, wasting block space on every DKG cycle.

**Impact**

For any validator set size outside {4,5,6}, DKG can never complete: every `register_round1_package` fails, `prepare_round2_storage` intersects the qualification bitmap down to zero, `on_initialize` (lib.rs:1940-1946) misses quorum, `restart_dkg()` fires, and the cycle fails forever. No key generation, no rotation, no threshold signing (bridge-out, rotation, governance). This is a total, deterministic liveness kill of the pallet’s core function and it needs no attacker at all. Every test in the repo uses 4 validators (`mock.rs: FixedAuthorities = vec![0,1,2,3]`), which is exactly the size where both formulas agree, so this slipped through.

**Reproduction**

Local PoC (no network, no mainnet touched). It builds honest round-1 packages with the same library the pallet uses (`frost_secp256k1_tr::keys::dkg::part1`), reads `commitment.coefficients().len()`, and applies the exact on-chain condition from lib.rs:991-996:

```auto
n= 4: min_signers(threshold)=3, coeffs=3, on-chain requires count_ones-1=3 -> ACCEPTED
n= 5: min_signers(threshold)=4, coeffs=4, on-chain requires count_ones-1=4 -> ACCEPTED
n= 6: min_signers(threshold)=5, coeffs=5, on-chain requires count_ones-1=5 -> ACCEPTED
n= 7: min_signers(threshold)=5, coeffs=5, on-chain requires count_ones-1=6 -> REJECTED (Round1PackageInvalidLength)
n= 8: min_signers(threshold)=6, coeffs=6, on-chain requires count_ones-1=7 -> REJECTED (Round1PackageInvalidLength)
n=10: min_signers(threshold)=7, coeffs=7, on-chain requires count_ones-1=9 -> REJECTED (Round1PackageInvalidLength)

```

A full integration test `poc_round1_package_rejected_with_seven_authorities` was also added to `pallets/exodus/src/tests.rs` and passes.

**Severity: High**

Reason: deterministic total liveness kill of the threshold-signing pipeline for any validator set outside {4,5,6}, with no attacker required. Not Critical because there is no fund theft or loss path, only denial of the DKG/signing service.

**Recommendation**

Replace the check with the same threshold the offchain worker uses, e.g. `estimated_commitment_count == get_byzantium_quorum_threshold(state.count_ones())` (or against total authorities). Add DKG tests with 7 and 10 validators so the two formulas are exercised where they disagree.

**Verification status**

VERIFIED. Mechanism confirmed line-by-line in source; PoC executed locally and passed.

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 3, 2026, 10:15pm UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99/2 "2026-10-03T22:15:02Z")

</div>

Thank you for your report!

This one duplicates another [report filed here](https://ghostforum.org/t/h16-dkg-round-1-rejects-all-honest-packages-unless-exactly-threshold-n-1-registered-round-0-deterministic-bridge-halt-for-n-7/123). I know you were the first to report it, but either way, please read the answer there to avoid a copy-paste from my side.

To summarize: this is not a bug as described, because the check effectively verifies only `checked_sub`. The coefficient-count comparison is never enforced - the expression `.map(...).ok_or(...)?` operates on `Option<Option<()>>`, so the inner `Option` produced by `.then(|| ())` is silently discarded. The gate only rejects when `count_ones == 0`.

If you think this issue could be elevated in other ways, please continue the thread.

---

<div class="post-metadata">

### Author: ![Rusdiwireng](https://ghostforum.org/letter_avatar_proxy/v4/letter/r/9de0a6/32.png) [@Rusdiwireng](https://ghostforum.org/u/Rusdiwireng)
#### Post date: [October 4, 2026, 3:53pm UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99/3 "2026-10-04T15:53:39Z")

</div>

You’re right, and I’m withdrawing this report.

Re-reading `register_round1_package` (lib.rs:991), the check is:

```rust
state.count_ones::<AuthIndex>().checked_sub(1)
    .map(|needed| estimated_commitment_count.eq(&needed).then(|| ()))
    .ok_or(Error::<T>::Round1PackageInvalidLength)?;

```

The `.map` over the `Option` from `checked_sub` produces `Option<Option<()>>`; `ok_or` turns it into `Result<Option<()>, _>`, and `?` silently discards the inner comparison. The coefficient-count equality is never enforced. The only rejection path is `count_ones == 0`, which cannot happen while a DKG session is running. Honest packages are accepted at any validator set size, so the deterministic brick I described does not exist.

My PoC replicated the check as I misread it (an enforced comparison) rather than as written, so the passing PoC proved my misreading, not the chain’s behavior. That’s on me.

As a code-hygiene note, not a finding: if the intent is to enforce the count, this wants a plain `ensure!` on the boolean; as written the gate is dead code. Withdrawing.

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 11, 2026, 5:35pm UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99/4 "2026-10-11T17:35:22Z")

</div>

Looks like it partially fixed [here](https://ghostforum.org/t/h16-dkg-round-1-rejects-all-honest-packages-unless-exactly-threshold-n-1-registered-round-0-deterministic-bridge-halt-for-n-7/123/10). At the same time @Chaplin slightly deviate from the issue described here. Can you guys confirm on is it the same issue or they are different?

---

<div class="post-metadata">

### Author: ![Chaplin](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/74df32/32.png) [@Chaplin](https://ghostforum.org/u/Chaplin)
#### Post date: [October 12, 2026, 12:59am UTC](https://ghostforum.org/t/dkg-permanently-stuck-for-validator-sets-outside-4-5-6-wrong-coefficient-count-check-in-register-round1-package/99/5 "2026-10-12T00:59:34Z")

</div>

They’re related but distinct — two failure modes of the same check in `register_round1_package`.

**#99** is about false negatives: the pre-fix check (`count_ones - 1`) disagrees with what the offchain worker actually puts in the package (`get_byzantium_quorum_threshold`), so every honest round-1 package is rejected with `Round1PackageInvalidLength` for n outside {4,5,6}. I reproduced this independently — the formulas only agree at n=4,5,6, and the worker’s coefficient count is `threshold(n)` by construction (`frost dkg::part1` with `min_signers`). DKG can never complete; that’s the liveness kill described in #99.

**H16 (#123)** was about the other direction — false positives. The same length check could be bypassed with trailing-byte smuggling, letting a malformed package through the gate. Different attacker model, different consequence.

Commit `653bf0848` replaces the check wholesale with `estimated_commitment_count == get_byzantium_quorum_threshold(max_signers)`, which is exactly the formula #99 recommended. That one change closes both: honest packages are accepted at every n (fixing #99), and the smuggling bypass no longer works (fixing H16’s round-1 vector).

So: same line of code, different bugs, one fix covers both.
