3

type SignedRewardBase: Get<BalanceOf<Self>> Base reward for a signed solution

type SignedDepositBase: Get<BalanceOf<Self>> Base deposit for a signed solution.

type SignedDepositByte: Get<BalanceOf<Self>> Per-byte deposit for a signed solution.

Are some configuration items from ElectionProviderMultiPhase. What are these rewards and deposits aimed at? Who is depositing/getting the rewards? Are nodes that are tied to validators running the election algorithm getting the rewards, which in most cases is a stash account? Does submitting a solution incur a cost as well?

2 Answers 2

4

tl;dr The Config types in your question are configuration parameters for the deposit of submitting a signed solution and reward for a signed solution accepted at the end of the signed phase.

Context

These are configurations for submitting solutions during the signed phase. For the canonical explanation of the phases see the module docs.

For quick a background though, the 3 phases supported by the election provider multi phase pallet are:

  • Off Phase: Nothing is happening. At the threshold of the off and signed phase is when the election snapshot is taken. The off phase starts when the unsigned phase ends.
  • Signed Phase: Anyone can submit a solution via the submit extrinsic. When a user submits a solution, some funds are reserved as a deposit. More on this below.
  • Unsigned Phase: Block authors can submit an inherent with a solution mined by the offchain worker.

At the end of the signed phase the submitted solution with the best score is checked to ensure the claimed score matches the actual score. If it does we save it as the queued solution and reward the submitter, but if it does not we discard the solution, slash the submitters deposit, and repeat the process for the next best solution (see here for high level docs). When a solution is determined to have a correct score, the submitter is refunded the extrinsic fee, and the deposits based on SignedDepositBase, SignedDepositByte and a reward based on SignedRewardBase - more on this below.

Here is an example of the flow for checking solutions at the end of the signed phase (see module docs here)

+-------------------------------+
|Solution(score=20, valid=false)| +-->  Slashed
+-------------------------------+
|Solution(score=15, valid=true )| +-->  Rewarded, Saved
+-------------------------------+
|Solution(score=10, valid=true )| +-->  Discarded
+-------------------------------+
|Solution(score=05, valid=false)| +-->  Discarded
+-------------------------------+
|             None              |
+-------------------------------+

Answer

The values are for configuring the deposit of submitting a solution and the reward for the highest scored solution at the end fo the signed phase.

Both values are calculated from within Call::submit.

The deposit is calculated by deposit_for, using SignedDepositByte and SignedDepositBase. SignedDepositByte is multiplied by the length of the encoded solution to create 1 o 3 components for the deposit. SignedDepositBase is a static component of the deposit that is added to the other deposit components to create the final deposit. (Note the estimated weight for the feasibility check of the solution is also multiplied by SignedDepositWeight to create third deposit component). This deposit is then reserved here. Below is the exact logic:

    /// Collect a sufficient deposit to store this solution.
    ///
    /// The deposit is composed of 3 main elements:
    ///
    /// 1. base deposit, fixed for all submissions.
    /// 2. a per-byte deposit, for renting the state usage.
    /// 3. a per-weight deposit, for the potential weight usage in an upcoming on_initialize
    pub fn deposit_for(
        raw_solution: &RawSolution<SolutionOf<T>>,
        size: SolutionOrSnapshotSize,
    ) -> BalanceOf<T> {
        let encoded_len: u32 = raw_solution.encoded_size().saturated_into();
        let encoded_len: BalanceOf<T> = encoded_len.into();
        let feasibility_weight = Self::feasibility_weight_of(raw_solution, size);

        let len_deposit = T::SignedDepositByte::get().saturating_mul(encoded_len);
        let weight_deposit =
            T::SignedDepositWeight::get().saturating_mul(feasibility_weight.saturated_into());

        T::SignedDepositBase::get()
            .saturating_add(len_deposit)
            .saturating_add(weight_deposit)
    }
}

source

The reward is calculated by estimating the fee the user paid to submit the solution and adding that to the SignedRewardBase. In practical terms, at the end of the signed phase, if the user submitted a wining solution, the cost of submission is reimbursed and they are given an award of SignedRewardBase. The exact logic is below:

  let reward = {
        let call =
            Call::submit { raw_solution: raw_solution.clone(), num_signed_submissions };
        let call_fee = T::EstimateCallFee::estimate_call_fee(&call, None.into());
        T::SignedRewardBase::get().saturating_add(call_fee)
    };

source

Both the reward and deposit are stored with the submitted solution. Lets look at the logic for a when a winning solution is accepted to see how the reward and deposit are used:

    /// Helper function for the case where a solution is accepted in the signed phase.
    ///
    /// Extracted to facilitate with weight calculation.
    ///
    /// Infallible
    pub fn finalize_signed_phase_accept_solution(
        ready_solution: ReadySolution<T::AccountId>,
        who: &T::AccountId,
        deposit: BalanceOf<T>,
        reward: BalanceOf<T>,
    ) {
        // write this ready solution.
        <QueuedSolution<T>>::put(ready_solution);

        // emit reward event
        Self::deposit_event(crate::Event::Rewarded { account: who.clone(), value: reward });

        // unreserve deposit.
        let _remaining = T::Currency::unreserve(who, deposit);
        debug_assert!(_remaining.is_zero());

        // Reward.
        let positive_imbalance = T::Currency::deposit_creating(who, reward);
        T::RewardHandler::on_unbalanced(positive_imbalance);
    }

source

The deposit is unreserved (it was originally reserved in Call:submit) and the reward is deposited into the submitters account. Note that in polkadot the RewardHandler is a default impl, thus the rewards are “minted”, increasing the total issuance.

Finally, lets take a look at what happens when a solution is rejected:

    /// Helper function for the case where a solution is accepted in the rejected phase.
    ///
    /// Extracted to facilitate with weight calculation.
    ///
    /// Infallible
    pub fn finalize_signed_phase_reject_solution(who: &T::AccountId, deposit: BalanceOf<T>) {
        Self::deposit_event(crate::Event::Slashed { account: who.clone(), value: deposit });
        let (negative_imbalance, _remaining) = T::Currency::slash_reserved(who, deposit);
        debug_assert!(_remaining.is_zero());
        T::SlashHandler::on_unbalanced(negative_imbalance);
    }

source

The deposit is slashed from the users reserved funds. Note that in polkadot the SlashHandler is a default impl, thus the slashes are “burned”, decreasing the total issuance.

Pulling it all together

The one missing piece is: how does one submit a signed solution? Parity has been working on a staking election solution miner. At the time of writing it lives here, in the polkadot repo. In theory this can be made generic for any chain that uses the staking and election provider multi pallets, but it was setup in the polkadot repo to have easy access to the concrete runtime types. It is still in development but in the future Parity hopes to see many miner implementations that compete on speed and, more importantly, the quality of the solution (which can be done by implementing different algorithms).

1
  • I appreciate the depth you provide. However I am struggling with another issue, namely this, for days: substrate.stackexchange.com/questions/630/…. My intuition tells me that no one is submitting a signed solution here, even though I've set my Fallback to OnChainSequentialPhragmen, it still gives me those errors on era change. I've made sure to run my nodes with --offchain-workers always but that doesn't seem to help. What should I do? Commented Mar 7, 2022 at 13:12
1

The docs on this pallet may help. As well as reading the signed phase source to see the mechanics more in detail.

The sumbit callable is open to anyone to use, so long as they pass the typical let who = ensure_signed(origin)?; check and conply with the deposit mechanisms imposed, and like all callables, there is a weight and thus a fee that is paid to call it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.