From 7db8e8e11d8b06866895d857963ecd0096f3741b Mon Sep 17 00:00:00 2001 From: Jonas Theis <mail@jonastheis.de> Date: Tue, 26 May 2020 20:24:22 +0200 Subject: [PATCH] TipManager & ValueObject factory (#417) * Add TipManager with naive locking and some basic tests * Add ValueObjectFactory * Bones * Refactor TipManager to be able to handle tips in the context of branches * Add unit tests * Add more tests * Feat: initial commit * Feat: added setPreferred to TransactionMetadata * Feat: added a Conflicting() method to the transactionMetadata * Fix: fixed logic bug * Feat: refactored fcob * Refactor: refactored additional code * Fix: fixed a bug in ForeachConsumers * Refactor: cleaned up code * Feat: implemented FCOB consensus into the valuetransfer dapp * Refactor: refactored FCOB * Docs: added some additional comments * Docs: fixed comments * Refactor: commit before branch change * Feat: added PayloadLiked Event * Refactor: fixed some missing comments + added liked to marshal * Feat: reworked the preferred and liked propagation * Refactor: cleaned up some logic * Refactor: simplified code * Refactor: cleaned up more stuff :P * Refactor: refactor * Feat: moved test + refactored fcob * Fix: fixed a few bugs in liked propagation * Rewrite TipManager to maintain a simple flat map of tips. The value transfers plugin needs to call the corresponding functions AddTip and RemoveTip when a value object is liked or disliked, respectively. * adapt to new hive.go version * upgrade hive.go * Feat: started implementing a wallet * Feat: extended wallet files * Integrate tipmanager on tangle events * Rename TipCount to Size Co-authored-by: Wolfgang Welz <welzwo@gmail.com> Co-authored-by: Hans Moog <hm@mkjc.net> --- dapps/valuetransfers/dapp.go | 38 +++++ .../valuetransfers/packages/tangle/factory.go | 45 ++++++ .../packages/tipmanager/events.go | 18 +++ .../packages/tipmanager/tipmanager.go | 83 +++++++++++ .../packages/tipmanager/tipmanager_test.go | 132 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 dapps/valuetransfers/packages/tangle/factory.go create mode 100644 dapps/valuetransfers/packages/tipmanager/events.go create mode 100644 dapps/valuetransfers/packages/tipmanager/tipmanager.go create mode 100644 dapps/valuetransfers/packages/tipmanager/tipmanager_test.go diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index e15bd11a..6aaf256b 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -1,8 +1,11 @@ package valuetransfers import ( + "sync" "time" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager" "github.com/iotaledger/goshimmer/plugins/database" "github.com/iotaledger/hive.go/daemon" "github.com/iotaledger/hive.go/events" @@ -42,6 +45,12 @@ var ( // log holds a reference to the logger used by this app. log *logger.Logger + + tipManager *tipmanager.TipManager + tipManagerOnce sync.Once + + valueObjectFactory *tangle.ValueObjectFactory + valueObjectFactoryOnce sync.Once ) func configure(_ *node.Plugin) { @@ -54,6 +63,19 @@ func configure(_ *node.Plugin) { log.Error(err) })) + // initialize tip manager and value object factory + tipManager = TipManager() + valueObjectFactory = ValueObjectFactory() + + Tangle.Events.PayloadLiked.Attach(events.NewClosure(func(cachedPayload *payload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) { + cachedMetadata.Release() + cachedPayload.Consume(tipManager.AddTip) + })) + Tangle.Events.PayloadDisliked.Attach(events.NewClosure(func(cachedPayload *payload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) { + cachedMetadata.Release() + cachedPayload.Consume(tipManager.RemoveTip) + })) + // configure FCOB consensus rules FCOB = consensus.NewFCOB(Tangle, AverageNetworkDelay) FCOB.Events.Vote.Attach(events.NewClosure(func(id string, initOpn vote.Opinion) { @@ -110,3 +132,19 @@ func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cach Tangle.AttachPayload(valuePayload) } + +// TipManager returns the TipManager singleton. +func TipManager() *tipmanager.TipManager { + tipManagerOnce.Do(func() { + tipManager = tipmanager.New() + }) + return tipManager +} + +// ValueObjectFactory returns the ValueObjectFactory singleton. +func ValueObjectFactory() *tangle.ValueObjectFactory { + valueObjectFactoryOnce.Do(func() { + valueObjectFactory = tangle.NewValueObjectFactory(TipManager()) + }) + return valueObjectFactory +} diff --git a/dapps/valuetransfers/packages/tangle/factory.go b/dapps/valuetransfers/packages/tangle/factory.go new file mode 100644 index 00000000..6720e5d2 --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/factory.go @@ -0,0 +1,45 @@ +package tangle + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tipmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/hive.go/events" +) + +// ValueObjectFactory acts as a factory to create new value objects. +type ValueObjectFactory struct { + tipManager *tipmanager.TipManager + Events *ValueObjectFactoryEvents +} + +// NewValueObjectFactory creates a new ValueObjectFactory. +func NewValueObjectFactory(tipManager *tipmanager.TipManager) *ValueObjectFactory { + return &ValueObjectFactory{ + tipManager: tipManager, + Events: &ValueObjectFactoryEvents{ + ValueObjectConstructed: events.NewEvent(valueObjectConstructedEvent), + }, + } +} + +// IssueTransaction creates a new value object including tip selection and returns it. +// It also triggers the ValueObjectConstructed event once it's done. +func (v *ValueObjectFactory) IssueTransaction(tx *transaction.Transaction) *payload.Payload { + parent1, parent2 := v.tipManager.Tips() + + valueObject := payload.New(parent1, parent2, tx) + v.Events.ValueObjectConstructed.Trigger(valueObject) + + return valueObject +} + +// ValueObjectFactoryEvents represent events happening on a ValueObjectFactory. +type ValueObjectFactoryEvents struct { + // Fired when a value object is built including tips. + ValueObjectConstructed *events.Event +} + +func valueObjectConstructedEvent(handler interface{}, params ...interface{}) { + handler.(func(*transaction.Transaction))(params[0].(*transaction.Transaction)) +} diff --git a/dapps/valuetransfers/packages/tipmanager/events.go b/dapps/valuetransfers/packages/tipmanager/events.go new file mode 100644 index 00000000..13a4c05e --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/events.go @@ -0,0 +1,18 @@ +package tipmanager + +import ( + "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))(params[0].(payload.ID)) +} diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go new file mode 100644 index 00000000..edb1ac8d --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go @@ -0,0 +1,83 @@ +package tipmanager + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/packages/binary/datastructure" + "github.com/iotaledger/hive.go/events" +) + +// TipManager manages liked tips and emits events for their removal and addition. +type TipManager struct { + // tips are all currently liked tips. + tips *datastructure.RandomMap + Events Events +} + +// New creates a new TipManager. +func New() *TipManager { + return &TipManager{ + tips: datastructure.NewRandomMap(), + Events: Events{ + TipAdded: events.NewEvent(payloadIDEvent), + TipRemoved: events.NewEvent(payloadIDEvent), + }, + } +} + +// AddTip adds the given value object as a tip. +func (t *TipManager) AddTip(valueObject *payload.Payload) { + objectID := valueObject.ID() + parent1ID := valueObject.TrunkID() + parent2ID := valueObject.BranchID() + + if t.tips.Set(objectID, objectID) { + t.Events.TipAdded.Trigger(objectID) + } + + // remove parents + if _, deleted := t.tips.Delete(parent1ID); deleted { + t.Events.TipRemoved.Trigger(parent1ID) + } + if _, deleted := t.tips.Delete(parent2ID); deleted { + t.Events.TipRemoved.Trigger(parent2ID) + } +} + +// RemoveTip removes the given value object as a tip. +func (t *TipManager) RemoveTip(valueObject *payload.Payload) { + objectID := valueObject.ID() + + if _, deleted := t.tips.Delete(objectID); deleted { + t.Events.TipRemoved.Trigger(objectID) + } +} + +// Tips returns two randomly selected tips. +func (t *TipManager) Tips() (parent1ObjectID, parent2ObjectID payload.ID) { + tip := t.tips.RandomEntry() + if tip == nil { + parent1ObjectID = payload.GenesisID + parent2ObjectID = payload.GenesisID + return + } + + parent1ObjectID = tip.(payload.ID) + + if t.tips.Size() == 1 { + parent2ObjectID = parent1ObjectID + return + } + + parent2ObjectID = t.tips.RandomEntry().(payload.ID) + // select 2 distinct tips if there's more than 1 tip available + for parent1ObjectID == parent2ObjectID && t.tips.Size() > 1 { + parent2ObjectID = t.tips.RandomEntry().(payload.ID) + } + + return +} + +// Size returns the total liked tips. +func (t *TipManager) Size() int { + return t.tips.Size() +} diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go new file mode 100644 index 00000000..f3d2db45 --- /dev/null +++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go @@ -0,0 +1,132 @@ +package tipmanager + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/hive.go/events" + "github.com/stretchr/testify/assert" +) + +// TestTipManager tests the functionality of the TipManager. +func TestTipManager(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) + + // create value object and add it + v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + tipManager.AddTip(v) + + // check count + assert.Equal(t, 1, tipManager.Size()) + + // check if both reference it + parent1, parent2 = tipManager.Tips() + assert.Equal(t, v.ID(), parent1) + assert.Equal(t, v.ID(), parent2) + + // create value object and add it + v2 := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + tipManager.AddTip(v2) + + // check count + assert.Equal(t, 2, tipManager.Size()) + + // attach new value object to previous 2 tips + parent1, parent2 = tipManager.Tips() + assert.Contains(t, []payload.ID{v.ID(), v2.ID()}, parent1) + assert.Contains(t, []payload.ID{v.ID(), v2.ID()}, parent2) + v3 := payload.New(parent1, parent2, createDummyTransaction()) + tipManager.AddTip(v3) + + // check that parents are removed + assert.Equal(t, 1, tipManager.Size()) + parent1, parent2 = tipManager.Tips() + assert.Equal(t, v3.ID(), parent1) + assert.Equal(t, v3.ID(), parent2) +} + +// TestTipManagerParallel tests the TipManager's functionality by adding and selecting tips concurrently. +func TestTipManagerConcurrent(t *testing.T) { + numThreads := 10 + numTips := 100 + numSelected := 10 + + var tipsAdded uint64 + countTipAdded := events.NewClosure(func(valueObjectID payload.ID) { + atomic.AddUint64(&tipsAdded, 1) + }) + var tipsRemoved uint64 + countTipRemoved := events.NewClosure(func(valueObjectID payload.ID) { + atomic.AddUint64(&tipsRemoved, 1) + }) + + var wg sync.WaitGroup + tipManager := New() + tipManager.Events.TipAdded.Attach(countTipAdded) + tipManager.Events.TipRemoved.Attach(countTipRemoved) + + for t := 0; t < numThreads; t++ { + wg.Add(1) + go func() { + defer wg.Done() + + tips := make(map[payload.ID]struct{}) + // add a bunch of tips + for i := 0; i < numTips; i++ { + v := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction()) + tipManager.AddTip(v) + tips[v.ID()] = struct{}{} + } + // add a bunch of tips that reference previous tips + for i := 0; i < numSelected; i++ { + v := payload.New(randomTip(tips), randomTip(tips), createDummyTransaction()) + tipManager.AddTip(v) + } + }() + } + + wg.Wait() + + // check if count matches and corresponding events have been triggered + assert.EqualValues(t, numTips*numThreads+numSelected*numThreads, tipsAdded) + assert.EqualValues(t, 2*numSelected*numThreads, tipsRemoved) + assert.EqualValues(t, numTips*numThreads-numSelected*numThreads, tipManager.Size()) +} + +func randomTip(tips map[payload.ID]struct{}) payload.ID { + var tip payload.ID + + for k := range tips { + tip = k + } + delete(tips, tip) + + return tip +} + +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), + }, + }), + ) +} -- GitLab