eai

package
v1.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2021 License: Apache-2.0 Imports: 12 Imported by: 9

README

EAI

EAI is an acronym for EAI Ain't Interest, or something even more forgettable1. It is an incentive for holding lots of ndau, plus an incentive for locking ndau. These incentives take the form of ndau, conjured from the ether. They are awarded when an account's delegated node decides to issue a CreditEAI transaction, on whatever schedule that node feels like.

But how is it calculated?

Use the eai.Calculate function.

No really, how do I calculate it by hand?

If you absolutely must hand-calculate EAI for verification or other purposes, take as your first reference the test cases, which are fairly well documented.

For any account, over any period of time, one can break down the period into some number of (rate, duration) pairs. Once you have the correct list of pairs, you simply compute the product , where rate is expressed numerically (i.e. 1% == 0.01) and duration is expressed as a fractional year (i.e. 1 day == 1/365), using the ndau canonical duration definitions. After computing factor, .

We've glossed over the mechanism for getting the (rate, duration) pairs, because it's complicated.

Computing (rate, duration) pairs for an arbitrary period

The easiest portion of EAI rate to calculate has to do with the lock: if an account is locked, then at the time of lock, a bonus lock rate is computed by reference to a lock rate lookup table, and stored with the lock. For a locked account, simply retrieve the bonus lock rate. This will be added to all other rates computed.

Next, compute the effective age of the account for the period in question. The effective age of the account is the weighted average age plus the lock period.

Next, decompose the period into some number of periods based on the unlocked rate table. Note: if the account is notified, the effective age freezes at that point and does not change until the account is unlocked.

A worked example is likely to be helpful here. This is from one of the test cases:

Case 3: What happens if an account is:

- locked for 180 days
- notified to unlock 165 days from now
- 84 days since last EAI update
- current actual weighted average age is 123 days

The span of effective average age we care about for the unlocked
portion runs from actual day 39 to actual day 123. The notify happens
on actual day 108. It expires on actual day 288. At that point, the
rate will drop back to the actual weighted average age.

The effective period begins on day 129, and runs forward normally
until effective day 157. Effective time freezes at that point. On
actual day 157, the notice period ends and calculations resume using
the actual weighted average age.

Dashed lines in the following graph indicate points in the future,
assuming no further transactions are issued.

10%                     ┌────────|────────x-------
 9%              ┌──────┘        |
 8%      ──x─────┘               |
        ___________________________________________
 actual    39    60     90      108      123   288
 effect.  219   240    270      288......288...288
 month    (7)   (8)    (9)                     (9)

Because the account was locked for 180 days, and 180 days has a bonus
rate of 2%, the actual rate used during the lock and notification
periods should increase by a constant rate of 2%.
We thus get the following calculation to compute the EAI factor:

   e^(10% * 21 days)
 * e^(11% * 30 days)
 * e^(12% * 33 days)

The 33 days of the final term are simply the 18 unnotified days
of the rate period plus the 15 days notified to date.

1: Ecosystem Alignment Incentive

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Calculate

func Calculate(
	balance math.Ndau,
	blockTime, lastEAICalc math.Timestamp,
	weightedAverageAge math.Duration,
	lock Lock,
	ageTable RateTable,
	fixUnlockBug bool,
) (math.Ndau, error)

Calculate the EAI due for a given account

EAI is not interest. Ndau never earns interest. However, for ease of explanation, we will borrow some terminology from interest rate calculations.

EAI is continuously compounded according to the formula

eai = balance * (e ^ (rate * time) - 1)

Rates are expressed in percent per year.

Thus, 100 ndau at 1 percent rate over 1 year yields 1.00501670 ndau EAI.

The use of continuously compounded interest instead of simple interest aids in EAI predictability: using simple interest, an account which simply accumulates its EAI, whose node won frequently, would see a higher rate of actual return than an identical account whose node won infrequently. Continuously compounded interest avoids that issue: both accounts will see the same rate of return; the benefit of the one registered to the frequent node is that it sees the increase more often.

Types

type Lock

type Lock interface {
	GetNoticePeriod() math.Duration
	GetUnlocksOn() *math.Timestamp
	GetBonusRate() Rate
}

