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

Commit

Permalink
Merge pull request #32 from lazyledger/adlerjohn-state_representation
Browse files Browse the repository at this point in the history
Add state representation
  • Loading branch information
adlerjohn committed Jun 16, 2020
2 parents 9dc801e + 4d87996 commit 99732e7
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 15 deletions.
56 changes: 55 additions & 1 deletion proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,58 @@ message MessageData {
message Message {
bytes namespace_id = 1;
bytes raw_data = 2;
}
}

message Account {
uint64 balance = 1;
uint64 nonce = 2;
bool isValidator = 3;
bool isDelegating = 4;
Delegation delegationInfo = 5;
}

message Delegation {
enum DelegationStatus {
DELEGATION_STATUS_UNSPECIFIED = 0;
DELEGATION_STATUS_BONDED = 1;
DELEGATION_STATUS_UNBONDING = 2;
}

DelegationStatus status = 1;
Address validator = 2;
uint64 stakedBalance = 3;
PeriodEntry beginEntry = 4;
PeriodEntry endEntry = 5;
uint64 unbondingHeight = 6;
}

message Validator {
enum ValidatorStatus {
VALIDATOR_STATUS_UNSPECIFIED = 0;
VALIDATOR_STATUS_QUEUED = 1;
VALIDATOR_STATUS_BONDED = 2;
VALIDATOR_STATUS_UNBONDING = 3;
VALIDATOR_STATUS_UNBONDED = 4;
}

ValidatorStatus status = 1;
uint64 stakedBalance = 2;
Decimal commissionRate = 3;
uint32 delegatedCount = 4;
uint64 votingPower = 5;
uint64 pendingRewards = 6;
PeriodEntry latestEntry = 7;
uint64 unbondingHeight = 8;
bool isSlashed = 9;
Decimal slashRate = 10;
}

message ActiveValidatorCount {
uint32 numValidators = 1;
}

message PeriodEntry {
uint64 rewardRate = 1;
}

message Decimal {}
54 changes: 54 additions & 0 deletions rationale/distributing_rewards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Rationale: Distributing Rewards and Penalties
===

