- [gh issue](https://github.com/paritytech/substrate/issues/11965) - [PR](https://github.com/paritytech/substrate/pull/14328) Currently, we calculate and update the `MinimumUntrustedScore` manually by calling [`Call::set_untrusted_min_score`](https://github.com/paritytech/substrate/blob/master/frame/election-provider-multi-phase/src/lib.rs#L955). The `MinimumUntrustedScore` value is calculated by fetching the last 30 election scores and halving their average. This approach requires developers to keep an eye on how the election scores are evolving with time and updating the `MinimumUnstrustedScore` requires considerable overhead with governance proposals and manual work. In addition to that, values that are lagging behind the previous may open attack vectors for attacks. We propose an on-chain mechanism that dynamically updates `MinimumUntrustedScore` using the same approach as currently (halving the average of previous election's scores). ### Risks The `MinimumUntrustedScore` is used in one of the `feasibility_check` checks to ensure that the sinal solution is strictly better than the untrusted minimum score. - **Signed Phase**: if a queued solution is not stricly better than `MinimumUntrustedScore`, the solution is discarded and the submitter is slashed. - **Unsigned Phase**: `feasibility_check` is called in the `submitted_unsigned` inherent and it will panic if the checks fail, thus the solution will not be added to the queue if the score is not better than `MinimumUnstrustedScore` At the extreme, if no miner/validator find a solution with a better score than `MinimumUntrustedScore`, the election will fail and enter in emergency mode (the previous active set of validators will remain in the set until the emergency mode is recovered through governance). The `MinimumUntrustedScore` should be so that it's not too high relative to the current stake/nominations state, so that miners are able to find a solution that passes the feasibility check. However, keeping `MinimumUnstrustedScore` too low will allow miners to submit solutions that are sub-optimal given the current stakes and nominations. The current approach is sub-optimal because updating the `MinimumUntrustedScore` requires manual checking and governance proposals. And given the risks explained above,it would be ideal to have a reasonable (perhaps conservative) mechanism which would update the `MinimumUntrustedScore` on-chain given the state of the staking system and/or the past election scores. ### Implementation #### 1. On-chain rolling average At the end of a successful election, we update `MinimumUnstrustedScore` for the next election: `new_min_score = (previous_min_score + (new_score/2)) / 2` ```rust= /// Updates the `MinimumUntrustedScore` by calculating the rolling half average of the /// election scores. /// /// Should be called at the end of every successful election. pub fn update_min_untrusted_score(score: ElectionScore) { let updated_score = (<MinimumUntrustedScore<Runtime>>::get() .saturating_add(score.saturating_div(2))) .saturating_div(2); let minmum_score_backstop = MinimumUntrustedScoreBackstop<Runtime>::get().unwrap_or(0); if updated_score < minmum_score_backstop { Self::deposit_event(Event::LowMinimumUntrustedScore { attempted: updated_score, }); }; // updates `MinimumUntrustedScore`, making sure it does not go below // `MinimumUntrustedScoreBackstop`. <MinimumUntrustedScore<Runtime>>::set( updated_score.max(minmum_score_backstop) ); } ``` #### 2. Add `MinimumUnstrustedScoreBackstop` configuration `MinimumUntrustedScoreBackstop` ensures that the `MinimumUntrustedScore` does not go below a configured threshold. #### 3. Disable auto-update We should have a way to disable the auto update of the `MinimunUnstrustedScore` on-chain, for emergency cases. #### 4. On-chain costs The on-chain overhead is 2x reads and 1x write extra compared to the current implementation at every finalized election (i.e. once every ~14K blocks in Polkadot). #### 5. Notes - `Event::LowMinimumUntrustedScore` will be triggered everytime the new calculatede score is below the configured `MinimumUntrustedScoreBackstop`; - `Call::set_untrusted_min_score` is still available to update the unstrusted score manually thorugh governance. --- ### Comments/notes - [ ] if the rolling base average period is calculated based on the `EraIndex`, we need to add dependencies of `sp_staking` in EPM.