Lock is anything which acts like a lock struct.

If we want to use a Lock struct literal, we have two options:

  1. Implement it in this package and end up with a dependency on noms.
  2. Implement it in the ndaunode.backing package and end up with a dependency cycle.

We started with option 1, but a noms dependency from this low in the stack is not great. Instead, let's define a Lock interface which we can implement elsewhere.

type RSRow

type RSRow struct {
	Duration math.Duration
	Rate     Rate
}

RSRow is a single row of a rate slice.

func (*RSRow) DecodeMsg

func (z *RSRow) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*RSRow) EncodeMsg

func (z *RSRow) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*RSRow) MarshalMsg

func (z *RSRow) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (*RSRow) Msgsize

func (z *RSRow) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (*RSRow) UnmarshalMsg

func (z *RSRow) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

type RTRow

type RTRow struct {
	From math.Duration
	Rate Rate
}

RTRow is a single row of a rate table

func (*RTRow) DecodeMsg

func (z *RTRow) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*RTRow) EncodeMsg

func (z *RTRow) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*RTRow) MarshalMsg

func (z *RTRow) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (RTRow) MarshalText

func (r RTRow) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler

func (*RTRow) Msgsize

func (z *RTRow) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (*RTRow) UnmarshalMsg

func (z *RTRow) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

func (*RTRow) UnmarshalText

func (r *RTRow) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler

type Rate

type Rate int64

A Rate defines a rate of increase over time.

EAI is not interest. Ndau never earns interest. However, for ease of explanation, we will borrow some terminology from interest rate calculations.

EAI is continuously compounded according to the formula

eai = balance * (e ^ (rate * time) - 1)

Rates are expressed in percent per year.

Thus, 100 ndau at 1 percent rate over 1 year yields 1.00501670 ndau EAI.

The use of continuously compounded interest instead of simple interest aids in EAI predictability: using simple interest, an account which simply accumulates its EAI, whose node won frequently, would see a higher rate of actual return than an identical account whose node won infrequently. Continuously compounded interest avoids that issue: both accounts will see the same rate of return; the benefit of the one registered to the frequent node is that it sees the increase more often.

We use a signed int so that json2msgp won't need type hints for encoding rate tables in system variables. If we use a rate denominator of 1e12, corresponding to a rate of 100%, then 63 bits gives us enough room to handle rates in the hundreds of millions of percents.

func CalculateEAIRate

func CalculateEAIRate(
	weightedAverageAge math.Duration,
	lock Lock,
	unlockedTable RateTable,
	at math.Timestamp,
) Rate

CalculateEAIRate accepts a WAA, a lock, a rate table, and a calculation timestamp, and looks up the current EAI rate from that info. The rate is returned as a Rate: a newtype wrapping a uint64, with an implied denominator of constants.RateDenominator.

The timestamp is necessary in order to determine whether the lock is still notified, or the notice period has expired.

func ParseRate

func ParseRate(s string) (Rate, error)

ParseRate attempts to parse a Rate from the provided string

func RateFromPercent

func RateFromPercent(nPercent uint64) Rate

RateFromPercent returns a Rate whose value is that of the input, as percent.

i.e. to express 1%, `nPercent` should equal `1`

func (*Rate) DecodeMsg

func (z *Rate) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (Rate) EncodeMsg

func (z Rate) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (Rate) MarshalMsg

func (z Rate) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (Rate) Msgsize

func (z Rate) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (Rate) String

func (r Rate) String() string

String writes this Rate as a string

func (*Rate) UnmarshalMsg

func (z *Rate) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

type RateSlice

type RateSlice []RSRow

A RateSlice is derived from a RateTable, optimized for computation.

Whereas a RateTable is meant to be easy for humans to understand, a RateSlice is more efficient for computation. It is a list of rates, and the actual duration over which each rate is active.

func (*RateSlice) DecodeMsg

func (z *RateSlice) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (RateSlice) EncodeMsg

func (z RateSlice) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (RateSlice) MarshalMsg

func (z RateSlice) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (RateSlice) Msgsize

func (z RateSlice) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (*RateSlice) UnmarshalMsg

