diff --git a/packages/errors/errors.go b/packages/errors/errors.go index 738ed629704070ce452d025deebb90905098128d..5ad2f9cc5955eebe5f83226406b95b17443c1084 100644 --- a/packages/errors/errors.go +++ b/packages/errors/errors.go @@ -142,7 +142,7 @@ func (f *fundamental) Derive(msg string) *fundamental { func (f *fundamental) Error() string { return f.msg } func (f *fundamental) Equals(err IdentifiableError) bool { - return f.id == err.Id() + return err != nil && f.id == err.Id() } func (f *fundamental) Id() int { diff --git a/packages/mana/balance.go b/packages/mana/balance.go index 1c407ff90c536e2246c39124fb6b77e2929c86da..95e6e857dcb99ebdb9c4906c21c7327c5de40bfc 100644 --- a/packages/mana/balance.go +++ b/packages/mana/balance.go @@ -1,59 +1,115 @@ package mana import ( - "fmt" + "github.com/iotaledger/goshimmer/packages/datastructure" ) type Balance struct { - calculator *Calculator - currentBalance uint64 - lastErosion uint64 - accumulatedRoundingError float64 + calculator *Calculator + transferHistory *datastructure.DoublyLinkedList } func NewBalance(calculator *Calculator) *Balance { return &Balance{ - calculator: calculator, - currentBalance: 0, - lastErosion: 0, - accumulatedRoundingError: 0, + calculator: calculator, + transferHistory: &datastructure.DoublyLinkedList{}, } } +// Returns the current mana balance. func (balance *Balance) GetValue() uint64 { - return balance.currentBalance + if lastBalanceHistoryEntry, err := balance.transferHistory.GetLast(); datastructure.ErrNoSuchElement.Equals(err) { + return 0 + } else { + return lastBalanceHistoryEntry.(*BalanceHistoryEntry).balance + } } -func (balance *Balance) AddTransfer(movedCoins uint64, receivedTime uint64, spentTime uint64) { - gainedMana, roundingError := balance.calculator.GenerateMana(movedCoins, spentTime-receivedTime) - - if balance.currentBalance != 0 { - if spentTime >= balance.lastErosion { - balance.Erode(spentTime) - } else { - fmt.Println("empty") - // revert old actions - // apply new - // replay old - } +// Returns the timestamp of the last mana erosion. +func (balance *Balance) GetLastErosion() uint64 { + if lastBalanceHistoryEntry, err := balance.transferHistory.GetLast(); datastructure.ErrNoSuchElement.Equals(err) { + return 0 + } else { + return lastBalanceHistoryEntry.(*BalanceHistoryEntry).transfer.spentTime } +} + +// Books a new transfer to the balance. +func (balance *Balance) AddTransfer(transfer *Transfer) { + // check if we need to rollback transfers (to prevent rounding errors) + rolledBackTransactions := balance.rollbackTransfers(transfer.spentTime) - balance.currentBalance += gainedMana - balance.accumulatedRoundingError += roundingError - balance.lastErosion = spentTime + // apply new transfer + balance.applyTransfer(transfer) - fmt.Println("GENERATE: ", spentTime-receivedTime, movedCoins, gainedMana) + // replay rolled back transfers (in reverse order) + for i := len(rolledBackTransactions) - 1; i >= 0; i-- { + balance.applyTransfer(rolledBackTransactions[i]) + } } -func (balance *Balance) Erode(erosionTime uint64) { - if balance.lastErosion <= erosionTime { - erodedMana, _ := balance.calculator.ErodeMana(balance.currentBalance, erosionTime-balance.lastErosion) +// Rolls back transfers that have their spentTime after the given referenceTime and returns a slice containing the +// rolled back transfers. +// +// Since the mana calculations use floats, we will see rounding errors. To allow all nodes to have consensus on the +// current mana balance, we need to make nodes use the exact same formulas and apply them in the exact same order. +// Because of the asynchronous nature of the tangle, nodes will see different transactions at different times and will +// therefore process their mana gains in a different order. This could lead to discrepancies in the balance due to +// accumulated rounding errors. To work around this problem, we keep a history of the latest transfers (up till a +// certain age), that can be rolled back. This allows us to apply all mana changes in the exact same order which will +// lead to a network wide consensus on the mana balances. +func (balance *Balance) rollbackTransfers(referenceTime uint64) (result []*Transfer) { + result = make([]*Transfer, 0) + + for { + if lastListEntry, err := balance.transferHistory.GetLast(); err != nil { + if !datastructure.ErrNoSuchElement.Equals(err) { + panic(err) + } + + return + } else if lastTransfer := lastListEntry.(*BalanceHistoryEntry).transfer; lastTransfer.spentTime < referenceTime { + return + } else { + result = append(result, lastTransfer) + + if _, err := balance.transferHistory.RemoveLast(); err != nil { + panic(err) + } + } + } +} - fmt.Println("ERODE: ", erosionTime-balance.lastErosion, balance.currentBalance, erodedMana) +// Applies the balance changes of the given transfer. +func (balance *Balance) applyTransfer(transfer *Transfer) { + // retrieve current values + var currentBalance, lastErosion uint64 + if lastListEntry, err := balance.transferHistory.GetLastEntry(); err != nil { + if !datastructure.ErrNoSuchElement.Equals(err) { + panic(err) + } - balance.currentBalance = erodedMana + currentBalance = 0 + lastErosion = 0 } else { - fmt.Println("empty") - // revert old erosions + lastBalanceHistoryEntry := lastListEntry.GetValue().(*BalanceHistoryEntry) + + currentBalance = lastBalanceHistoryEntry.balance + lastErosion = lastBalanceHistoryEntry.transfer.spentTime + } + + // erode if we have a balance + if currentBalance != 0 { + currentBalance, _ = balance.calculator.ErodeMana(currentBalance, transfer.spentTime-lastErosion) } + + // calculate mana gains + gainedMana, roundingError := balance.calculator.GenerateMana(transfer.movedCoins, transfer.spentTime-transfer.receivedTime) + + // store results + balance.transferHistory.AddLast(&BalanceHistoryEntry{ + transfer: transfer, + balance: currentBalance + gainedMana, + accumulatedRoundingError: roundingError, + }) } diff --git a/packages/mana/balance_history_entry.go b/packages/mana/balance_history_entry.go new file mode 100644 index 0000000000000000000000000000000000000000..7d4d6d439f74607f4cce10081f8a58d0a9f5538e --- /dev/null +++ b/packages/mana/balance_history_entry.go @@ -0,0 +1,7 @@ +package mana + +type BalanceHistoryEntry struct { + transfer *Transfer + balance uint64 + accumulatedRoundingError float64 +} diff --git a/packages/mana/balance_test.go b/packages/mana/balance_test.go index 233458983ac1baa1fa5194d09b9c52fd58f14490..5aa0b3f1e7f4384d4ac4e8f8e7730dd9d341df02 100644 --- a/packages/mana/balance_test.go +++ b/packages/mana/balance_test.go @@ -1,20 +1,25 @@ package mana import ( - "fmt" "testing" + + "github.com/magiconair/properties/assert" ) func TestBalance_AddTransfer(t *testing.T) { calculator := NewCalculator(500, 0.1) + // spend coins multiple times balance1 := NewBalance(calculator) - balance1.AddTransfer(1000, 0, 500) - balance1.AddTransfer(1000, 500, 1000) - balance1.AddTransfer(1000, 1000, 1700) - fmt.Println(balance1.GetValue()) + balance1.AddTransfer(NewTransfer(1000, 1000, 1700)) + balance1.AddTransfer(NewTransfer(1000, 700, 1000)) + balance1.AddTransfer(NewTransfer(1000, 0, 700)) + // hold coins for the full time balance2 := NewBalance(calculator) - balance2.AddTransfer(1000, 0, 1700) - fmt.Println(balance2.GetValue()) + balance2.AddTransfer(NewTransfer(1000, 0, 1700)) + + // check result + assert.Equal(t, balance1.GetValue(), uint64(301)) + assert.Equal(t, balance2.GetValue(), uint64(301)) } diff --git a/packages/mana/calculator.go b/packages/mana/calculator.go index 46aa3c53046a68f3c28d5caaebe6bd09fdc77170..38b3135a7037edb3b05d7c00e8111118c5921a98 100644 --- a/packages/mana/calculator.go +++ b/packages/mana/calculator.go @@ -18,13 +18,13 @@ func NewCalculator(decayInterval float64, decayRate float64, optionalOptions ... // store key settings decayInterval: decayInterval, - // configure optional parameters - options: DEFAULT_OPTIONS.Override(optionalOptions...), - // derive important factors ... // ... make mana reach exactly the token supply as it's max value (n coins => n mana) decayFactor: 1 - decayRate, tokenSupplyScalefactor: decayRate / (1 - decayRate), + + // configure optional parameters + options: DEFAULT_OPTIONS.Override(optionalOptions...), } } diff --git a/packages/mana/transfer.go b/packages/mana/transfer.go new file mode 100644 index 0000000000000000000000000000000000000000..eae37e0df564885fbc095a907c8439fadbb4b213 --- /dev/null +++ b/packages/mana/transfer.go @@ -0,0 +1,15 @@ +package mana + +type Transfer struct { + movedCoins uint64 + receivedTime uint64 + spentTime uint64 +} + +func NewTransfer(movedCoins uint64, receivedTime uint64, spentTime uint64) *Transfer { + return &Transfer{ + movedCoins: movedCoins, + receivedTime: receivedTime, + spentTime: spentTime, + } +}