From a85eaf06fc4884333323314016760a038cc4da20 Mon Sep 17 00:00:00 2001
From: jonastheis <mail@jonastheis.de>
Date: Tue, 12 May 2020 20:58:00 +0200
Subject: [PATCH] Refactor TipManager to be able to handle tips in the context
 of branches

---
 .../packages/tipmanager/tipmanager.go         | 268 +++++++++++-------
 .../packages/tipmanager/tipmanager_test.go    |  32 +--
 2 files changed, 171 insertions(+), 129 deletions(-)

diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
index a36f795a..52d9bedf 100644
--- a/dapps/valuetransfers/packages/tipmanager/tipmanager.go
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
@@ -1,7 +1,6 @@
 package tipmanager
 
 import (
-	"math/rand"
 	"sync"
 
 	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
@@ -10,17 +9,21 @@ import (
 	"github.com/iotaledger/hive.go/events"
 )
 
-// TipManager manages tips of all branches and emits events for their removal and addition.
+// TipManager manages liked tips, tips of all branches and emits events for their removal and addition.
 type TipManager struct {
-	tips      map[branchmanager.BranchID]*datastructure.RandomMap
-	tipsMutex sync.Mutex
-	Events    Events
+	// tips are all currently liked tips.
+	tips *datastructure.RandomMap
+	// branches contains data for every branch and its current tips.
+	branches map[branchmanager.BranchID]*Branch
+	mutex    sync.Mutex // TODO: should be locked at branch level
+	Events   Events
 }
 
 // New creates a new TipManager.
 func New() *TipManager {
 	return &TipManager{
-		tips: make(map[branchmanager.BranchID]*datastructure.RandomMap),
+		tips:     datastructure.NewRandomMap(),
+		branches: make(map[branchmanager.BranchID]*Branch),
 		Events: Events{
 			TipAdded:   events.NewEvent(payloadIDEvent),
 			TipRemoved: events.NewEvent(payloadIDEvent),
@@ -28,136 +31,203 @@ func New() *TipManager {
 	}
 }
 
-// AddTip adds the given value object as a tip in the given branch.
-func (t *TipManager) AddTip(valueObject *payload.Payload, branch branchmanager.BranchID) {
-	objectID := valueObject.ID()
-	//parent1ID := valueObject.TrunkID()
-	//parent2ID := valueObject.BranchID()
-
-	t.tipsMutex.Lock()
-	defer t.tipsMutex.Unlock()
+// Tip represents a tip with its corresponding branchID and a reverse mapping to branches that reference it.
+type Tip struct {
+	id       payload.ID
+	branchID branchmanager.BranchID
+	// referencedByOtherBranches is a reverse mapping to all branches that reference this tip as entryPoint.
+	referencedByOtherBranches map[branchmanager.BranchID]*Branch
+}
 
-	branchTips, ok := t.tips[branch]
-	if !ok {
-		// add new branch to tips map
-		branchTips = datastructure.NewRandomMap()
-		t.tips[branch] = branchTips
+func newTip(id payload.ID, branchID branchmanager.BranchID) *Tip {
+	return &Tip{
+		id:                        id,
+		branchID:                  branchID,
+		referencedByOtherBranches: make(map[branchmanager.BranchID]*Branch),
 	}
+}
 
-	if branchTips.Set(objectID, objectID) {
-		t.Events.TipAdded.Trigger(objectID, branch)
-	}
+// Branch represents a branch with its tips and entryPoints.
+type Branch struct {
+	branchID branchmanager.BranchID
+	liked    bool
 
-	// remove parents
-	// TODO: parents could be in another branch. get branch via ID ?
-	// utxoDAG.ValueObjectBooking(valueObjectID).Unwrap().BranchID()
-	//if _, deleted := branchTips.Delete(parent1ID); deleted {
-	//	t.Events.TipRemoved.Trigger(parent1ID, branch)
-	//}
-	//
-	//if _, deleted := branchTips.Delete(parent2ID); deleted {
-	//	t.Events.TipRemoved.Trigger(parent2ID, branch)
-	//}
+	// tips are the current tips in this branch.
+	tips map[payload.ID]*Tip
+	// entryPoints are tips in other branches that are referenced by this branch.
+	entryPoints map[payload.ID]*Tip
 }
 
-// Tips randomly selects tips in the given branches weighted by size.
-func (t *TipManager) Tips(branches ...branchmanager.BranchID) (parent1ObjectID, parent2ObjectID payload.ID) {
-	if len(branches) == 0 {
-		parent1ObjectID = payload.GenesisID
-		parent2ObjectID = payload.GenesisID
-		return
+func newBranch(branchID branchmanager.BranchID, liked bool) *Branch {
+	return &Branch{
+		branchID:    branchID,
+		tips:        make(map[payload.ID]*Tip),
+		entryPoints: make(map[payload.ID]*Tip),
+		liked:       liked,
 	}
+}
 
-	weights := make([]int, len(branches))
-	totalTips := 0
-	tipsSlice := make([]*datastructure.RandomMap, len(branches))
+// AddTip adds the given value object as a tip in the given branch.
+// If the branch is liked it is also added to t.tips.
+// Parents are handled depending on the relation (same or different branch).
+func (t *TipManager) AddTip(valueObject *payload.Payload, b *branchmanager.Branch) {
+	t.mutex.Lock()
+	defer t.mutex.Unlock()
 
-	t.tipsMutex.Lock()
-	defer t.tipsMutex.Unlock()
+	objectID := valueObject.ID()
+	parent1ID := valueObject.TrunkID()
+	parent2ID := valueObject.BranchID()
 
-	// prepare weighted random selection
-	for i, b := range branches {
-		branchTips, ok := t.tips[b]
-		if !ok {
-			continue
-		}
+	branch, ok := t.branches[b.ID()]
+	if !ok {
+		branch = newBranch(b.ID(), b.Liked())
+		t.branches[b.ID()] = branch
+	}
 
-		tipsSlice[i] = branchTips
-		weights[i] = branchTips.Size()
-		totalTips += weights[i]
+	// add new tip to branch.tips
+	tip := newTip(objectID, branch.branchID)
+	branch.tips[objectID] = tip
+
+	// TODO: retrieve parents' branches
+	parent1branch := branch
+	addTipHandleParent(parent1ID, parent1branch, branch)
+	parent2branch := branch
+	addTipHandleParent(parent2ID, parent2branch, branch)
+
+	// add to t.tips and remove parents from t.tips if branch is liked
+	if branch.liked {
+		t.tips.Set(tip.id, tip)
+		t.tips.Delete(parent1ID)
+		t.tips.Delete(parent2ID)
 	}
+}
 
-	// no tips in the given branches
-	// TODO: what exactly to do in this case?
-	if totalTips == 0 {
+// Tips returns two randomly selected tips.
+func (t *TipManager) Tips() (parent1ObjectID, parent2ObjectID payload.ID) {
+	// TODO: this might be over locking
+	t.mutex.Lock()
+	defer t.mutex.Unlock()
+
+	tip := t.tips.RandomEntry()
+	if tip == nil {
 		parent1ObjectID = payload.GenesisID
 		parent2ObjectID = payload.GenesisID
 		return
 	}
 
-	// select tips
-	random := weightedRandom(weights)
-
-	branchTips := tipsSlice[random]
-	tip := branchTips.RandomEntry()
-	if tip == nil {
-		// this case should never occur due to weighted random selection
-		panic("tip is nil.")
-	}
-	parent1ObjectID = tip.(payload.ID)
+	parent1ObjectID = tip.(Tip).id
 
-	if totalTips == 1 {
+	if t.tips.Size() == 1 {
 		parent2ObjectID = parent1ObjectID
 		return
 	}
 
-	// adjust weights
-	weights[random] -= 1
-
-	branchTips = tipsSlice[weightedRandom(weights)]
-	tip = branchTips.RandomEntry()
-	if tip == nil {
-		// this case should never occur due to weighted random selection
-		panic("tip is nil.")
+	parent2ObjectID = t.tips.RandomEntry().(Tip).id
+	// select 2 distinct tips if there's more than 1 tip available
+	for parent1ObjectID == parent2ObjectID && t.tips.Size() > 1 {
+		parent2ObjectID = t.tips.RandomEntry().(Tip).id
 	}
-	parent2ObjectID = tip.(payload.ID)
 
 	return
 }
 
-// TipCount returns the total tips in the given branches.
-func (t *TipManager) TipCount(branches ...branchmanager.BranchID) int {
-	t.tipsMutex.Lock()
-	defer t.tipsMutex.Unlock()
+// TipCount returns the total liked tips.
+func (t *TipManager) TipCount() int {
+	// TODO: this might be over locking
+	t.mutex.Lock()
+	defer t.mutex.Unlock()
 
-	totalTips := 0
-	for _, b := range branches {
-		branchTips, ok := t.tips[b]
-		if !ok {
-			continue
-		}
+	return t.tips.Size()
+}
+
+// OnBranchLiked is called when a branch is liked.
+// It adds the branch's tips to t.tips and removes tips from referenced branches from t.tips.
+func (t *TipManager) OnBranchLiked(branchID branchmanager.BranchID) {
+	t.mutex.Lock()
+	defer t.mutex.Unlock()
+
+	branch := t.branches[branchID]
+	branch.liked = true
+
+	// add tips of current branch
+	for _, tip := range branch.tips {
+		addTipIfNotReferencedByLikedBranch(tip, t)
+	}
+
+	// remove tips from referenced branches
+	for objectID, _ := range branch.entryPoints {
+		t.tips.Delete(objectID)
+	}
+}
 
-		totalTips += branchTips.Size()
+// OnBranchDisliked is called when a branch is disliked.
+// It removes the branch's tips from t.tips and adds tips from referenced branches back to t.tips.
+func (t *TipManager) OnBranchDisliked(branchID branchmanager.BranchID) {
+	t.mutex.Lock()
+	defer t.mutex.Unlock()
+
+	branch := t.branches[branchID]
+	branch.liked = false
+
+	// remove tips of current branch
+	for _, tip := range branch.tips {
+		t.tips.Delete(tip.id)
+	}
+
+	// add tips from referenced branches
+	for _, tipFromOtherBranch := range branch.entryPoints {
+		addTipIfNotReferencedByLikedBranch(tipFromOtherBranch, t)
 	}
-	return totalTips
 }
 
-func weightedRandom(weights []int) int {
-	if len(weights) == 0 {
-		return 0
+// OnBranchDisliked is called when a branch is merged.
+// TODO: it should perform some cleanup logic
+func (t *TipManager) OnBranchMerged(branchID branchmanager.BranchID) {
+	// remove all tips from t.tips
+	// remove tips out of entryPoints of other branches
+}
+
+// removeTipAsEntryPoint removes the tip from all its referenced branches' entry point map.
+func removeTipAsEntryPoint(tip *Tip) {
+	for _, branch := range tip.referencedByOtherBranches {
+		delete(branch.entryPoints, tip.id)
 	}
+}
 
-	totalWeight := 0
-	for _, w := range weights {
-		totalWeight += w
+// addTipHandleParent handles a tip's parent when adding it.
+// If the parent is in the same branch it is removed as a tip.
+// If the parent is not in the same branch a two-way mapping from parent to branch and branch to parent is created.
+func addTipHandleParent(parentID payload.ID, parentBranch *Branch, branch *Branch) {
+	parentTip := parentBranch.tips[parentID]
+
+	if parentBranch.branchID == branch.branchID {
+		// remove parents out of branch.tips
+		delete(branch.tips, parentID)
+		// properly remove parent -> not a tip anymore!
+		removeTipAsEntryPoint(parentTip)
+	} else {
+		// add reference to current branch to parent tip
+		parentTip.referencedByOtherBranches[branch.branchID] = branch
+
+		// add reference to parent to current branch
+		branch.entryPoints[parentID] = parentTip
 	}
-	r := rand.Intn(totalWeight)
+}
+
+// addTipIfNotReferencedByLikedBranch adds a tip to t.tips
+// only if it is not referenced by any liked branch.
+func addTipIfNotReferencedByLikedBranch(tip *Tip, t *TipManager) {
+	addTip := true
 
-	for i, w := range weights {
-		r -= w
-		if r < 0 {
-			return i
+	// if there's any liked branch referencing this tip we do not add it to t.tips
+	for _, b := range tip.referencedByOtherBranches {
+		if b.liked {
+			addTip = false
+			break
 		}
 	}
-	return len(weights) - 1
+
+	if addTip {
+		t.tips.Set(tip.id, tip)
+	}
 }
diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
index 926b7a8b..1bf532d7 100644
--- a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
@@ -1,18 +1,12 @@
 package tipmanager
 
 import (
-	"math/rand"
-	"sync"
-	"testing"
-
 	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
 	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
-	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
-	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
 	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
-	"github.com/stretchr/testify/assert"
 )
 
+/*
 func TestTipManagerSimple(t *testing.T) {
 	tipManager := New()
 
@@ -88,29 +82,7 @@ func TestTipManagerParallel(t *testing.T) {
 	// check total tip count
 	assert.Equal(t, totalTips*totalThreads, tipManager.TipCount(branches...))
 }
-
-func TestTipManager_weightedRandom(t *testing.T) {
-	totalRounds := 1000000
-	weights := []int{1, 20, 39, 30, 9, 1}
-	weightsSum := 0
-	for _, w := range weights {
-		weightsSum += w
-	}
-	// make sure result is within delta of 0.01
-	delta := float64(totalRounds / weightsSum)
-
-	counts := make([]int, len(weights))
-
-	// calculate weighted random
-	for i := 0; i < totalRounds; i++ {
-		counts[weightedRandom(weights)]++
-	}
-
-	for i, c := range counts {
-		expected := totalRounds * weights[i] / weightsSum
-		assert.InDelta(t, expected, c, delta)
-	}
-}
+*/
 
 func createDummyTransaction() *transaction.Transaction {
 	return transaction.New(
-- 
GitLab