Cross-generation DKG package replay: signed payload does not bind dkg_index

Component: pallets/exodus @ 0162da46, src/types/packages.rs:105-112 (PackageContext holds only authority_index + network_curve), src/lib.rs:901-950 (register_round0_package), src/lib.rs:2669-2700 (validate_round0_package_size), src/lib.rs:3760-3766 (offchain skip), src/types/dkg.rs:236-239 (restart increments the index).

Description: Every sr25519-signed DkgPackage (Rounds 0-6) binds only (authority_index, network_curve), with no dkg_index/generation. After restart_dkg()/new_dkg (phase returns to Round0, bitmap nullified), anyone (permissionless, unsigned tx, Pays::No) can replay a stale register_round0_package extrinsic from validator V from the previous generation: all checks pass (verify on verbatim replayed bytes is still valid). Consequences: (1) Round0Packages[V] is filled with a stale hash and the bitmap marks V as registered; (2) V’s real round-1 package is rejected (Round1PackageBadHash, lib.rs:978-980); (3) V’s own offchain worker skips submission because Round0Packages::contains_key (lib.rs:3760-3766): the victim is unaware and does not retry. A “franken-DKG” variant (also replaying a stale Round1 package so old key material mixes into the new group key) is HYPOTHESIS (needs further review).

Impact: Selective censorship/DoS of a validator’s DKG participation; if more than n - threshold validators are censored, DKG misses quorum and loops on restart. Permissionless and cheap.

Reproduction: Static reproduction: (1) capture validator V’s register_round0_package extrinsic from generation N; (2) wait for generation N+1 to enter the Round0 phase; (3) re-submit as an unsigned tx: it is accepted. Full code rationale above; executable PoC not yet run.

Severity: Medium. Reason: the mechanism is fully present in code and cheap, but taking down DKG entirely needs noisy mass censorship; single censorship is only degradation. Not High (no proven fund loss).

Recommendation: Bind dkg_index into the signed payload (add it to PackageContext or to each package) and reject packages whose dkg_index differs from QualificationDkgState.get_dkg_index() during validation.

Verification: VERIFIED (static analysis).

Thank you for your report!

That’s actually make sense! You are right here, the fix is understandable and pretty easy. IMHO, this thread deserves elevation. If you are interested in continuing to reserach other possible inconcistencies for the protocol you are welcome!

Hey @Rusdiwireng - it looks like the dev team has fixed the issue you reported. Could you confirm whether it’s resolved on your end now, or continue the thread if it still persists?

Since each message is now bounded to dkg_index, we decided to remove the ExodusActivity mechanism - which was originally designed to fill exactly the gap you highlighted in your report, but ultimately didn’t do so successfully.

Checked 74d74f4. PackageContext now carries dkg_index, covered by the sr25519 signature over the SCALE-encoded package, and each round validates package_dkg_index against the current generation with BadProof on mismatch. Cross-generation replay is rejected at both layers, and the mempool uniqueness tuple now includes dkg_index. The ExodusActivity removal matches what you described. Confirmed resolved.