Witnesser rotation may queue call multiple times
Description
The witnesser pallet is responsible of keeping track of how many witnessers have observed an event as well as dispatching a call to act on the event once enough witnessers have witnessed it. This functionality is implemented by the witness_at_epoch
function.
The pallet can be paused in case of emergencies via the SafeMode pallet. If the pallet is paused, witness_at_epoch
does not dispatch the calls immediately. Instead, the calls are scheduled for execution by appending them to a queue (WitnessedCallsScheduledForDispatch
). The calls will be dispatched by on_idle
once the safe mode is disabled.
Normally, the same call cannot be executed twice; the pallet keeps track of which calls have executed in each previous valid epoch, and witness_at_epoch
does not dispatch the call if it has already executed. However, on_idle
does not perform the same check, so in principle the same call could be dispatched multiple times if it was added to the WitnessedCallsScheduledForDispatch
queue more than once.
Scenario 1
In a scenario where safe mode is enabled and a witnesser rotation is imminent, the same event could be observed by two different majority sets of validators in two different epochs and therefore be pushed to the queue twice.
Scenario 2
When a call is scheduled, it executes any time there is sufficient weight at the end of the block (on_idle
).
There is a force_witness
function that allows the root origin to bypass replay and multi-sig checks and execute a call immediately. This action uses the dispatch_call
function, which adds the call hash to the CallHashExecuted
pallet storage; but if a call is already scheduled, it is no longer subject to replay protections.
Impact
In an unlikely edge case scenario, the same call could be scheduled and eventually dispatched twice, duplicating the effects of an event.
Additionally, if a call were scheduled and the force_witness
function forced the call to be made, it would be replayed. Though the force_witness
function intends to allow calls to be replayed, the scheduled calls are not intended to be replayed. So, we believe this behavior is unintended.
Recommendations
Ensure the event was not already executed in on_idle
before dispatching it, similarly to how witness_at_epoch
performs the check.
Remediation
This issue has been acknowledged by Chainflip Labs, and a fix was implemented in commit 20641113↗.
The patch ensures that the on_idle
function also checks that the call has not been dispatched yet by checking CallHashExecuted
.