func (z *RateSlice) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

type RateTable

type RateTable []RTRow

A RateTable defines a stepped sequence of EAI rates which apply at varying durations.

It is a logic error if the elements of a RateTable are not sorted in increasing order by their From field.

var (
	// DefaultUnlockedEAI is the default base rate table for unlocked accounts
	//
	// The UnlockedEAI rate table is a system variable which is adjustable
	// whenever the BPC desires, but for testing purposes, we use this
	// approximation as a default.
	//
	// Defaults drawn from https://tresor.it/p#0041o9iot7hm4kb5y707es7o/Oneiro%20Company%20Info/Whitepapers%20and%20Presentations/ndau%20Whitepaper%201.3%2020180425%20Final.pdf
	// page 15.
	DefaultUnlockedEAI RateTable

	// DefaultLockBonusEAI is the bonus rate for locks of varying length
	//
	// The LockBonusEAI rate table is a system variable which is adjustable
	// whenever the BPC desires, but for testing purposes, we use this
	// approximation as a default.
	//
	// Defaults drawn from https://tresor.it/p#0041o9iot7hm4kb5y707es7o/Oneiro%20Company%20Info/Whitepapers%20and%20Presentations/ndau%20Whitepaper%201.3%2020180425%20Final.pdf
	// page 15.
	DefaultLockBonusEAI RateTable
)

func (*RateTable) DecodeMsg

func (z *RateTable) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (RateTable) EncodeMsg

func (z RateTable) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (RateTable) MarshalMsg

func (z RateTable) MarshalMsg(b []byte) (o []byte, err error)

MarshalMsg implements msgp.Marshaler

func (RateTable) Msgsize

func (z RateTable) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

func (RateTable) RateAt

func (rt RateTable) RateAt(point math.Duration) Rate

RateAt returns the rate in a RateTable for a given point

func (RateTable) Slice

func (rt RateTable) Slice(from, to, offset math.Duration) RateSlice

Slice transforms a RateTable into a form more amenable for computation.

Rates vary over time, and we want to efficiently compute the sum of interest at varying rates. Instead of repeatedly calling RateAt, it's more efficient to perform the calculation once to slice the affected data out of the RateTable.

func (RateTable) SliceF

func (rt RateTable) SliceF(from, to, offset, freeze math.Duration) RateSlice

SliceF transforms a RateTable into a form more amenable for computation.

Rates vary over time, and we want to efficiently compute the sum of interest at varying rates. Instead of repeatedly calling RateAt, it's more efficient to perform the calculation once to slice the affected data out of the RateTable.

Let's diagram the variables in play in here: (parentheticized variables are not present)

Timestamps
     │ (effective account open)
     │   │        (lastEAICalc)
     │   │           │  (notify)              (blockTime)  (lock.UnlocksOn)

TIME ─┼───┼───────────┼─────┼─────────────────────┼────────────┼──>

    │   │           │     ├────── freeze ───────┤            │
    │   │           │     └───────────── offset ─────────────┘
    │   ├── from ───┴──── (lastEAICalcAge) ─────┤
    │   └──────────────── to ───────────────────┘
Durations

Where freeze == 0, this function returns the rate slice from (from+offset) to (to+offset).

R3                                         ┌────|────────...
R2                            ┌────────────┘ / /|
R1              ┌────|────────┘ / / / / / / / / |
R0  ────────────┘    | / / / / / / / / / / / / /|
                (from+offset)                (to+offset)

Where freeze != 0, this function returns the rate slice from (from+offset) to (to+offset), but with the actual rate frozen at the freeze point.

(This diagram is not to the same scale as the timeline overview above.)

R3                                         ┌────|────────...
R2                            ┌─────────|───────|──
R1              ┌────|────────┘ / / / / | / / / |
R0  ────────────┘    | / / / / / / / / /|/ / / /|
                (from+offset)           |    (to+offset)
                              (to+offset-freeze)

func (*RateTable) UnmarshalMsg

func (z *RateTable) UnmarshalMsg(bts []byte) (o []byte, err error)

UnmarshalMsg implements msgp.Unmarshaler

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL