Skip to content
Snippets Groups Projects
Commit 106a4a82 authored by Hans Moog's avatar Hans Moog
Browse files

Feat: added balance - to allow nodes to reach consensus on mana balances

parent 4e6c2ff3
No related branches found
No related tags found
No related merge requests found
......@@ -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 {
......
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,
})
}
package mana
type BalanceHistoryEntry struct {
transfer *Transfer
balance uint64
accumulatedRoundingError float64
}
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))
}
......@@ -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...),
}
}
......
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,
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment