coin/coin_test.go
213 lines
1package coin23import (4"math"5"math/big"6"testing"78"peoplecoin/params"9)1011// powFloat computes x^n with big.Float at 256 bits of precision.12func powFloat(x *big.Float, n uint64) *big.Float {13res := new(big.Float).SetPrec(256).SetInt64(1)14base := new(big.Float).SetPrec(256).Set(x)15for n > 0 {16if n&1 == 1 {17res.Mul(res, base)18}19base.Mul(base, base)20n >>= 121}22return res23}2425// Decay0 must be floor(2^63 × 0.983^(1/40320)): (Decay0/2^63)^40320 ≤ 0.98326// and ((Decay0+1)/2^63)^40320 > 0.983.27func TestDecay0IsFloor(t *testing.T) {28two63 := new(big.Float).SetPrec(256).SetInt(new(big.Int).Lsh(big.NewInt(1), 63))29target := new(big.Float).SetPrec(256).SetRat(big.NewRat(983, 1000))3031ratio := func(d uint64) *big.Float {32x := new(big.Float).SetPrec(256).SetUint64(d)33return x.Quo(x, two63)34}35lo := powFloat(ratio(params.Decay0), params.SlotsPerPeriod)36hi := powFloat(ratio(params.Decay0+1), params.SlotsPerPeriod)37if lo.Cmp(target) > 0 {38t.Fatalf("Decay0 too large: (Decay0/2^63)^40320 = %s > 0.983", lo.Text('g', 25))39}40if hi.Cmp(target) <= 0 {41t.Fatalf("Decay0 too small: ((Decay0+1)/2^63)^40320 = %s ≤ 0.983", hi.Text('g', 25))42}43t.Logf("(Decay0/2^63)^40320 = %s", lo.Text('g', 20))44}4546func TestTableShape(t *testing.T) {47for i := 1; i < DecayLen; i++ {48if Decay[i] >= Decay[i-1] {49t.Fatalf("Decay[%d] not decreasing", i)50}51// Decay[i] ≈ (Decay[i-1]/2^63)^2 × 2^63, within rounding.52want := new(big.Int).Mul(new(big.Int).SetUint64(Decay[i-1]), new(big.Int).SetUint64(Decay[i-1]))53want.Rsh(want, 63)54if want.Uint64() != Decay[i] {55t.Fatalf("Decay[%d] = %d, want %d", i, Decay[i], want.Uint64())56}57}58// Decay[26] is the retention after 2^26 slots ≈ 1665 periods: ~2.5e-13.59if f := float64(Decay[DecayLen-1]) / math.Ldexp(1, 63); f > 1e-11 || f < 1e-14 {60t.Fatalf("Decay[26] = %g looks wrong", f)61}62}6364// refEffective is the same squaring chain done with big.Int, as a check65// on the 128-bit arithmetic.66func refEffective(amount, tt uint64) uint64 {67if tt >= 1<<DecayLen {68return 069}70x := new(big.Int).SetUint64(amount)71for i := 0; i < DecayLen; i++ {72if tt&(1<<uint(i)) != 0 {73x.Mul(x, new(big.Int).SetUint64(Decay[i]))74x.Rsh(x, 63)75}76}77return x.Uint64()78}7980func TestSpecFigures(t *testing.T) {81// One token after one period retains 98 299 996 (spec), i.e. the82// demurrage on it equals the UBI within rounding.83got := Effective(params.TOKEN, params.SlotsPerPeriod)84if got != 98_299_996 {85t.Errorf("decay(TOKEN, SlotsPerPeriod) = %d, spec says 98 299 996", got)86}87if got+params.UBIPerPeriod < params.TOKEN-10 || got+params.UBIPerPeriod > params.TOKEN {88t.Errorf("system not neutral at one token: %d + %d vs %d", got, params.UBIPerPeriod, params.TOKEN)89}9091// The largest uint64 has decayed below one base unit by ~2 600 periods.92if v := Effective(math.MaxUint64, 2600*params.SlotsPerPeriod); v != 0 {93t.Errorf("MaxUint64 after 2600 periods = %d, want 0", v)94}95if v := Effective(math.MaxUint64, 2500*params.SlotsPerPeriod); v == 0 {96t.Errorf("MaxUint64 after 2500 periods already 0")97}9899// A 1 UBI entry retains ~80% after a year (13 periods) and ~11% after ten.100year := 13 * params.SlotsPerPeriod101if r := float64(Effective(params.UBIPerPeriod, year)) / float64(params.UBIPerPeriod); r < 0.79 || r > 0.81 {102t.Errorf("1 UBI after a year retains %.3f, spec says ~80%%", r)103}104if r := float64(Effective(params.UBIPerPeriod, 10*year)) / float64(params.UBIPerPeriod); r < 0.10 || r > 0.12 {105t.Errorf("1 UBI after ten years retains %.3f, spec says ~11%%", r)106}107108// Rent overtakes a 1 UBI entry after ~292 periods.109cross := uint64(0)110for p := uint64(1); p < 400; p++ {111if Prunable(params.UBIPerPeriod, p*params.SlotsPerPeriod) {112cross = p113break114}115}116if cross < 285 || cross > 300 {117t.Errorf("1 UBI becomes prunable at period %d, spec says ~292", cross)118}119t.Logf("decay(TOKEN, period) = %d; 1 UBI prunable from period %d", got, cross)120}121122func TestEffectiveEdges(t *testing.T) {123if Effective(12345, 0) != 12345 {124t.Fatal("t=0 must be identity")125}126if Effective(0, 999) != 0 {127t.Fatal("zero stays zero")128}129if Effective(math.MaxUint64, 1<<DecayLen) != 0 || Effective(math.MaxUint64, math.MaxUint64) != 0 {130t.Fatal("t ≥ 2^27 must be zero")131}132if Effective(math.MaxUint64, (1<<DecayLen)-1) != 0 {133t.Fatal("t = 2^27−1 exceeds 2^27 periods worth of decay; must be zero")134}135// Small amounts: 1 base unit decays to 0 after one slot (floor).136if Effective(1, 1) != 0 {137t.Fatal("1 base unit after one slot should floor to 0")138}139}140141// exactEffective is amount × Π Decay[i]/2^63 over the set bits of t,142// without intermediate flooring.143func exactEffective(amount, tt uint64) *big.Float {144x := new(big.Float).SetPrec(256).SetUint64(amount)145two63 := new(big.Float).SetPrec(256).SetInt(new(big.Int).Lsh(big.NewInt(1), 63))146for i := 0; i < DecayLen; i++ {147if tt&(1<<uint(i)) != 0 {148f := new(big.Float).SetPrec(256).SetUint64(Decay[i])149x.Mul(x, f.Quo(f, two63))150}151}152return x153}154155// The chain floors after every multiplication, so the result is below the156// unrounded product by less than one unit per multiplication — at most157// DecayLen. That also means it is not strictly monotone in t for small158// amounts (1 000 units after 1 023 slots, ten floors, come out below 1 000159// units after 1 024 slots, one floor); the deviation is bounded the same way.160func TestRoundingBoundAndReference(t *testing.T) {161amounts := []uint64{1, 1000, params.UBIPerPeriod, params.TOKEN, 1 << 40, math.MaxUint64}162ts := []uint64{0, 1, 2, 3, 1023, 1024, 40320, 40321, 13 * 40320, 1 << 20, (1 << 20) + 7, 1000 * 40320, (1 << 27) - 1} // ascending163for _, a := range amounts {164prev := a // Effective(a, 0) == a and nothing exceeds it165for _, tt := range ts {166got := Effective(a, tt)167if got != refEffective(a, tt) {168t.Fatalf("Effective(%d, %d) = %d, reference %d", a, tt, got, refEffective(a, tt))169}170if got > a {171t.Fatalf("value grew")172}173exact := exactEffective(a, tt)174diff := new(big.Float).Sub(exact, new(big.Float).SetPrec(256).SetUint64(got))175if d, _ := diff.Float64(); d < 0 || d >= DecayLen {176t.Fatalf("Effective(%d, %d) = %d is %.2f below exact; want within [0, %d)", a, tt, got, d, DecayLen)177}178if got > prev && got-prev > DecayLen {179t.Fatalf("Effective(%d, %d) = %d exceeds earlier value %d by more than rounding", a, tt, got, prev)180}181prev = got182}183}184}185186func TestRentAndSpendable(t *testing.T) {187if Rent(1023) != 0 || Rent(1024) != 1 || Rent(params.SlotsPerPeriod) != 39 {188t.Fatalf("rent: %d %d %d", Rent(1023), Rent(1024), Rent(params.SlotsPerPeriod))189}190// A tiny UTXO expires: 100 base units are gone after 100×1024 slots.191tt := uint64(100 * 1024)192if Spendable(100, tt) != 0 || !Prunable(100, tt) {193t.Fatal("100 base units should be consumed by rent after 102 400 slots")194}195// A large one is negligibly affected over a period.196e := Effective(params.TOKEN, params.SlotsPerPeriod)197if s := Spendable(params.TOKEN, params.SlotsPerPeriod); s != e-39 {198t.Fatalf("spendable = %d, want effective−39 = %d", s, e-39)199}200if Prunable(params.TOKEN, params.SlotsPerPeriod) {201t.Fatal("a token is not prunable after a period")202}203if Elapsed(10, 5) != 0 || Elapsed(10, 15) != 5 {204t.Fatal("elapsed")205}206}207208func BenchmarkEffective(b *testing.B) {209for i := 0; i < b.N; i++ {210Effective(params.TOKEN, uint64(i)&(1<<27-1))211}212}