diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index e15bd11a7700132e6b8fe699a90496b8b8d390e7..6aaf256bede9688549cd77aeb593c31f9b4b0027 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 0000000000000000000000000000000000000000..6720e5d2f9c4361a3a4947706db17e848f16ffd1 --- /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 0000000000000000000000000000000000000000..13a4c05e28bf239b9e9b2f144aabbe7d2723d7f3 --- /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 0000000000000000000000000000000000000000..edb1ac8d1edddc0e1df8c6eb3413c19b37414eaf --- /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 0000000000000000000000000000000000000000..f3d2db45b7b979aa6884ab74c7a7efbe1f262dba --- /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), + }, + }), + ) +}