Skip to main content

Decentralized Court

Zeitgeist implements a decentralized court to handle disputes that may arise in the resolution of prediction markets outcomes.

The court system is responsible for ensuring that accurate information is added to the blockchain. Prediction markets, which rely on truthful data, reward traders who base their decisions on accurate information. If someone provides false information, they will be punished, while those who share accurate information will be rewarded. In essence, the court is a stake-weighted plurality decision-making machine. Zeitgeist's court makes use of the so-called Schelling point. This is achieved by voting in secret and revealing the raw vote information later. The outcome with the most votes wins (plurality). The court serves as a dispute resolution mechanism. The court is inspired by Kleros, a project that has already experimented with an on-chain court system.

How to participate?

Anyone can participate by joining the court system as a juror or delegator. As a juror, you are responsible for supplying the truthful outcome of a prediction market by voting and revealing the raw vote information. As a delegator, you can delegate your voting rights to active jurors.

What is the process of the court system?

Suppose the oracle submitted a wrong outcome for a prediction market. Anyone can bring the market into the Disputed state. At this point the court takes over. At a known request block in the future, the Vote Phase begins. During the vote phase the jurors cast their votes as encrypted hashes. After the vote phase ends, during the Aggregation Phase, the jurors publicly reveal their votes. The raw vote information can be matched to the encrypted hash. If no one appeals during the Appeal Phase, the outcome with the most votes wins. If someone appeals, the court schedules a new vote phase. This can be repeated up to three times. At the forth appeal, the global dispute system takes over. The global dispute system is a token voting mechanism that involves all ZTG holders to resolve the market on the most voted outcome.

Court Phases

Details

Dispute Management within Court

If the oracle submits the wrong outcome, disputes come into play. Anyone can dispute the oracle report, and once a dispute is triggered, the court takes over. The court is comprised of jurors and delegators who need to lock a certain amount of ZTG tokens to join the court system. The more tokens locked, the higher the probability of being selected as an active juror or delegator, who risks funds on behalf of delegated jurors. Delegators can transfer their voting rights to active jurors, who participate in the voting system. The court uses a plurality voting system, meaning the outcome with the most votes wins. Each market is associated with one court case, which can be appealed multiple times.

Global Disputes as the Last Instance

If the number of appeals reaches a certain threshold (currently three appeals) or if during the appeal period (or dispute_duration) the total unconsumed stake (see “The Selection Algorithm”) is below the necessary draw weights (see “Calculating necessary draw weights”), the global dispute system can be initiated by anyone. This global dispute system allows all ZTG token holders to lock funds based on their beliefs. The outcome with the most locked balance wins and serves as the final outcome for traders to rely on for redemption. Global dispute voting participants have all of their funds unlocked after the winner is determined.

Joining the Court

You can either join the court (extrinsic join_court) as an active juror, who is responsible for voting, or be a delegator and delegate (extrinsic delegate) funds to active jurors. It is important to note that the court pool, which contains all jurors and delegators, is bounded in size (config parameter MaxCourtParticipants). If the court pool is full, the lowest-staked participant is about to be replaced by a new and higher-staked participant (juror or delegator) account id. For this reason, the court pool is sorted by the staked amount of each participant. This comes in handy in order to use binary searches for items inside the court pool. Court participants can increase their stake at any time by calling join_court or delegate with a higher amount than the previous call to these functions. To decrease the stake, it’s necessary to exit the court (see ”Exiting court”). To update the pool item associated to a court participant, there are two binary searches, the first for finding the pool item (the search key is a tuple of stake and the participant account id) and with the previous data and the second for inserting the updated data inside the pool again.

For the delegate extrinsic, the function argument delegations contains a list of account ids. The dispatch function ensures that the list of delegations contains actually account ids which are actively participating jurors. The list needs to contain at least one account and all account ids need to be unique.

Exiting the Court

Each juror and delegator can exit the court system to retrieve their remaining funds. If the juror or delegator is still actively involved in inner court cases, only the unused (non-active) funds are returned. The unused stake is equivalent to the stake that was not already selected by the selection algorithm (see “The Selection Algorithm”). In order to exit the court and get the funds back as a juror or delegator, one has to call the prepare_exit_court extrinsic. This extrinsic removes the participant from the court pool and saves the current block number to notice when the unused funds can be returned.

With the introduction of the inflation period it is required to restrict court hopping. It is possible that users enter the court whenever the rewards of the inflationary system get spend (see chapter “Incentives”). Thus, we put a locking period of InflationPeriod in place in the case that a juror wants to exit the court. So after a participant requested to leave the court system (extrinsic prepare_exit_court), the participant has to wait at least another inflation period to get the staked funds back. To finally return the unused funds, the participant needs to invoke the exit_court dispatch function. The used funds (active_lock) still remain locked.

