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

Add delegation rewards and commission #114

Merged
merged 5 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit here?

Copy link
Member Author

@adlerjohn adlerjohn Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the refactor of removing staked balance for validators:

Remove stakedBalance for validators. Validators now have no balance and need to delegate to themselves. This can be abstracted away at the UI layer.

(Note that this does introduce a divide-by-zero case when computing entries, but I'll take care of that in a future PR, issue in #115.)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec assumes these computations will be done every block. Why not epoch it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is because this is how it works in the SDK currently. While we want epochs on the long run I'm against putting more effort into this now. It is "just" an optimization (yeah, a really cool one but I think there are higher prio ones).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cosmos/cosmos-sdk#8328 😉 Should land in the sdk before launch. May be worth leaving a note that epochs are preferred.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for bringing this to our attention.

Copy link
Member Author

@adlerjohn adlerjohn Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only real benefit to epochs for our usecase is reducing light client overhead since fewer block headers need to be downloaded. It'll probably be important long-term, but it's just an optimization and we can worry about those later.

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
```

adlerjohn marked this conversation as resolved.
Show resolved Hide resolved
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