Skip to content
This repository has been archived by the owner on Mar 24, 2023. It is now read-only.

Commit

Permalink
Add delegation rewards and commission (#114)
Browse files Browse the repository at this point in the history
* Clean up text for reward distribution rationale a bit.
* Add commission and rewards for delegations.
* Remove validator staked balance in favor of only delegations.
* Refactor commission to levied before rewards are added to pending rewards instead of when unbonding delegation.
  • Loading branch information
adlerjohn committed Jan 27, 2021
1 parent e5df39c commit 665a77d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 35 deletions.
4 changes: 3 additions & 1 deletion rationale/distributing_rewards.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This forms the primary motivation of the scheme discussed here: a mechanism for

The scheme presented here is an incarnation of Cosmos' [F1 fee distribution scheme](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/fee_distribution/f1_fee_distr.pdf). F1 has the nice property of being approximation-free and, with proper implementation details, can be highly efficient with state usage and completely iteration-free in all cases.

Naively, when considering a single block, the reward that should be given to a delegator with stake $x$, who's delegating to a validator with total voting power $n$, whose reward in that block is $T$, is:
Naively, when considering a single block, the reward that should be given to a delegator with stake $x$, who is delegating to a validator with total voting power $n$, whose reward in that block is $T$, is:

$$
\text{naive reward} = x \frac{T}{n}
Expand All @@ -36,6 +36,8 @@ Entry_f = \begin{cases}
\end{cases}
$$

Note that $Entry$ is a monotonically increasing function.

Finally, the raw reward for a delegation is simply proportional to the difference in entries between the period where undelegation ended ($f$) and where it began ($k$).

$$
Expand Down
80 changes: 58 additions & 22 deletions specs/consensus.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ state.accounts[sender].nonce += 1

state.accounts[sender].balance -= totalCost(tx.amount, bytesPaid)
state.accounts[tx.to].balance += tx.amount
state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)

state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataPayForMessage
Expand All @@ -284,7 +285,7 @@ Apply the following to the state:
state.accounts[sender].nonce += 1
state.accounts[sender].balance -= totalCost(tx.amount, bytesPaid)

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataCreateValidator
Expand All @@ -298,7 +299,7 @@ The following checks must be `true`:
1. `tx.type` == [`TransactionType.CreateValidator`](./data_structures.md#signedtransactiondata).
1. `tx.fee.baseRateMax` >= `block.header.feeHeader.baseRate`.
1. `tx.fee.tipRateMax` >= `block.header.feeHeader.tipRate`.
1. `totalCost(tx.amount, bytesPaid)` <= `state.accounts[sender].balance`.
1. `totalCost(0, bytesPaid)` <= `state.accounts[sender].balance`.
1. `tx.nonce` == `state.accounts[sender].nonce + 1`.
1. `tx.commissionRate` <!--TODO check some bounds here-->
1. `state.accounts[sender].status` == `AccountStatus.None`.
Expand All @@ -307,14 +308,13 @@ Apply the following to the state:

```py
state.accounts[sender].nonce += 1
state.accounts[sender].balance -= totalCost(tx.amount, bytesPaid)
state.accounts[sender].balance -= totalCost(0, bytesPaid)
state.accounts[sender].status = AccountStatus.ValidatorQueued

validator = new Validator
validator.stakedBalance = tx.amount
validator.commissionRate = tx.commissionRate
validator.delegatedCount = 0
validator.votingPower = tx.amount
validator.votingPower = 0
validator.pendingRewards = 0
validator.latestEntry = PeriodEntry(0)
validator.unbondingHeight = 0
Expand All @@ -324,7 +324,7 @@ validatorQueueInsert(validator)

state.inactiveValidatorSet[sender] = validator

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataBeginUnbondingValidator
Expand Down Expand Up @@ -362,8 +362,9 @@ validatorQueueRemove(validator, sender)

state.inactiveValidatorSet[sender] = validator

state.activeValidatorSet.votingPower -= validator.votingPower
state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.activeVotingPower -= validator.votingPower

state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataUnbondValidator
Expand All @@ -388,20 +389,18 @@ Apply the following to the state:
validator = state.inactiveValidatorSet[sender]

state.accounts[sender].nonce += 1
state.accounts[sender].balance += validator.stakedBalance
state.accounts[sender].balance -= totalCost(0, bytesPaid)
state.accounts[sender].status = AccountStatus.ValidatorUnbonded

validator.votingPower -= validator.stakedBalance
validator.stakedBalance = 0
state.accounts[sender].balance += validator.commissionRewards

state.inactiveValidatorSet[sender] = validator

if validator.delegatedCount == 0
state.accounts[sender].status = AccountStatus.None
delete state.inactiveValidatorSet[sender]

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataCreateDelegation
Expand Down Expand Up @@ -455,9 +454,9 @@ if state.accounts[tx.to].status == AccountStatus.ValidatorQueued
state.inactiveValidatorSet[tx.to] = validator
else if state.accounts[tx.to].status == AccountStatus.ValidatorBonded
state.activeValidatorSet[tx.to] = validator
state.activeValidatorSet.votingPower += tx.amount
state.activeValidatorSet.activeVotingPower += tx.amount

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataBeginUnbondingDelegation
Expand Down Expand Up @@ -498,7 +497,7 @@ delegation.unbondingHeight = block.height + 1
validator.latestEntry += validator.pendingRewards // validator.votingPower
validator.pendingRewards = 0
validator.delegatedCount -= 1
validator.votingPower -= delegation.votingPower
validator.votingPower -= delegation.stakedBalance

# Update the validator in the linked list by first removing then inserting
# Only do this if the validator is actually in the queue (i.e. bonded or queued)
Expand All @@ -515,9 +514,9 @@ if state.accounts[delegation.validator].status == AccountStatus.ValidatorQueued
state.inactiveValidatorSet[delegation.validator] = validator
else if state.accounts[delegation.validator].status == AccountStatus.ValidatorBonded
state.activeValidatorSet[delegation.validator] = validator
state.activeValidatorSet.votingPower -= delegation.votingPower
state.activeValidatorSet.activeVotingPower -= delegation.stakedBalance

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataUnbondDelegation
Expand All @@ -542,11 +541,13 @@ Apply the following to the state:
delegation = state.accounts[sender].delegationInfo

state.accounts[sender].nonce += 1
state.accounts[sender].balance += delegation.stakedBalance
state.accounts[sender].balance -= totalCost(0, bytesPaid)
state.accounts[sender].status = None

# Return the delegated stake
state.accounts[sender].balance += delegation.stakedBalance
# Also disperse rewards (commission has already been levied)
state.accounts[sender].balance += delegation.stakedBalance * (delegation.endEntry - delegation.beginEntry)

if state.accounts[delegation.validator].status == AccountStatus.ValidatorQueued ||
state.accounts[delegation.validator].status == AccountStatus.ValidatorUnbonding
Expand All @@ -562,7 +563,7 @@ if validator.delegatedCount == 0 &&

delete state.accounts[sender].delegationInfo

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### SignedTransactionDataBurn
Expand All @@ -585,7 +586,7 @@ Apply the following to the state:
state.accounts[sender].nonce += 1
state.accounts[sender].balance -= totalCost(tx.amount, bytesPaid)

state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid)
state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid)
```

#### Begin Block
Expand All @@ -595,12 +596,47 @@ At the beginning of the block, rewards are distributed to the block proposer.
Apply the following to the state:

```py
proposer = state.activeValidatorSet[block.header.proposerAddress]

rewardFactor = (TARGET_ANNUAL_ISSUANCE * BLOCK_TIME) / (SECONDS_PER_YEAR * sqrt(GENESIS_COIN_COUNT))
state.activeValidatorSet[block.header.proposerAddress].pendingRewards += rewardFactor * sqrt(state.activeValidatorSet.votingPower)
blockReward = rewardFactor * sqrt(state.activeValidatorSet.activeVotingPower)
state.activeValidatorSet.proposerBlockReward = blockReward

state.activeValidatorSet[block.header.proposerAddress] = proposer
```

#### End Block

```py
account = state.accounts[block.header.proposerAddress]

if account.status == AccountStatus.ValidatorUnbonding
account.status == AccountStatus.ValidatorUnbonded
proposer = state.inactiveValidatorSet[block.header.proposerAddress]
else if account.status == AccountStatus.ValidatorBonded
proposer = state.activeValidatorSet[block.header.proposerAddress]

blockReward = state.activeValidatorSet.proposerBlockReward
commissionReward = proposer.commission * blockReward
proposer.commissionRewards += commissionReward
proposer.pendingRewards += blockReward - commissionReward

# Even though the voting power hasn't changed yet, we consider this a period change.
# Note: this isn't perfect because the block reward is shared with delegations
# _through_ this block rather than up to the beginning of this block.
proposer.latestEntry += proposer.pendingRewards // proposer.votingPower
proposer.pendingRewards = 0

proposer.votingPower += blockReward
state.activeValidatorSet.activeVotingPower += blockReward

if account.status == AccountStatus.ValidatorUnbonding
account.status == AccountStatus.ValidatorUnbonded
state.inactiveValidatorSet[block.header.proposerAddress] = proposer
else if account.status == AccountStatus.ValidatorBonded
state.activeValidatorSet[block.header.proposerAddress] = proposer
```

At the end of a block, the top `MAX_VALIDATORS` validators by voting power are or become active (bonded). For newly-bonded validators, the entire validator object is moved to the active validators subtree and their status is changed to bonded. For previously-bonded validators that are no longer in the top `MAX_VALIDATORS` validators begin unbonding.

Bonding validators is simply setting their status to `AccountStatus.ValidatorBonded`. The logic for validator unbonding is found [here](#signedtransactiondatabeginunbondingvalidator), minus transaction sender updates (nonce, balance, and fee).
Expand Down
33 changes: 21 additions & 12 deletions specs/data_structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Data Structures
- [Validator](#validator)
- [ActiveValidatorCount](#activevalidatorcount)
- [ActiveVotingPower](#activevotingpower)
- [ProposerBlockReward](#proposerblockreward)
- [ValidatorQueueHead](#validatorqueuehead)
- [PeriodEntry](#periodentry)
- [Decimal](#decimal)
Expand Down Expand Up @@ -830,18 +831,18 @@ In the delegation subtree, delegations are keyed by the [hash](#hashdigest) of t

### Validator

| name | type | description |
| ----------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
| `stakedBalance` | [VotingPower](#type-aliases) | Validator's personal staked balance, in `4u`. |
| `commissionRate` | [Decimal](#decimal) | Commission rate. |
| `delegatedCount` | `uint32` | Number of accounts delegating to this validator. |
| `votingPower` | [VotingPower](#type-aliases) | Total voting power as staked balance + delegated stake, in `4u`. |
| `pendingRewards` | [Amount](#type-aliases) | Rewards collected so far this period, in `1u`. |
| `latestEntry` | [PeriodEntry](#periodentry) | Latest entry, used for calculating reward distribution. |
| `unbondingHeight` | [Height](#type-aliases) | Block height validator began unbonding. |
| `isSlashed` | `bool` | If this validator has been slashed or not. |
| `slashRate` | [Decimal](#decimal) | _Optional_, only if `isSlashed` is set. Rate at which this validator has been slashed. |
| `next` | [Address](#type-aliases) | Next validator in the queue. Zero if this validator is not in the queue. |
| name | type | description |
| ------------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
| `commissionRewards` | `uint64` | Validator's commission rewards, in `1u`. |
| `commissionRate` | [Decimal](#decimal) | Commission rate. |
| `delegatedCount` | `uint32` | Number of accounts delegating to this validator. |
| `votingPower` | [VotingPower](#type-aliases) | Total voting power as staked balance + delegated stake, in `4u`. |
| `pendingRewards` | [Amount](#type-aliases) | Rewards collected so far this period, in `1u`. |
| `latestEntry` | [PeriodEntry](#periodentry) | Latest entry, used for calculating reward distribution. |
| `unbondingHeight` | [Height](#type-aliases) | Block height validator began unbonding. |
| `isSlashed` | `bool` | If this validator has been slashed or not. |
| `slashRate` | [Decimal](#decimal) | _Optional_, only if `isSlashed` is set. Rate at which this validator has been slashed. |
| `next` | [Address](#type-aliases) | Next validator in the queue. Zero if this validator is not in the queue. |

Validator objects represent all the information needed to be keep track of a validator.

Expand All @@ -865,6 +866,14 @@ Since the [active validator set](#validator) is stored in a [Sparse Merkle Tree]

Since the [active validator set](#validator) is stored in a [Sparse Merkle Tree](#sparse-merkle-tree), there is no compact way of proving the active voting power. The active voting power is stored in the active validators subtree, and is keyed with `1` (i.e. `0x0000000000000000000000000000000000000000000000000000000000000001`), with the first byte replaced with `ACTIVE_VALIDATORS_SUBTREE_ID`.

### ProposerBlockReward

| name | type | description |
| -------- | -------- | ------------------------------------------------------------------------------ |
| `reward` | `uint64` | Total block reward (subsidy + fees) in current block so far. Reset each block. |

The current block reward for the proposer is kept track of here. This is keyed with `2` (i.e. `0x0000000000000000000000000000000000000000000000000000000000000002`), with the first byte replaced with `ACTIVE_VALIDATORS_SUBTREE_ID`.

### ValidatorQueueHead

| name | type | description |
Expand Down

0 comments on commit 665a77d

Please sign in to comment.