spacemeshos / pm

Project management. Meta-tasks related to research, dev, and specs for the Spacemesh protocol and infrastructure.
http://spacemesh.io/
Creative Commons Zero v1.0 Universal
2 stars 0 forks source link

Implement genesis rewards #151

Closed lrettig closed 1 year ago

lrettig commented 1 year ago

See #5

Spec

Rewards are split into two portions, tx-fee-rewards and block-subsidy-rewards. Both portions depend upon knowing the per-coinbase layer weight.

Calculating weight

Total layer weight $\gamma\ell$ for layer $\ell$ is defined as the sum of the weights of all valid proposals (i.e., those passed by hare) $P\ell$ in the layer:

$$ \gamma\ell \equiv \sum{p \in P_\ell} w(p)$$

where $w(p)$ is the weight (in accumulated PoET ticks, recorded in the referenced ATX) of valid proposal $p$.

The weight associated with a particular coinbase $c$ in layer $\ell$, $\gamma_{\ell c}$, is calculated as:

$$ \gamma{\ell c} \equiv \sum{p \in P_\ell, cb(p) = c} w(p)$$

where $cb(p)$ is the coinbase associated with the proposal (recorded in the referenced ATX).

tx-fee-rewards

We "smooth" the tx-fee-rewards for each layer among all miners who participated in building that layer (i.e., submitted one or more proposal that was passed by hare). First, we must calculate the weight per coinbase, then the total weight for the layer, and finally, the reward breakdown per smesher.

Block construction

We cannot store absolute values for tx-fee-rewards in a block, since tx fees are contextual and the fees collected for the txs in the block could change if the block context changes. Instead, we store the coinbase layer weights $\gamma_{\ell c}$ (as defined above).

The coinbase layer weights for the layer should be stored explicitly in the block as a list mapping coinbase addresses to rational numbers $num/denom$ where both $num$ and $denom$ are uint64. This is necessary since the weights can only be calculated from valid proposals, which are ephemeral.

VM execution

To calculate tx-fee-rewards $v_B$ for a given block, the VM must execute each block tx in order, in situ, and sum the total fees paid by each tx:

$$ vB = \sum{T \in B_T} T_g$$

