From 106a4a829f92a07ef852a1eb8fc5afdbeed4bc65 Mon Sep 17 00:00:00 2001
From: Hans Moog <hm@mkjc.net>
Date: Thu, 8 Aug 2019 01:21:12 +0200
Subject: [PATCH] Feat: added balance - to allow nodes to reach consensus on
 mana balances

---
 packages/errors/errors.go              |   2 +-
 packages/mana/balance.go               | 122 ++++++++++++++++++-------
 packages/mana/balance_history_entry.go |   7 ++
 packages/mana/balance_test.go          |  19 ++--
 packages/mana/calculator.go            |   6 +-
 packages/mana/transfer.go              |  15 +++
 6 files changed, 127 insertions(+), 44 deletions(-)
 create mode 100644 packages/mana/balance_history_entry.go
 create mode 100644 packages/mana/transfer.go

diff --git a/packages/errors/errors.go b/packages/errors/errors.go
index 738ed629..5ad2f9cc 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 1c407ff9..95e6e857 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 00000000..7d4d6d43
--- /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 23345898..5aa0b3f1 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 46aa3c53..38b3135a 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 00000000..eae37e0d
--- /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,
+	}
+}
-- 
GitLab