diff --git a/rationale/distributing_rewards.md b/rationale/distributing_rewards.md index a39e513..19c30c2 100644 --- a/rationale/distributing_rewards.md +++ b/rationale/distributing_rewards.md @@ -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} @@ -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$). $$ diff --git a/specs/consensus.md b/specs/consensus.md index a7b4647..f527b0a 100644 --- a/specs/consensus.md +++ b/specs/consensus.md @@ -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 @@ -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 @@ -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` 1. `state.accounts[sender].status` == `AccountStatus.None`. @@ -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 @@ -324,7 +324,7 @@ validatorQueueInsert(validator) state.inactiveValidatorSet[sender] = validator -state.activeValidatorSet[block.header.proposerAddress].pendingRewards += tipCost(bytesPaid) +state.activeValidatorSet.proposerBlockReward += tipCost(bytesPaid) ``` #### SignedTransactionDataBeginUnbondingValidator @@ -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 @@ -388,12 +389,10 @@ 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 @@ -401,7 +400,7 @@ 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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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). diff --git a/specs/data_structures.md b/specs/data_structures.md index b6ff61a..c94edc2 100644 --- a/specs/data_structures.md +++ b/specs/data_structures.md @@ -64,6 +64,7 @@ Data Structures - [Validator](#validator) - [ActiveValidatorCount](#activevalidatorcount) - [ActiveVotingPower](#activevotingpower) + - [ProposerBlockReward](#proposerblockreward) - [ValidatorQueueHead](#validatorqueuehead) - [PeriodEntry](#periodentry) - [Decimal](#decimal) @@ -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. @@ -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 |