where $B_T$ is the set of transactions in block $B$, and $T_g$ is the total gas paid by the transaction (zero if it's ineffective).

To calculate the coinbase layer reward for a given coinbase $v_{\ell c}$, take the total tx-fee-rewards for the block $vB$, multiply by the coinbase layer weight $\gamma{\ell c}$ (calculated and stored in the block as described above), and divide by the total layer weight $\gamma\ell$. The coinbase layer reward $v{\ell c}$ should be added to the coinbase account balance $\sigma[c]_B$ at the end of the block, i.e., after all other block transactions have been applied:

\begin{align}
v_{\ell c} & = & v_B \times \frac{\gamma_{\ell c}}{\gamma_\ell} \\
\sigma'[c]_B & \equiv & \sigma[c] + v_{\ell c}
\end{align}

See the note on rounding, below.

block-subsidy-rewards

Key math

The Spacemesh economic model provides a function from layer number $\ell$ to the total smidge that should have been issued as block subsidy as of that layer, based on an exponential decay function with a particular half life. The function is as follows:

$$ % GitHub markdown does not support equation numbering so we need to jump through hoops to make this work; due to a bug, using \tag also does not work \begin{align} CumulativeSubsidy_\ell \equiv TotalSubsidy \times (1-\exp(-\lambda \ell ))&&\text{(1)} \end{align} $$

Note that the layer number $\ell$ is 1-indexed; see note below about how we index layers.

$TotalSubsidy$ includes only the portion of total issuance issued as a block subsidy to miners. In particular, it does not include the portion of issuance in the initial vaults (nor does it include tx-fee-rewards as outlined above). It is calculated as:

$$ TotalSubsidy \equiv TotalIssuance - TotalVaulted$$

where $TotalIssuance$ is set to the total, eventual issuance of 2.4bn smesh and $TotalVaulted$ is set to the total vault amount of 150m smesh; i.e., $TotalSubsidy$ is 2.25bn smesh.

$\lambda$ (lambda) is the decay parameter corresponding to our half life, defined as:

$$ \begin{align} \lambda \equiv \frac{\ln(2)}{HalfLife}&&\text{(2)} \end{align} $$

$HalfLife$ is calculated such that, exactly ten years post-genesis, $TenYearTarget$ (600m smesh, including the vaulted amount) has been issued and is circulating; i.e., in addition to $TotalVaulted$, we want to issue precisely $TenYearTarget - TotalVaulted = 600 \times 10^6 - 150 \times 10^6 = 450 \times 10^6$ smesh.

(The value of these constants is set in https://github.com/spacemeshos/economics/blob/main/constants/constants.go.)

$HalfLife$ is denominated in layers and defined to be:

$$ HalfLife \equiv TenYears \times \frac{-\ln(2)}{\ln(1-\frac{TenYearTarget-TotalVaulted}{TotalSubsidy})}$$

We can thus calculate $HalfLife$ and $\lambda$ (derivation) as:

$$ \begin{eqnarray} HalfLife & \equiv & TenYears & \times & \frac{-\ln(2)}{\ln(1-\frac{TenYearTarget-TotalVaulted}{TotalSubsidy})} & \ HalfLife & \equiv & 1051201 & \times & \frac{-\ln(2)}{\ln(1-\frac{450 \times 10^6}{2.25 \times 10^9})} & \approx 3.265328552 \ldots \times 10^6 \end{eqnarray} $$

This is between layer 3,265,328 and layer 3,265,329, or approximately 31.04 years post-genesis.

$$ \lambda = \frac{\ln(2)}{HalfLife} = \frac{\ln(2)}{3.265328552 \ldots \times 10^6} \approx 2.122748659 \ldots \times 10^{-7}$$

Effective genesis

Spacemesh includes both a genesis layer and an effective genesis layer. The genesis layer is the zeroth layer in the zeroth epoch. The first set of smeshers submits their initial ATX in epoch 1, and become eligible to smesh in epoch 2. No transactions are mined and no blocks produced until epoch 2 (chronologically, the third epoch). Therefore, no rewards are issued in epochs 0 and 1.

The effective genesis layer is the first layer in epoch 2. This is when rewards begin to be issued. For the purposes of all rewards-related math and economics, we treat the effective genesis layer as the genesis (zeroth) layer.

Layer indexing and handling time

For our purposes, we define one year to be exactly 365 24-hour days, or exactly 105120 layers of 5 minutes each. Note that equation $\text{(1)}$ above assumes that layers are indexed from 1. However, we zero-index layers: layer 0 is the effective genesis layer. We therefore add one to each layer number in equation $\text{(1)}$ so that layers are indexed from 1; otherwise, there would be zero issuance in the first layer (the effective genesis layer). In other words, the $LayerNum$ for the effective genesis layer is 1.

For any abstract time interval, e.g., $OneYear$, this has absolutely no impact: a year is always 105120 layers. However, for any fixed time interval, e.g., ten years post-genesis, we need to add one layer to account for this indexing: "ten years post genesis" is the effective genesis layer (1 layer) plus $10 \times OneYear$. For this reason we set the ten year mark, at which point we want to have achieved the $TenYearTarget$ issuance target, to ten years' worth of layers plus one:

$$ OneYear \equiv 105120$$

$$ TenYears \equiv 10 \times OneYear + 1 = 1051201$$

Number format

The key constant, $HalfLife$, is defined precisely and may be found in the tests. All other constants, including $\lambda$, can be derived from $HalfLife$ (see equation $\text{(2)}$).

We perform only fixed-point integer arithmetic using 128 bit unsigned integers rounded towards zero, as implemented by https://github.com/ericlagergren/decimal/releases/tag/v3.3.1. We run tests across all relevant platforms and architectures against a suite of hardcoded, expected outputs to ensure that the results of this math are deterministic.

Subsidy calculation

The formula for the total subsidy issued as of layer $\ell$ is given in equation $\text{(1)}$ above. We can therefore calculate the additional subsidy $R_\ell$ to be issued in $\ell$ in a straightforward fashion as:

$$ R\ell \equiv \begin{align} \lfloor \begin{cases} CumulativeSubsidy{\ell} - CumulativeSubsidy{\ell-1} & \ell > 0 \ CumulativeSubsidy{\ell} & \ell = 0 \end{cases} \rfloor&\text{(3)} \end{align} $$

Rounding

Note that, due to the nature of the math in equation $\text{(1)}$, the $CumulativeSubsidy$ values will contain a fractional component. Note the rounding (down to the nearest integer units of smidge) in equation $\text{(3)}$, the per-layer subsidy calculation. In particular, the rounding happens after the layer-to-layer difference is calculated, which means that rounded-off amounts will be accounted for in later layers.

Tail issuance

With exponential decay, issuance approaches zero asymptotically but never actually reaches zero. For this reason, asking, "When does issuance cease?" is not a well-defined question. We can be more precise and provide three answers.

We can calculate the layer when the final full smidge and full smesh are issued using the derivative of equation $\text{(1)}$:

$$ \begin{align} CumulativeSubsidy\ell & \equiv TotalSubsidy \times (1-\exp(-\lambda \ell )) \ f(\ell) & = TotalSubsidy \times (1-\exp(-\lambda \ell )) \ f'(\ell) & = TotalSubsidy \times \lambda \exp(-\lambda \ell) = 1 \ \ell & = \frac{ln(\lambda \times TotalSubsidy)}{\lambda} \ \ell{LastFullSmesh} \equiv \ell & = 1 \text{ Smesh} \ \ell_{LastFullSmidge} \equiv \ell & = 1 \text{ Smidge} \end{align} $$

The last full smesh will be issued in layer 29,060,492, approximately 276 years post-genesis, and the last full smidge will be issued in layer 126,685,172, approximately 1204 years post-genesis. The full derivations of the above can be found here: last smesh, last smidge.

However, due to the way per-layer subsidy issuance is defined as the difference between the previous layer and the current layer (equation $\text{(3)}$), even after per-layer issuance drops below one smidge (the minimum unit of value), issuance will continue (with less than one smidge issued per layer on average) until the total amount left to be issued falls below one smidge. After this point, no further issuance will occur since the amount will always round down to zero. We call this layer $\ell_{Final}$.

Note that this also means that the last smidge will never actually be issued, and the final, actual total issuance will in fact be one smidge less than $TotalIssuance$. $\ell_{Final}$ can be calculated by solving for $\ell$ in equation $\text{(1)}$:

$$ \begin{align} CumulativeSubsidy\ell & \equiv TotalSubsidy \times (1-\exp(-\lambda \ell )) \ \ell & = -\frac{ln(1-\frac{CumulativeSubsidy\ell}{TotalSubsidy})}{\lambda} \ \ell_{Final} & = -\frac{ln(1-\frac{TotalSubsidy-1}{TotalSubsidy})}{\lambda} \end{align} $$

$\ell_{Final}$ is 199,069,549, which is approximately 1,892 years post-genesis.

Per-coinbase subsidy

The total layer subsidy $R_\ell$ should be split among the layer coinbases, weighted in the same way as in tx-fee-rewards, above. The sum of the tx-fee-rewards and block-subsidy-rewards should be rounded down to the nearest smidge after the two are summed and before being added to the coinbase balance. These rounded-down "dust" smidge units are effectively burnt. Thus the subsidy awarded to a given coinbase in a given layer $R_{\ell c}$ is calculated as:

\begin{align}
R_{\ell c} & = & R_B \times \frac{\gamma_{\ell c}}{\gamma_\ell} \\
\sigma'[c]_B & \equiv & \lfloor \sigma[c] + v_{\ell c} + R_{\ell c} \rfloor
\end{align}

(for avoidance of doubt, if smesher A registered coinbase X, and smeshers B and C registered coinbase Y, and coinbase X is due 100.5 smidge, and coinbase Y is due 100.3 smidge from smesher B and 100.8 smidge from smesher C, then ultimately coinbase X will receive 100 smidge and coinbase Y will receive 201 smidge for the layer; the remaining 0.5 + 0.1 = 0.6 smidge is burnt. the rounding down happens per coinbase not per smesher.)

Block construction

Unlike tx-fee-rewards, which are contextual (the outcome of a transaction depends upon the transactions that came before it, which can change), block-subsidy-rewards are not contextual and will never change for a given block or layer. For this reason, unlike tx-fee-rewards, they could be added to the block as a set of explicit transactions (one per coinbase). However, this is unnecessary since they may be trivially calculated from the set of coinbase layer weights, included in the block, and the block total subsidy, which is a pure function of the layer number.

Empty layers

Note that the algorithm defined here (and the implementation, below) has no context about the actual conditions of the blockchain, e.g., whether or not Hare succeeded, whether any blocks were successfully produced for a layer, what the network conditions are, etc. As such, the total subsidy values for a given layer $R_\ell$ should be considered a "platonic, ideal, best case scenario", or in other words, a ceiling on the total issuance. The most obvious example where less effective issuance will occur is an empty layer: in this case, since the protocol failed to come to consensus on the contents of a layer, no subsidy issuance can occur, and total effective issuance will be less than this "platonic ideal." No affordance is made to increase issuance in later layers to account for issuance that failed to occur on older layers and in such cases the issuance that failed to be effected should be considered burnt from an economic perspective.

Code spec

The block-subsidy-rewards portion of this specification has been implemented in https://github.com/spacemeshos/economics, which may be called directly: rewards.TotalAccumulatedSubsidyAtLayer corresponds to equation $\text{(1)}$ and rewards.TotalSubsidyAtLayer corresponds to equation $\text{(3)}$. (Note that the code spec does not include the tx-fee-rewards, nor the weight calculation.)

Dev task breakdown

Weight calculation and tx-fee-rewards code is already in place. The only thing to be added is block-subsidy-rewards:

dshulyak commented 1 year ago

remove cfg.BaseReward (and, therefore, remove entire cfg object?)

there are other things in that config

blockRewards should never be empty (this would cause issuance to be wrong)

as a part of this task we should add this validation to the blocks handler, to avoid saving blocks with empty rewards

@lrettig but what was decided about empty layers? won't they cause issuance to be wrong?

lrettig commented 1 year ago

remove cfg.BaseReward (and, therefore, remove entire cfg object?)

there are other things in that config

you're right, removed this note

blockRewards should never be empty (this would cause issuance to be wrong)

as a part of this task we should add this validation to the blocks handler, to avoid saving blocks with empty rewards

@lrettig but what was decided about empty layers? won't they cause issuance to be wrong?

good point. I'm not sure there's anything we can do about this. Will give it some thought.

dshulyak commented 1 year ago

just for the visibility, i will enforce next syntactic validation while working on this issue:

dshulyak commented 1 year ago

@lrettig is there any reason to round fees and subsidy separately?

lrettig commented 1 year ago

@lrettig is there any reason to round fees and subsidy separately?

Thanks for checking. Subsidy is not context-dependent, whereas fees are. It means that, in order to know exactly how much subsidy was paid in a given block, you'd have to also know how much fees were paid (to know how it was rounded) and it would no longer be meaningful to talk about subsidy paid in a block without its context. But I think this is okay, since blocks always need to have context anyway.

Whatever you decide to do, let's update the spec accordingly, because this will be enshrined in protocol and future implementations will need to do the same thing.

countvonzero commented 1 year ago

@lrettig i don't understand why the spec does not deal with empty layer.

this mess up the subsidy math. if we have 1000 layers with 10 layers being empty, wouldn't that mess up the issuance in a significant way? in other discussions, you were very adamant about there cannot be any miscalculation in issuance. why doesn't it matter here?

we can still give rewards to miners who did make proposals, even tho those proposals didn't make it into the hare output.

countvonzero commented 1 year ago

we can still give rewards to miners who did make proposals, even tho those proposals didn't make it into the hare output.

alternatively, we can burn it to keep the math straight.

dshulyak commented 1 year ago

Whatever you decide to do, let's update the spec accordingly, because this will be enshrined in protocol and future implementations will need to do the same thing.

i think rounding down after adding fees to subsidy will burn slightly less in total, lets stick with that

lrettig commented 1 year ago

in other discussions, you were very adamant about there cannot be any miscalculation in issuance. why doesn't it matter here?

as @noamnelke pointed out on slack, there's a difference between increasing issuance and decreasing it. issuing less is much less of an issue than issuing more (which was the context of the previous conversation about a miscalculation).

as Noam also pointed out, we can't give out rewards in layers with no block because by definition in such layers the protocol failed to come to agreement so there's no way to agree on who to give them to. if the protocol "fails" to do something, no one should be rewarded.

and "Not giving out rewards to proposers who’s proposal was not included in Hare results is a feature, not a bug"