Calculating Necessary Draw Weights

If jurors vote against the plurality decision, they are penalized by a multiple of a constant amount (n * MinJurorStake). The penalized amount is rewarded proportionally to the jurors and delegators who backed the most-voted outcome. The amount of penalty risk depends on the juror's overall stake. When a court case is triggered, the court requests a small multiple of the constant amount (currently 31 * MinJurorStake) from the total stake of all jurors and delegators randomly. The current configuration of MinJurorStake is 500 ZTG. So, 31 * 500 ZTG = 15,500 ZTG are randomly drawn from the total stake for the first court round.

The formula to determine the necessary requested vote weight for each appeal is as follows:

2^(appeal_number) * 31 + 2^(appeal_number) - 1

Assume one specific court case is in the last appeal round (3). The number of randomly selected draw weights for jurors and delegators is 255, and, therefore, the amount of requested ZTG is 127,500 ZTG.

2331+231=255.2^3 \cdot 31 + 2^3 - 1 = 255.
255500ZTG=127, ⁣500ZTG.255 \cdot 500 \, \mathrm{ZTG} = 127,\!500 \, \mathrm{ZTG}.

The Selection Algorithm

Here is the code reference to the selection algorithm.

The court pool keeps track of all the stake of the jurors and delegators to randomly select n * MinJurorStake draw weights from it. It is important to note that if some of the juror’s or delegator’s stake was previously already selected, the rule of drawing without replacement is followed. This is accomplished by saving the consumed_stake to the pool item’s storage. For new draws the unconsumed stake is calculated by the total stake subtracted by the consumed_stake. The higher the unconsumed stake, the higher the probability to get selected by the algorithm. In addition, if the unconsumed stake is not exactly divisible by MinJurorStake, it is rounded down (unconsumed = unconsumed - (unconsumed % MinJurorStake).

The active_lock is essentially equivalent to the unconsumed stake, but restricts the court participant to return the funds behind this active lock. Hence, the active_lock is not defined inside the court pool, but individually for each participant (storage map Participants).

To randomly draw n numbers without replacement, a partial version of the Fisher-Yates shuffle algorithm (function get_n_random_section_ends) is used. The unconsumed total stake of all jurors and delegators is divided by MinJurorStake to get the sections_len. The result are n random numbers between 1 and sections_len, which get multiplied by MinJurorStake to receive a random subset of all section ends for the cumulated juror’s and delegator’s stakes. That’s why cumulative_section_ends exists. It adds up all the unconsumed stake of the court participants, saves for each participant the cumulative section end, and the generated random subset can be matched to each associated account id (juror or delegator). One randomly selected draw weight is equal to one MinJurorStake and associated to one juror or delegator account id.

Delegation of Draw Weight

If one draw weight of a delegator is selected by the algorithm (see ”The selection algorithm”), one random delegated juror is chosen out of the delegations list. This delegations list was specified inside the call argument of the extrinsic delegate by the delegator. There is one edge case to note here. At the point of calling delegate the delegations list is checked, whether the specified account ids are actually valid jurors of the court system. If not, the delegate extrinsic fails. But at the time of the selection process, the delegations list could contain invalid account ids, which don’t represent proper jurors anymore (for example the juror exited the court system). For this reason only the actual contained jurors are taken into consideration of all delegations. If there are no valid jurors inside the delegations list, the delegator is removed from the court pool (error inside the code: SelectionError::NoValidDelegatedJuror).

For delegations a vote weight (code reference: SelectionAdd::DelegationWeight) goes to a random and valid juror from the delegations list, but the delegator risks (slashable and code reference SelectionAdd::DelegationStake) the MinJurorStake associated to the vote weight. If the juror makes bad decisions, the delegator loses the selected MinJurorStake.

Voting at predefined Time Points

Jurors are requested to vote in a periodic interval (RequestInterval) at a known request block in the future. This ensures that jurors only need to check at predefined times if they need to take action. If there wasn’t this concept of predefined requests, the jurors would have needed to check in a much smaller time interval if they are selected in court cases.

Commitment Voting

The voting system uses a commit-reveal scheme, which is required to prevent jurors from simply voting for the obvious plurality decision. There are voting, aggregation, and appeal phases. During the voting phase, jurors cast their votes as encrypted hashes (extrinsic vote), which must later be revealed (extrinsic reveal_vote) as raw information. If a juror fails to vote or reveal their vote, they lose their stake for that specific court case. The encrypted hash consists of a BlakeTwo256 hash of the juror account, the vote item (outcome), and a salt. The salt is a hash derived from the juror's signature of the specific court ID. Without a salt, a malicious actor could try every vote item to obtain raw information. If a juror's salt is known before the voting phase ends, they could be exposed and penalized (extrinsic denounce_vote) by those aware of the salt. This adds a layer of protection against cheating and increases trust in the system.

Appeals

After the aggregation phase ends and all jurors have had the chance to make their votes public, anyone can appeal the plurality decision (during the appeal period). This triggers a new court round with more requested court participants stakes or (in case after the last round) allows a global dispute to take over. If nobody appeals, the court resolves based on the plurality decision of the last court round. Finally, the losers must pay the winners proportionally to their selected stakes for the specific court case (extrinsic reassign_court_stakes).

In order to make an appeal, the caller of appeal has to reserve a bond. The cost of an appeal is calculated as following: cost of appeal = AppealBond * 2^(appeal_number + 1).

  1. First appeal cost: 2000 ZTG (AppealBond) * 2^1 = 4000 ZTG
  2. Second appeal cost: 2000 ZTG (AppealBond) * 2^2 = 8000 ZTG
  3. Third appeal cost: 2000 ZTG (AppealBond) + 2^3 = 16000 ZTG

At the end of the appeal period and if there are no further appeals, all accounts which provided appeal bonds and didn’t appeal on the winner outcome, get their funds back. In this case, if the appealed outcome is not equal to the winner outcome, the appeal was justified. Otherwise the appeal bond is slashed and given to the treasury. This punishes the malicious behaviour that someone appeals the correct outcome.

The last possible call to appeal is necessary for the global dispute to get triggered, because there has to be some kind of financial commitment to appeal the winner outcome of the last appeal round.

Determining the Winner Outcome

The basic concept is to get the outcome with the most juror vote weights and use that as winner outcome. But there are two edge cases with that approach. If there are no jurors who actually revealed the raw vote information, because of different reasons like inactivity, denounces or other reasons, the court uses the oracle report as the winner outcome. The second edge case is that there are more than two outcomes, which received the same amount of votes. In this case the court resolves on the winner outcome of the last appeal round. If there was no previous appeal round, the court resolves on the oracle report again.

Incentives

By making good decisions, you can be rewarded by those who lose. This is done by the extrinsic reassign_court_stakes. At the end of the appeal phase, the court resolves on a winning outcome (see ”Determining the winner outcome”). This winner outcome is then compared to the jurors voted outcomes. All jurors and delegators who sided with a different outcome to the winner outcome get slashed according to their draw weights. All jurors who failed to vote or failed to reveal the encrypted vote or got denounced, as well as their delegators, get also slashed according to their draw weights. All jurors and their delegators, who sided with the winner outcome get the previously mentioned slashed funds proportional to their share of all the other winner stake (total_winner_stake).

Additionally, the court system is incentivized by inflation. Participants who stake funds in the court receive newly minted tokens proportional to their stake. The current configuration involves a yearly inflation rate of 2% for all jurors and delegators in the court system. Inflation is applied at regular intervals, known as InflationPeriod, to reduce the strain on the blockchain. To prevent users from joining the court just to receive token emissions (court hopping), they must remain in the court for at least one full InflationPeriod before they can receive inflation rewards. The block number at the point of a participant joining the court system is saved as joined_at in the court pool item. Assume now is the current block number, the inflation is rewarded to a participant if the following condition is true: now - joined_at ≥ InflationPeriod.

The YearlyInflation can be configured by a MonetaryGovernanceOrigin using the extrinsic set_inflation.

The reward per participant is calculated as the following:

YearlyInflationAmount = YearlyInflation * TotalIssuance
IssuePerBlock = YearlyInflationAmount / BlocksPerYear
InflationPeriodMint = IssuePerBlock * InflationPeriod

for (Participant, ParticipantStake) in CourtPool {
Share = ParticipantStake / ParticipantsTotalStake;
MintAmount = Share * InflationPeriodMint;
deposit(Participant, MintAmount)
}

Terminology

  • Court Hopping: Joining the court just to receive token emission benefits and then exiting the court.
  • Court Pool: All jurors and delegators.
  • Delegator: An account which gives its vote power to jurors.
  • Draw Weight: One slash-able MinJurorStake that belongs to one vote weight.
  • Global Dispute: A token voting mechanism for all ZTG holders.
  • Juror: An account which is responsible to vote in secret and reveal the raw information later on.
  • Participant: A juror or delegator account inside the court pool.
  • Vote Weight: The derivative voting value of one draw weight; it represents one MinJurorStake.