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:
// 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:
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.