diff --git a/dapps/valuetransfers/packages/tipmanager/events.go b/dapps/valuetransfers/packages/tipmanager/events.go new file mode 100644 index 0000000000000000000000000000000000000000..96f8f7d6c232ec93798264f363b9b891e3c446d1 --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/events.go @@ -0,0 +1,19 @@ +package tipmanager + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/hive.go/events" +) + +// Events represents events happening on the TipManager. +type Events struct { + // Fired when a tip is added. + TipAdded *events.Event + // Fired when a tip is removed. + TipRemoved *events.Event +} + +func payloadIDEvent(handler interface{}, params ...interface{}) { + handler.(func(payload.ID, branchmanager.BranchID))(params[0].(payload.ID), params[1].(branchmanager.BranchID)) +} diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go new file mode 100644 index 0000000000000000000000000000000000000000..a36f795a1627c67d35bc7bc4a8211c0efd218135 --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go @@ -0,0 +1,163 @@ +package tipmanager + +import ( + "math/rand" + "sync" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/packages/binary/datastructure" + "github.com/iotaledger/hive.go/events" +) + +// TipManager manages 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 +} + +// New creates a new TipManager. +func New() *TipManager { + return &TipManager{ + tips: make(map[branchmanager.BranchID]*datastructure.RandomMap), + Events: Events{ + TipAdded: events.NewEvent(payloadIDEvent), + TipRemoved: events.NewEvent(payloadIDEvent), + }, + } +} + +// 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() + + branchTips, ok := t.tips[branch] + if !ok { + // add new branch to tips map + branchTips = datastructure.NewRandomMap() + t.tips[branch] = branchTips + } + + if branchTips.Set(objectID, objectID) { + t.Events.TipAdded.Trigger(objectID, branch) + } + + // 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 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 + } + + weights := make([]int, len(branches)) + totalTips := 0 + tipsSlice := make([]*datastructure.RandomMap, len(branches)) + + t.tipsMutex.Lock() + defer t.tipsMutex.Unlock() + + // prepare weighted random selection + for i, b := range branches { + branchTips, ok := t.tips[b] + if !ok { + continue + } + + tipsSlice[i] = branchTips + weights[i] = branchTips.Size() + totalTips += weights[i] + } + + // no tips in the given branches + // TODO: what exactly to do in this case? + if totalTips == 0 { + 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) + + if totalTips == 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 = 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() + + totalTips := 0 + for _, b := range branches { + branchTips, ok := t.tips[b] + if !ok { + continue + } + + totalTips += branchTips.Size() + } + return totalTips +} + +func weightedRandom(weights []int) int { + if len(weights) == 0 { + return 0 + } + + totalWeight := 0 + for _, w := range weights { + totalWeight += w + } + r := rand.Intn(totalWeight) + + for i, w := range weights { + r -= w + if r < 0 { + return i + } + } + return len(weights) - 1 +} diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..926b7a8b91f06497c2c28f61d4a825c935aafe0d --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go @@ -0,0 +1,130 @@ +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() + + // check if first tips point to genesis + parent1, parent2 := tipManager.Tips() + 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...)) + + // 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) + + // 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) + } + + // select tips from two branches + parent1, parent2 = tipManager.Tips(branches[0], branches[1]) + s := []payload.ID{ + branchesMap[branches[0]].ID(), + branchesMap[branches[1]].ID(), + } + assert.Contains(t, s, parent1) + assert.Contains(t, s, parent2) +} + +func TestTipManagerParallel(t *testing.T) { + totalTips := 1000 + totalThreads := 100 + totalBranches := 10 + + tipManager := New() + + branches := make([]branchmanager.BranchID, totalBranches) + for i := 0; i < totalBranches; i++ { + branchID := branchmanager.NewBranchID(transaction.RandomID()) + branches[i] = branchID + } + + // add tips in parallel + var wg sync.WaitGroup + for i := 0; i < totalThreads; i++ { + wg.Add(1) + go func() { + 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]) + } + }() + } + wg.Wait() + + // 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( + // inputs + transaction.NewInputs( + transaction.NewOutputID(address.Random(), transaction.RandomID()), + transaction.NewOutputID(address.Random(), transaction.RandomID()), + ), + + // outputs + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + address.Random(): { + balance.New(balance.ColorIOTA, 1337), + }, + }), + ) +}