- [Rationale: Distributing Rewards and Penalties](#rationale-distributing-rewards-and-penalties)
- [Preamble](#preamble)
- [Distribution Scheme](#distribution-scheme)
- [State-Efficient Implementation](#state-efficient-implementation)

# Preamble

Due to the requirement that all incorrect state transitions on LazyLedger be provable with a [compact fraud proof](https://arxiv.org/abs/1809.09044) that is cheap enough to verify within a smart contract on a remote chain (e.g. Ethereum), computing how rewards and penalties are distributed must involve no iterations. To understand why, let us consider the following desiderata in a staking system:
1. In-protocol stake delegation: this makes it easier for users to participate in the consensus process, and reduces reliance on custodial staking services.
1. In-protocol enforcement of proper distribution of rewards and penalities to delegators: rewards and penalties collected by validators should be distributed to delegators trustlessly.

Naively, rewards and penalties (henceforth referred to collectively as "rewards", since penalties are simply negative rewards) can be distributed immediately. For example, when a validator produces a new block and is entitled to collecting transaction fees, these fees can be distributed to every single account delegating stake to this validator. This requires iterating over potentially a huge number of state elements for a single state transition (i.e. transaction), which is computationally expensive. The specific problem is that it would be infeasible to prove that such a state transition was _incorrect_ (i.e. with a fraud proof) within the execution system of a remote blockchain (i.e. with a smart contract).

This forms the primary motivation of the scheme discussed here: a mechanism for distributing rewards that is state-efficient and requires no iteration over state elements for any state transition.

# Distribution Scheme

The scheme presented here is an incarnation of Cosmos' [F1 fee distribution scheme](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/_proposals/f1-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:

$$
\text{naive reward} = x \frac{T}{n}
$$

In other words, the voting power contributed by the delegator multiplied by the _reward rate_, i.e. the rewards per unit of voting power. We note that if the total voting power of a validator remains constant forever, then the above equation holds and is approximation-free. However, changes to the total voting power need to be accounted for.

Blocks between two changes to a validator's voting power (i.e. whenever a user delegates or undelegates stake) are a _period_. Every time a validator's voting power changes (i.e. a new period $f$ begins), an entry $Entry_f$ for this period is saved in state, which records _the reward rate up to the beginning of_ $f$. This is simply the sum of the reward rate up to the beginning of previous period $f-1$ and the reward rate of the period $f$ itself:

$$
Entry_f = \begin{cases}
0 & f = 0 \\
Entry_{f-1} + \frac{T_f}{n_f} & f > 0 \\
\end{cases}
$$

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$).

$$
\text{reward} = x (Entry_f - Entry_k)
$$

This raw reward can be scaled by additional factors, such as commissions or slashing penalties.

## State-Efficient Implementation

The F1 paper does not specify where entries are stored in state, but the understanding is that they are placed in independent state elements. This has the downside of requiring multiple Merkle branches to prove the inclusion of entries for e.g. fraud proofs. We can improve on this by leveraging a specific property of entries, namely that each entry is only used in exactly two cases:
1. To compute the next entry.
1. To compute the reward of a delegator.

Intuitively, after having being used twice, an entry can be pruned from the state. We can make use of this by storing only the latest entry with its respective validator object, and a copy of the two entries each delegation needs with the delegation object. By storing entries directly with the objects that require them, state transitions can be statelessly validated without extra inclusion proofs.
97 changes: 94 additions & 3 deletions specs/consensus.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ Consensus Rules
- [Constants](#constants)
- [Types](#types)
- [Reserved Namespace IDs](#reserved-namespace-ids)
- [Reserved State Subtree IDs](#reserved-state-subtree-ids)
- [Rewards and Penalties](#rewards-and-penalties)
- [Leader Selection](#leader-selection)
- [Fork Choice](#fork-choice)
- [Block Validity](#block-validity)
- [State Transitions](#state-transitions)
- [Validators and Delegations](#validators-and-delegations)
- [Formatting](#formatting)
- [Availability](#availability)

Expand All @@ -37,12 +40,16 @@ Consensus Rules
| `SHARE_RESERVED_BYTES` | `uint64` | `1` | `byte` | Bytes reserved at the beginning of each [share](data_structures.md#share). Must be sufficient to represent `SHARE_SIZE`. |
| `AVAILABLE_DATA_ORIGINAL_SQUARE_SIZE` | `uint64` | | `share` | Number of rows/columns of the original data [shares](data_structures.md#share) in [square layout](data_structures.md#arranging-available-data-into-shares). |
| `GENESIS_COIN_COUNT` | `uint64` | `10**8` | `4u` | `(= 100000000)` Number of coins at genesis. |
| `UNBONDING_DURATION` | `uint32` | | `block` | Duration, in blocks, for unbonding a validator or delegation. |
| `MAX_VALIDATORS` | `uint16` | `64` | | Maximum number of active validators. |
| `STATE_SUBTREE_RESERVED_BYTES` | `uint64` | `1` | `byte` | Number of bytes reserved to identify state subtrees. |

## Types

| name | type |
| ------------- | -------- |
| `NamespaceID` | `uint64` |
| name | type |
| ---------------- | -------- |
| `NamespaceID` | `uint64` |
| `StateSubtreeID` | `byte` |

## Reserved Namespace IDs

Expand All @@ -53,6 +60,15 @@ Consensus Rules
| `EVIDENCE_NAMESPACE_ID` | `NamespaceID` | `0x0000000000000000000000000000000000000000000000000000000000000003` |
| `PARITY_SHARE_NAMESPACE_ID` | `NamespaceID` | `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF` |

## Reserved State Subtree IDs

| name | type | value |
| -------------------------------- | ---------------- | ------ |
| `ACCOUNTS_SUBTREE_ID` | `StateSubtreeID` | `0x01` |
| `ACTIVE_VALIDATORS_SUBTREE_ID` | `StateSubtreeID` | `0x02` |
| `INACTIVE_VALIDATORS_SUBTREE_ID` | `StateSubtreeID` | `0x03` |


## Rewards and Penalties

| name | type | value | unit | description |
Expand All @@ -66,6 +82,81 @@ Consensus Rules

# Block Validity

## State Transitions

### Validators and Delegations

A transaction `tx` that requests a new validator initializes a new [Validator](data_structures.md#validator) leaf in the inactive validators subtree for that account as follows:
```
validator.status = ValidatorStatus.Queued
validator.stakedBalance = tx.amount
validator.commissionRate = tx.commissionRate
validator.delegatedCount = 0
validator.votingPower = tx.amount
validator.pendingRewards = 0
validator.latestEntry = PeriodEntry(0)
validator.unbondingHeight = 0
validator.isSlashed = false
```

At the end of a block at the end of an epoch, 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.
```
validator.status = ValidatorStatus.Bonded
```

For validators that were bonded but are no longer (either by being outside the top `MAX_VALIDATORS` validators, through a transaction that requests unbonding, or by being slashing), the validator object is moved to the inactive validators subtree and they begin unbonding.
```
validator.status = ValidatorStatus.Unbonding
validator.unbondingHeight = block.height + 1
```

Once an unbonding validator has waited at least `UNBONDING_DURATION` blocks, they can be unbonded, collecting their reward:
```
old_stakedBalance = validator.stakedBalance
validator.status = ValidatorStatus.Unbonded
validator.stakedBalance = 0
validator.votingPower -= old_stakedBalance
```

Every time a bonded validator's voting power changes (i.e. when a delegation is added or removed), or when a validator begins unbonding, the rewards per unit of voting power accumulated so far are calculated:
```
old_pendingRewards = validator.pendingRewards
old_votingPower = validator.votingPower
validator.pendingRewards = 0
validator.latestEntry += old_pendingRewards / old_votingPower
```

A transaction `tx` that requests a new delegation first updates the target validator's voting power:
```
validator.delegatedCount += 1
validator.votingPower += tx.amount
```

then initializes the [Delegation](data_structures.md#delegation) field of that account as follows:
```
delegation.status = DelegationStatus.Bonded
delegation.validator = tx.validator
delegation.stakedBalance = tx.amount
delegation.beginEntry = validator.latestEntry
delegation.endEntry = PeriodEntry(0)
delegation.unbondingHeight = 0
```

A transaction `tx` that requests withdrawing a delegation first updates the delegation field:
```
delegation.status = DelegationStatus.Unbonding
delegation.endEntry = validator.latestEntry
delegation.unbondingHeight = block.height + 1
```

then updates the target validator's voting power:
```
validator.delegatedCount -= 1
validator.votingPower -= delegation.votingPower
```

## Formatting

Leaves in the message [Namespace Merkle Tree](data_structures.md#namespace-merkle-tree) must be ordered lexicographically by namespace ID.
Expand Down
Loading

0 comments on commit 99732e7

Please sign in to comment.