diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go index 52d9bedfed21b3d9b22da91c89589f07561dfd4a..6f48262a6f856d00d5e6c781a24d028c71212af7 100644 --- a/dapps/valuetransfers/packages/tipmanager/tipmanager.go +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go @@ -67,10 +67,16 @@ func newBranch(branchID branchmanager.BranchID, liked bool) *Branch { } } +// LikableBrancher defines an interface for a likable branch. +type LikableBrancher interface { + ID() branchmanager.BranchID + Liked() bool +} + // 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) { +func (t *TipManager) AddTip(valueObject *payload.Payload, b LikableBrancher) { t.mutex.Lock() defer t.mutex.Unlock() @@ -115,17 +121,17 @@ func (t *TipManager) Tips() (parent1ObjectID, parent2ObjectID payload.ID) { return } - parent1ObjectID = tip.(Tip).id + parent1ObjectID = tip.(*Tip).id if t.tips.Size() == 1 { parent2ObjectID = parent1ObjectID return } - parent2ObjectID = t.tips.RandomEntry().(Tip).id + 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 = t.tips.RandomEntry().(*Tip).id } return @@ -155,7 +161,7 @@ func (t *TipManager) OnBranchLiked(branchID branchmanager.BranchID) { } // remove tips from referenced branches - for objectID, _ := range branch.entryPoints { + for objectID := range branch.entryPoints { t.tips.Delete(objectID) } } @@ -180,7 +186,7 @@ func (t *TipManager) OnBranchDisliked(branchID branchmanager.BranchID) { } } -// OnBranchDisliked is called when a branch is merged. +// OnBranchMerged 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 @@ -198,7 +204,11 @@ func removeTipAsEntryPoint(tip *Tip) { // 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] + parentTip, ok := parentBranch.tips[parentID] + // parent is not a tip anymore + if !ok { + return + } if parentBranch.branchID == branch.branchID { // remove parents out of branch.tips diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go index 1bf532d703a37ea7f2072389dc9ff83c8c56c146..08a7d463e211e1ed1a0a8f7afff8c00a8c56cc71 100644 --- a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go @@ -1,12 +1,17 @@ package tipmanager import ( + "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() @@ -15,74 +20,193 @@ func TestTipManagerSimple(t *testing.T) { assert.Equal(t, payload.GenesisID, parent1) assert.Equal(t, payload.GenesisID, parent2) - // add few tips and check whether total tips count matches - totalTips := 100 - branchesMap := make(map[branchmanager.BranchID]*payload.Payload, totalTips) - branches := make([]branchmanager.BranchID, totalTips) - for i := 0; i < totalTips; i++ { - branchID := branchmanager.NewBranchID(transaction.RandomID()) - branches[i] = branchID - branchesMap[branchID] = payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) - tipManager.AddTip(branchesMap[branchID], branchID) - } - assert.Equal(t, totalTips, tipManager.TipCount(branches...)) + // add 1 transaction in liked branch -> both tips need to point there + tx := createDummyTransaction() + branch1 := newMockBranch(tx.ID(), true) + valueObject := payload.New(payload.GenesisID, payload.GenesisID, tx) + branch1.addTip(valueObject) + tipManager.AddTip(valueObject, branch1) - // check if tips point to genesis when branches are not found - parent1, parent2 = tipManager.Tips(branchmanager.UndefinedBranchID, branchmanager.NewBranchID(transaction.RandomID())) - assert.Equal(t, 0, tipManager.TipCount(branchmanager.UndefinedBranchID, branchmanager.NewBranchID(transaction.RandomID()))) - assert.Equal(t, payload.GenesisID, parent1) - assert.Equal(t, payload.GenesisID, parent2) + assert.Equal(t, 1, tipManager.TipCount()) + parent1, parent2 = tipManager.Tips() + assert.Equal(t, valueObject.ID(), parent1) + assert.Equal(t, valueObject.ID(), parent2) - // use each branch to get the single tip - for b, o := range branchesMap { - parent1ObjectID, parent2ObjectID := tipManager.Tips(b) - assert.Equal(t, o.ID(), parent1ObjectID) - assert.Equal(t, o.ID(), parent2ObjectID) - } + // add a bunch of liked tips -> check count + numTips := 10 + branch2 := newMockBranch(transaction.RandomID(), true) + for i := 0; i < numTips; i++ { + v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + branch1.addTip(v) + tipManager.AddTip(v, branch1) - // select tips from two branches - parent1, parent2 = tipManager.Tips(branches[0], branches[1]) - s := []payload.ID{ - branchesMap[branches[0]].ID(), - branchesMap[branches[1]].ID(), + v = payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + branch2.addTip(v) + tipManager.AddTip(v, branch2) } - assert.Contains(t, s, parent1) - assert.Contains(t, s, parent2) -} -func TestTipManagerParallel(t *testing.T) { - totalTips := 1000 - totalThreads := 100 - totalBranches := 10 + assert.Equal(t, branch1.tipCount()+branch2.tipCount(), tipManager.TipCount()) + assert.Equal(t, branch1.tipCount(), len(tipManager.branches[branch1.ID()].tips)) + assert.Equal(t, branch2.tipCount(), len(tipManager.branches[branch2.ID()].tips)) + parent1, parent2 = tipManager.Tips() + assert.NotEqual(t, parent1, parent2) - tipManager := New() + // add a bunch of disliked tips -> count should be unchanged + branch3 := newMockBranch(transaction.RandomID(), false) + branch4 := newMockBranch(transaction.RandomID(), false) + for i := 0; i < numTips; i++ { + v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + branch3.addTip(v) + tipManager.AddTip(v, branch3) - branches := make([]branchmanager.BranchID, totalBranches) - for i := 0; i < totalBranches; i++ { - branchID := branchmanager.NewBranchID(transaction.RandomID()) - branches[i] = branchID + v = payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + branch4.addTip(v) + tipManager.AddTip(v, branch4) } - // add tips in parallel + assert.Equal(t, branch1.tipCount()+branch2.tipCount(), tipManager.TipCount()) + assert.Equal(t, branch1.tipCount(), len(tipManager.branches[branch1.ID()].tips)) + assert.Equal(t, branch2.tipCount(), len(tipManager.branches[branch2.ID()].tips)) + assert.Equal(t, branch3.tipCount(), len(tipManager.branches[branch3.ID()].tips)) + assert.Equal(t, branch4.tipCount(), len(tipManager.branches[branch4.ID()].tips)) + + // add tip to liked branch with parents from same branch -> should be in tipmanager.tips and parents removed from tipmanager.tips and branch's tips + removedTip1 := branch1.tip() + removedTip2 := branch1.tip() + valueObject = payload.New(removedTip1, removedTip2, createDummyTransaction()) + branch1.addTip(valueObject) + tipManager.AddTip(valueObject, branch1) + + assert.Equal(t, branch1.tipCount()+branch2.tipCount(), tipManager.TipCount()) + _, ok := tipManager.tips.Get(removedTip1) + assert.False(t, ok) + _, ok = tipManager.tips.Get(removedTip2) + assert.False(t, ok) + _, ok = tipManager.tips.Get(valueObject.ID()) + assert.True(t, ok) + + _, ok = tipManager.branches[branch1.ID()].tips[removedTip1] + assert.False(t, ok) + _, ok = tipManager.branches[branch1.ID()].tips[removedTip2] + assert.False(t, ok) + _, ok = tipManager.branches[branch1.ID()].tips[valueObject.ID()] + assert.True(t, ok) + + // add tip to disliked branch with parents from same branch -> tipmanager.tips should be unmodified, only branch's tips modified + removedTip1 = branch3.tip() + removedTip2 = branch3.tip() + valueObject = payload.New(removedTip1, removedTip2, createDummyTransaction()) + branch3.addTip(valueObject) + tipManager.AddTip(valueObject, branch3) + + assert.Equal(t, branch1.tipCount()+branch2.tipCount(), tipManager.TipCount()) + _, ok = tipManager.branches[branch3.ID()].tips[removedTip1] + assert.False(t, ok) + _, ok = tipManager.branches[branch3.ID()].tips[removedTip2] + assert.False(t, ok) + _, ok = tipManager.branches[branch3.ID()].tips[valueObject.ID()] + assert.True(t, ok) +} + +// TestTipManagerParallel tests the TipManager's functionality by adding and selecting tips concurrently. +func TestTipManagerConcurrent(t *testing.T) { + numThreads := 10 + numTips := 100 + branches := make([]*mockBranch, numThreads) + + tipManager := New() var wg sync.WaitGroup - for i := 0; i < totalThreads; i++ { + + // add tips to numThreads branches in parallel + for i := 0; i < numThreads; i++ { wg.Add(1) - go func() { + go func(number int) { defer wg.Done() - for t := 0; t < totalTips; t++ { - tip := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) - random := rand.Intn(totalBranches) - tipManager.AddTip(tip, branches[random]) + branch := newMockBranch(transaction.RandomID(), number < numThreads/2) + branches[number] = branch + + // add tips within a branch in parallel + var wg2 sync.WaitGroup + for t := 0; t < numTips; t++ { + v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + branch.addTip(v) + wg2.Add(1) + go func() { + defer wg2.Done() + tipManager.AddTip(v, branch) + }() } - }() + wg2.Wait() + + removedTip1 := branch.tip() + removedTip2 := branch.tip() + valueObject := payload.New(removedTip1, removedTip2, createDummyTransaction()) + branch.addTip(valueObject) + tipManager.AddTip(valueObject, branch) + }(i) } wg.Wait() - // check total tip count - assert.Equal(t, totalTips*totalThreads, tipManager.TipCount(branches...)) + // verify tips of every branch + for i := 0; i < numThreads; i++ { + branch := branches[i] + assert.Equal(t, branch.tipCount(), len(tipManager.branches[branch.ID()].tips)) + + for tip := range branch.tips { + // all tips should be in branch's list + _, ok := tipManager.branches[branch.ID()].tips[tip] + assert.True(t, ok) + // make sure that all tips from liked branches (and none from disliked branches) are in tipmanager.tips + _, ok = tipManager.tips.Get(tip) + assert.Equal(t, branch.Liked(), ok) + } + } + + // total tips: numThreads/2 (liked branches) * numTips - numThreads/2 (selected tips) + assert.Equal(t, numThreads/2*numTips-numThreads/2, tipManager.TipCount()) +} + +type mockBranch struct { + id branchmanager.BranchID + liked bool + tips map[payload.ID]payload.ID +} + +func newMockBranch(txID transaction.ID, liked bool) *mockBranch { + return &mockBranch{ + id: branchmanager.NewBranchID(txID), + liked: liked, + tips: make(map[payload.ID]payload.ID), + } +} + +func (m *mockBranch) ID() branchmanager.BranchID { + return m.id +} +func (m *mockBranch) Liked() bool { + return m.liked +} + +func (m *mockBranch) addTip(valueObject *payload.Payload) { + m.tips[valueObject.ID()] = valueObject.ID() +} + +func (m *mockBranch) tip() payload.ID { + var tip payload.ID + + // get some element + for t := range m.tips { + tip = t + } + delete(m.tips, tip) + + return tip +} + +func (m *mockBranch) tipCount() int { + return len(m.tips) } -*/ func createDummyTransaction() *transaction.Transaction { return transaction.New(