From 0355aee50611391e6ae4c585a5aad2e298bed16f Mon Sep 17 00:00:00 2001 From: Hans Moog <hm@mkjc.net> Date: Tue, 26 May 2020 14:45:15 +0200 Subject: [PATCH] Feat: Refactor Preferred / Liked logic (#428) * 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 * adapt to new hive.go version * upgrade hive.go * Feat: started implementing a wallet * Feat: extended wallet files * use store backed sequence * add option to use in-memory database * address review comments * Fix: fixed missing events in branchmanaer * Feat: propagate changes from branch to transaction * Feat: started implementing confirmed propagation * Fix: fixed unreachable code * Refactor: refactored some code according to wolfgangs review * Refactor: cleaned up the code according to DRY * Refactor: refactored according to wollac * Refactor: refactored according to wollac * Refactor: refactored according to wollac * Refactor: refactored the code to make it more readable * Refactor: added some doc comments + cleaned up some more code Co-authored-by: Wolfgang Welz <welzwo@gmail.com> --- dapps/valuetransfers/dapp.go | 144 ++---- .../packages/branchmanager/branch.go | 87 +++- .../packages/branchmanager/branchmanager.go | 28 ++ .../packages/branchmanager/events.go | 4 + .../valuetransfers/packages/consensus/fcob.go | 158 ++++++ .../valuetransfers/packages/tangle/events.go | 10 +- .../packages/tangle/objectstorage.go | 2 +- .../packages/tangle/payloadmetadata.go | 42 +- .../valuetransfers/packages/tangle/tangle.go | 451 +++++++++++++----- .../packages/tangle/transactionmetadata.go | 56 ++- .../packages/test/tangle_test.go | 116 +++++ .../packages/test/valuetransfers_test.go | 97 ---- dapps/valuetransfers/packages/wallet/seed.go | 26 + .../valuetransfers/packages/wallet/wallet.go | 20 + 14 files changed, 894 insertions(+), 347 deletions(-) create mode 100644 dapps/valuetransfers/packages/consensus/fcob.go create mode 100644 dapps/valuetransfers/packages/test/tangle_test.go delete mode 100644 dapps/valuetransfers/packages/test/valuetransfers_test.go create mode 100644 dapps/valuetransfers/packages/wallet/seed.go create mode 100644 dapps/valuetransfers/packages/wallet/wallet.go diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index ab27ed9d..e15bd11a 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -9,10 +9,9 @@ import ( "github.com/iotaledger/hive.go/logger" "github.com/iotaledger/hive.go/node" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/consensus" valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle" "github.com/iotaledger/goshimmer/packages/shutdown" @@ -25,7 +24,7 @@ const ( PluginName = "ValueTransfers" // AverageNetworkDelay contains the average time it takes for a network to propagate through gossip. - AverageNetworkDelay = 6 * time.Second + AverageNetworkDelay = 5 * time.Second ) var ( @@ -35,6 +34,9 @@ var ( // Tangle represents the value tangle that is used to express votes on value transactions. Tangle *tangle.Tangle + // FCOB contains the fcob consensus logic. + FCOB *consensus.FCOB + // LedgerState represents the ledger state, that keeps track of the liked branches and offers an API to access funds. LedgerState *tangle.LedgerState @@ -43,44 +45,35 @@ var ( ) func configure(_ *node.Plugin) { + // configure logger log = logger.NewLogger(PluginName) - log.Debug("configuring ValueTransfers") - - // create instances + // configure Tangle Tangle = tangle.New(database.Store()) + Tangle.Events.Error.Attach(events.NewClosure(func(err error) { + log.Error(err) + })) - // subscribe to message-layer - messagelayer.Tangle.Events.MessageSolid.Attach(events.NewClosure(onReceiveMessageFromMessageLayer)) - - // setup behavior of package instances - Tangle.Events.TransactionBooked.Attach(events.NewClosure(onTransactionBooked)) - Tangle.Events.Fork.Attach(events.NewClosure(onForkOfFirstConsumer)) - - configureFPC() - // TODO: DECIDE WHAT WE SHOULD DO IF FPC FAILS -> cry - // voter.Events().Failed.Attach(events.NewClosure(panic)) - voter.Events().Finalized.Attach(events.NewClosure(func(id string, opinion vote.Opinion) { - branchID, err := branchmanager.BranchIDFromBase58(id) - if err != nil { + // configure FCOB consensus rules + FCOB = consensus.NewFCOB(Tangle, AverageNetworkDelay) + FCOB.Events.Vote.Attach(events.NewClosure(func(id string, initOpn vote.Opinion) { + if err := voter.Vote(id, initOpn); err != nil { log.Error(err) - - return } + })) + FCOB.Events.Error.Attach(events.NewClosure(func(err error) { + log.Error(err) + })) - switch opinion { - case vote.Like: - if _, err := Tangle.BranchManager().SetBranchPreferred(branchID, true); err != nil { - panic(err) - } - // TODO: merge branch mutations into the parent branch - case vote.Dislike: - if _, err := Tangle.BranchManager().SetBranchPreferred(branchID, false); err != nil { - panic(err) - } - // TODO: merge branch mutations into the parent branch / cleanup - } + // configure FPC + link to consensus + configureFPC() + voter.Events().Finalized.Attach(events.NewClosure(FCOB.ProcessVoteResult)) + voter.Events().Failed.Attach(events.NewClosure(func(id string, lastOpinion vote.Opinion) { + log.Errorf("FPC failed for transaction with id '%s' - last opinion: '%s'", id, lastOpinion) })) + + // subscribe to message-layer + messagelayer.Tangle.Events.MessageSolid.Attach(events.NewClosure(onReceiveMessageFromMessageLayer)) } func run(*node.Plugin) { @@ -98,103 +91,22 @@ func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cach solidMessage := cachedMessage.Unwrap() if solidMessage == nil { - // TODO: LOG ERROR? + log.Debug("failed to unpack solid message from message layer") return } messagePayload := solidMessage.Payload() if messagePayload.Type() != valuepayload.Type { - // TODO: LOG ERROR? - return } valuePayload, ok := messagePayload.(*valuepayload.Payload) if !ok { - // TODO: LOG ERROR? + log.Debug("could not cast payload to value payload") return } Tangle.AttachPayload(valuePayload) } - -func onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID, decisionPending bool) { - defer cachedTransaction.Release() - defer cachedTransactionMetadata.Release() - defer cachedBranch.Release() - - if len(conflictingInputs) >= 1 { - // abort if the previous consumers where finalized already - if !decisionPending { - return - } - - branch := cachedBranch.Unwrap() - if branch == nil { - log.Error("failed to unpack branch") - - return - } - - err := voter.Vote(branch.ID().String(), vote.Dislike) - if err != nil { - log.Error(err) - } - - return - } - - // If the transaction is not conflicting, then we apply the fcob rule (we finalize after 2 network delays). - // Note: We do not set a liked flag after 1 network delay because that can be derived by the retriever later. - cachedTransactionMetadata.Retain() - time.AfterFunc(2*AverageNetworkDelay, func() { - defer cachedTransactionMetadata.Release() - - transactionMetadata := cachedTransactionMetadata.Unwrap() - if transactionMetadata == nil { - return - } - - // TODO: check that the booking goroutine in the UTXO DAG and this check is somehow synchronized - if transactionMetadata.BranchID() == branchmanager.NewBranchID(transactionMetadata.ID()) { - return - } - - transactionMetadata.SetFinalized(true) - }) -} - -// TODO: clarify what we do here -func onForkOfFirstConsumer(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) { - defer cachedTransaction.Release() - defer cachedTransactionMetadata.Release() - defer cachedBranch.Release() - - transactionMetadata := cachedTransactionMetadata.Unwrap() - if transactionMetadata == nil { - return - } - - branch := cachedBranch.Unwrap() - if branch == nil { - return - } - - if time.Since(transactionMetadata.SoldificationTime()) < AverageNetworkDelay { - if err := voter.Vote(branch.ID().String(), vote.Dislike); err != nil { - log.Error(err) - } - - return - } - - if _, err := Tangle.BranchManager().SetBranchPreferred(branch.ID(), true); err != nil { - log.Error(err) - } - - if err := voter.Vote(branch.ID().String(), vote.Like); err != nil { - log.Error(err) - } -} diff --git a/dapps/valuetransfers/packages/branchmanager/branch.go b/dapps/valuetransfers/packages/branchmanager/branch.go index 88dad184..09e862bc 100644 --- a/dapps/valuetransfers/packages/branchmanager/branch.go +++ b/dapps/valuetransfers/packages/branchmanager/branch.go @@ -21,11 +21,15 @@ type Branch struct { conflicts map[ConflictID]types.Empty preferred bool liked bool + finalized bool + confirmed bool parentBranchesMutex sync.RWMutex conflictsMutex sync.RWMutex preferredMutex sync.RWMutex likedMutex sync.RWMutex + finalizedMutex sync.RWMutex + confirmedMutex sync.RWMutex } // NewBranch is the constructor of a Branch and creates a new Branch object from the given details. @@ -211,7 +215,7 @@ func (branch *Branch) setPreferred(preferred bool) (modified bool) { branch.SetModified() modified = true - return branch.preferred + return } // Liked returns if the branch is liked (it is preferred and all of its parents are liked). @@ -246,6 +250,76 @@ func (branch *Branch) setLiked(liked bool) (modified bool) { return branch.liked } +// Finalized returns true if the branch has been marked as finalized. +func (branch *Branch) Finalized() bool { + branch.finalizedMutex.RLock() + defer branch.finalizedMutex.RUnlock() + + return branch.finalized +} + +// setFinalized is the setter for the finalized flag. It returns true if the value of the flag has been updated. +// A branch is finalized if a decisions regarding its preference has been made. +// Note: Just because a branch has been finalized, does not mean that all transactions it contains have also been +// finalized but only that the underlying conflict that created the Branch has been finalized. +func (branch *Branch) setFinalized(finalized bool) (modified bool) { + branch.finalizedMutex.RLock() + if branch.finalized == finalized { + branch.finalizedMutex.RUnlock() + + return + } + + branch.finalizedMutex.RUnlock() + branch.finalizedMutex.Lock() + defer branch.finalizedMutex.Unlock() + + if branch.finalized == finalized { + return + } + + branch.finalized = finalized + branch.SetModified() + modified = true + + return +} + +// Confirmed returns true if the branch has been accepted to be part of the ledger state. +func (branch *Branch) Confirmed() bool { + branch.confirmedMutex.RLock() + defer branch.confirmedMutex.RUnlock() + + return branch.confirmed +} + +// setConfirmed is the setter for the confirmed flag. It returns true if the value of the flag has been updated. +// A branch is confirmed if it is considered to have been accepted to be part of the ledger state. +// Note: Just because a branch has been confirmed, does not mean that all transactions it contains have also been +// confirmed but only that the underlying conflict that created the Branch has been decided. +func (branch *Branch) setConfirmed(confirmed bool) (modified bool) { + branch.confirmedMutex.RLock() + if branch.confirmed == confirmed { + branch.confirmedMutex.RUnlock() + + return + } + + branch.confirmedMutex.RUnlock() + branch.confirmedMutex.Lock() + defer branch.confirmedMutex.Unlock() + + if branch.confirmed == confirmed { + return + } + + branch.confirmed = confirmed + branch.SetModified() + modified = true + + return +} + // Bytes returns a marshaled version of this Branch. func (branch *Branch) Bytes() []byte { return marshalutil.New(). @@ -282,9 +356,10 @@ func (branch *Branch) ObjectStorageValue() []byte { parentBranches := branch.ParentBranches() parentBranchCount := len(parentBranches) - marshalUtil := marshalutil.New(2*marshalutil.BOOL_SIZE + marshalutil.UINT32_SIZE + parentBranchCount*BranchIDLength) - marshalUtil.WriteBool(branch.preferred) - marshalUtil.WriteBool(branch.liked) + marshalUtil := marshalutil.New(3*marshalutil.BOOL_SIZE + marshalutil.UINT32_SIZE + parentBranchCount*BranchIDLength) + marshalUtil.WriteBool(branch.Preferred()) + marshalUtil.WriteBool(branch.Liked()) + marshalUtil.WriteBool(branch.Confirmed()) marshalUtil.WriteUint32(uint32(parentBranchCount)) for _, branchID := range parentBranches { marshalUtil.WriteBytes(branchID.Bytes()) @@ -304,6 +379,10 @@ func (branch *Branch) UnmarshalObjectStorageValue(valueBytes []byte) (consumedBy if err != nil { return } + branch.confirmed, err = marshalUtil.ReadBool() + if err != nil { + return + } parentBranchCount, err := marshalUtil.ReadUint32() if err != nil { return diff --git a/dapps/valuetransfers/packages/branchmanager/branchmanager.go b/dapps/valuetransfers/packages/branchmanager/branchmanager.go index 5342c9ef..97100a8c 100644 --- a/dapps/valuetransfers/packages/branchmanager/branchmanager.go +++ b/dapps/valuetransfers/packages/branchmanager/branchmanager.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + "github.com/iotaledger/hive.go/events" "github.com/iotaledger/hive.go/kvstore" "github.com/iotaledger/hive.go/marshalutil" "github.com/iotaledger/hive.go/objectstorage" @@ -47,6 +48,12 @@ func New(store kvstore.KVStore) (branchManager *BranchManager) { childBranchStorage: osFactory.New(osChildBranch, osChildBranchFactory, osChildBranchOptions...), conflictStorage: osFactory.New(osConflict, osConflictFactory, osConflictOptions...), conflictMemberStorage: osFactory.New(osConflictMember, osConflictMemberFactory, osConflictMemberOptions...), + Events: &Events{ + BranchPreferred: events.NewEvent(branchCaller), + BranchUnpreferred: events.NewEvent(branchCaller), + BranchLiked: events.NewEvent(branchCaller), + BranchDisliked: events.NewEvent(branchCaller), + }, } branchManager.init() @@ -337,6 +344,10 @@ func (branchManager *BranchManager) SetBranchLiked(branchID BranchID, liked bool return branchManager.setBranchLiked(branchManager.Branch(branchID), liked) } +func (branchManager *BranchManager) SetBranchFinalized(branchID BranchID) (modified bool, err error) { + return +} + // Prune resets the database and deletes all objects (for testing or "node resets"). func (branchManager *BranchManager) Prune() (err error) { for _, storage := range []*objectstorage.ObjectStorage{ @@ -472,6 +483,23 @@ func (branchManager *BranchManager) setBranchLiked(cachedBranch *CachedBranch, l return } +// IsBranchLiked returns true if the Branch is currently marked as liked. +func (branchManager *BranchManager) IsBranchLiked(id BranchID) (liked bool) { + if id == UndefinedBranchID { + return + } + + if id == MasterBranchID { + return true + } + + branchManager.Branch(id).Consume(func(branch *Branch) { + liked = branch.Liked() + }) + + return +} + func (branchManager *BranchManager) propagateLike(cachedBranch *CachedBranch) (err error) { // unpack CachedBranch and abort of the branch doesn't exist or isn't preferred defer cachedBranch.Release() diff --git a/dapps/valuetransfers/packages/branchmanager/events.go b/dapps/valuetransfers/packages/branchmanager/events.go index 7f2cc58e..87aa61a7 100644 --- a/dapps/valuetransfers/packages/branchmanager/events.go +++ b/dapps/valuetransfers/packages/branchmanager/events.go @@ -18,3 +18,7 @@ type Events struct { // BranchLiked gets triggered whenever a Branch becomes preferred that was not preferred before. BranchDisliked *events.Event } + +func branchCaller(handler interface{}, params ...interface{}) { + handler.(func(branch *CachedBranch))(params[0].(*CachedBranch).Retain()) +} diff --git a/dapps/valuetransfers/packages/consensus/fcob.go b/dapps/valuetransfers/packages/consensus/fcob.go new file mode 100644 index 00000000..cb1069c1 --- /dev/null +++ b/dapps/valuetransfers/packages/consensus/fcob.go @@ -0,0 +1,158 @@ +package consensus + +import ( + "time" + + "github.com/iotaledger/hive.go/events" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/packages/vote" +) + +// FCOB defines the "Fast Consensus of Barcelona" rules that are used to form the initial opinions of nodes. It uses a +// local modifier based approach to reach approximate consensus within the network by waiting 1 network delay before +// setting a transaction to preferred (if it didnt see a conflict) and another network delay to set it to finalized (if +// it still didn't see a conflict). +type FCOB struct { + Events *FCOBEvents + + tangle *tangle.Tangle + averageNetworkDelay time.Duration +} + +// NewFCOB is the constructor for an FCOB consensus instance. It automatically attaches to the passed in Tangle and +// calls the corresponding Events if it needs to trigger a vote. +func NewFCOB(tangle *tangle.Tangle, averageNetworkDelay time.Duration) (fcob *FCOB) { + fcob = &FCOB{ + tangle: tangle, + averageNetworkDelay: averageNetworkDelay, + Events: &FCOBEvents{ + Error: events.NewEvent(events.ErrorCaller), + }, + } + + // setup behavior of package instances + tangle.Events.TransactionBooked.Attach(events.NewClosure(fcob.onTransactionBooked)) + tangle.Events.Fork.Attach(events.NewClosure(fcob.onFork)) + + return +} + +// ProcessVoteResult allows an external voter to hand in the results of the voting process. +func (fcob *FCOB) ProcessVoteResult(id string, opinion vote.Opinion) { + transactionID, err := transaction.IDFromBase58(id) + if err != nil { + fcob.Events.Error.Trigger(err) + + return + } + + if _, err := fcob.tangle.SetTransactionPreferred(transactionID, opinion == vote.Like); err != nil { + fcob.Events.Error.Trigger(err) + } +} + +// onTransactionBooked analyzes the transaction that was booked by the Tangle and initiates the FCOB rules if it is not +// conflicting. If it is conflicting and a decision is still pending we trigger a voting process. +func (fcob *FCOB) onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, decisionPending bool) { + defer cachedTransaction.Release() + + cachedTransactionMetadata.Consume(func(transactionMetadata *tangle.TransactionMetadata) { + if transactionMetadata.Conflicting() { + // abort if the previous consumers where finalized already + if !decisionPending { + return + } + + fcob.Events.Vote.Trigger(transactionMetadata.BranchID().String(), vote.Dislike) + + return + } + + fcob.scheduleSetPreferred(cachedTransactionMetadata.Retain()) + }) +} + +// scheduleSetPreferred schedules the setPreferred logic after 1 network delay. +func (fcob *FCOB) scheduleSetPreferred(cachedTransactionMetadata *tangle.CachedTransactionMetadata) { + if fcob.averageNetworkDelay == 0 { + fcob.setPreferred(cachedTransactionMetadata) + } else { + time.AfterFunc(fcob.averageNetworkDelay, func() { + fcob.setPreferred(cachedTransactionMetadata) + }) + } +} + +// setPreferred sets the Transaction to preferred if it is not conflicting. +func (fcob *FCOB) setPreferred(cachedTransactionMetadata *tangle.CachedTransactionMetadata) { + cachedTransactionMetadata.Consume(func(transactionMetadata *tangle.TransactionMetadata) { + if transactionMetadata.Conflicting() { + return + } + + modified, err := fcob.tangle.SetTransactionPreferred(transactionMetadata.ID(), true) + if err != nil { + fcob.Events.Error.Trigger(err) + + return + } + + if modified { + fcob.scheduleSetFinalized(cachedTransactionMetadata.Retain()) + } + }) +} + +// scheduleSetFinalized schedules the setFinalized logic after 2 network delays. +// Note: it is 2 network delays because this function gets triggered at the end of the first delay that sets a +// Transaction to preferred (see setPreferred). +func (fcob *FCOB) scheduleSetFinalized(cachedTransactionMetadata *tangle.CachedTransactionMetadata) { + if fcob.averageNetworkDelay == 0 { + fcob.setFinalized(cachedTransactionMetadata) + } else { + time.AfterFunc(fcob.averageNetworkDelay, func() { + fcob.setFinalized(cachedTransactionMetadata) + }) + } +} + +// setFinalized sets the Transaction to finalized if it is not conflicting. +func (fcob *FCOB) setFinalized(cachedTransactionMetadata *tangle.CachedTransactionMetadata) { + cachedTransactionMetadata.Consume(func(transactionMetadata *tangle.TransactionMetadata) { + if transactionMetadata.Conflicting() { + return + } + + transactionMetadata.SetFinalized(true) + }) +} + +// onFork triggers a voting process whenever a Transaction gets forked into a new Branch. The initial opinion is derived +// from the preferred flag that was set using the FCOB rule. +func (fcob *FCOB) onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata) { + defer cachedTransaction.Release() + defer cachedTransactionMetadata.Release() + + transactionMetadata := cachedTransactionMetadata.Unwrap() + if transactionMetadata == nil { + return + } + + switch transactionMetadata.Preferred() { + case true: + fcob.Events.Vote.Trigger(transactionMetadata.ID().String(), vote.Like) + case false: + fcob.Events.Vote.Trigger(transactionMetadata.ID().String(), vote.Dislike) + } +} + +// FCOBEvents acts as a dictionary for events of an FCOB instance. +type FCOBEvents struct { + // Error gets called when FCOB faces an error. + Error *events.Event + + // Vote gets called when FCOB needs to vote on a transaction. + Vote *events.Event +} diff --git a/dapps/valuetransfers/packages/tangle/events.go b/dapps/valuetransfers/packages/tangle/events.go index 5eea10a5..eaa5f33d 100644 --- a/dapps/valuetransfers/packages/tangle/events.go +++ b/dapps/valuetransfers/packages/tangle/events.go @@ -13,6 +13,8 @@ type Events struct { // Get's called whenever a transaction PayloadAttached *events.Event PayloadSolid *events.Event + PayloadLiked *events.Event + PayloadDisliked *events.Event MissingPayloadReceived *events.Event PayloadMissing *events.Event PayloadUnsolidifiable *events.Event @@ -34,6 +36,8 @@ func newEvents() *Events { return &Events{ PayloadAttached: events.NewEvent(cachedPayloadEvent), PayloadSolid: events.NewEvent(cachedPayloadEvent), + PayloadLiked: events.NewEvent(cachedPayloadEvent), + PayloadDisliked: events.NewEvent(cachedPayloadEvent), MissingPayloadReceived: events.NewEvent(cachedPayloadEvent), PayloadMissing: events.NewEvent(payloadIDEvent), PayloadUnsolidifiable: events.NewEvent(payloadIDEvent), @@ -56,12 +60,10 @@ func cachedPayloadEvent(handler interface{}, params ...interface{}) { } func transactionBookedEvent(handler interface{}, params ...interface{}) { - handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *branchmanager.CachedBranch, []transaction.OutputID, bool))( + handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, bool))( params[0].(*transaction.CachedTransaction).Retain(), params[1].(*CachedTransactionMetadata).Retain(), - params[2].(*branchmanager.CachedBranch).Retain(), - params[3].([]transaction.OutputID), - params[4].(bool), + params[2].(bool), ) } diff --git a/dapps/valuetransfers/packages/tangle/objectstorage.go b/dapps/valuetransfers/packages/tangle/objectstorage.go index a549f1a0..7c1eb49f 100644 --- a/dapps/valuetransfers/packages/tangle/objectstorage.go +++ b/dapps/valuetransfers/packages/tangle/objectstorage.go @@ -27,7 +27,7 @@ const ( var ( osLeakDetectionOption = objectstorage.LeakDetectionEnabled(true, objectstorage.LeakDetectionOptions{ - MaxConsumersPerObject: 10, + MaxConsumersPerObject: 20, MaxConsumerHoldTime: 10 * time.Second, }) ) diff --git a/dapps/valuetransfers/packages/tangle/payloadmetadata.go b/dapps/valuetransfers/packages/tangle/payloadmetadata.go index 86d06fd5..39840272 100644 --- a/dapps/valuetransfers/packages/tangle/payloadmetadata.go +++ b/dapps/valuetransfers/packages/tangle/payloadmetadata.go @@ -20,10 +20,12 @@ type PayloadMetadata struct { payloadID payload.ID solid bool solidificationTime time.Time + liked bool branchID branchmanager.BranchID solidMutex sync.RWMutex solidificationTimeMutex sync.RWMutex + likedMutex sync.RWMutex branchIDMutex sync.RWMutex } @@ -139,6 +141,38 @@ func (payloadMetadata *PayloadMetadata) SoldificationTime() time.Time { return payloadMetadata.solidificationTime } +// Liked returns true if the Payload was marked as liked. +func (payloadMetadata *PayloadMetadata) Liked() bool { + payloadMetadata.likedMutex.RLock() + defer payloadMetadata.likedMutex.RUnlock() + + return payloadMetadata.liked +} + +// SetLiked modifies the liked flag of the given Payload. It returns true if the value has been updated. +func (payloadMetadata *PayloadMetadata) SetLiked(liked bool) (modified bool) { + payloadMetadata.likedMutex.RLock() + if payloadMetadata.liked == liked { + payloadMetadata.likedMutex.RUnlock() + + return + } + + payloadMetadata.likedMutex.RUnlock() + payloadMetadata.likedMutex.Lock() + defer payloadMetadata.likedMutex.Unlock() + + if payloadMetadata.liked == liked { + return + } + + payloadMetadata.liked = liked + payloadMetadata.SetModified() + modified = true + + return +} + // BranchID returns the identifier of the Branch that this Payload was booked into. func (payloadMetadata *PayloadMetadata) BranchID() branchmanager.BranchID { payloadMetadata.branchIDMutex.RLock() @@ -173,7 +207,7 @@ func (payloadMetadata *PayloadMetadata) SetBranchID(branchID branchmanager.Branc // Bytes marshals the metadata into a sequence of bytes. func (payloadMetadata *PayloadMetadata) Bytes() []byte { - return marshalutil.New(payload.IDLength + marshalutil.TIME_SIZE + marshalutil.BOOL_SIZE + branchmanager.BranchIDLength). + return marshalutil.New(payload.IDLength + marshalutil.TIME_SIZE + 2*marshalutil.BOOL_SIZE + branchmanager.BranchIDLength). WriteBytes(payloadMetadata.ObjectStorageKey()). WriteBytes(payloadMetadata.ObjectStorageValue()). Bytes() @@ -202,9 +236,10 @@ func (payloadMetadata *PayloadMetadata) Update(other objectstorage.StorableObjec // ObjectStorageValue is required to match the encoding.BinaryMarshaler interface. func (payloadMetadata *PayloadMetadata) ObjectStorageValue() []byte { - return marshalutil.New(marshalutil.TIME_SIZE + marshalutil.BOOL_SIZE). + return marshalutil.New(marshalutil.TIME_SIZE + 2*marshalutil.BOOL_SIZE). WriteTime(payloadMetadata.solidificationTime). WriteBool(payloadMetadata.solid). + WriteBool(payloadMetadata.liked). WriteBytes(payloadMetadata.branchID.Bytes()). Bytes() } @@ -218,6 +253,9 @@ func (payloadMetadata *PayloadMetadata) UnmarshalObjectStorageValue(data []byte) if payloadMetadata.solid, err = marshalUtil.ReadBool(); err != nil { return } + if payloadMetadata.liked, err = marshalUtil.ReadBool(); err != nil { + return + } if payloadMetadata.branchID, err = branchmanager.ParseBranchID(marshalUtil); err != nil { return } diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go index ac495ff9..eaa3bba7 100644 --- a/dapps/valuetransfers/packages/tangle/tangle.go +++ b/dapps/valuetransfers/packages/tangle/tangle.go @@ -8,6 +8,7 @@ import ( "time" "github.com/iotaledger/hive.go/async" + "github.com/iotaledger/hive.go/events" "github.com/iotaledger/hive.go/kvstore" "github.com/iotaledger/hive.go/objectstorage" "github.com/iotaledger/hive.go/types" @@ -54,16 +55,43 @@ func New(store kvstore.KVStore) (result *Tangle) { approverStorage: osFactory.New(osApprover, osPayloadApproverFactory, objectstorage.CacheTime(time.Second), objectstorage.PartitionKey(payload.IDLength, payload.IDLength), objectstorage.KeysOnly(true)), transactionStorage: osFactory.New(osTransaction, osTransactionFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), transactionMetadataStorage: osFactory.New(osTransactionMetadata, osTransactionMetadataFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - attachmentStorage: osFactory.New(osAttachment, osAttachmentFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), + attachmentStorage: osFactory.New(osAttachment, osAttachmentFactory, objectstorage.CacheTime(time.Second), objectstorage.PartitionKey(transaction.IDLength, payload.IDLength), osLeakDetectionOption), outputStorage: osFactory.New(osOutput, osOutputFactory, OutputKeyPartitions, objectstorage.CacheTime(time.Second), osLeakDetectionOption), consumerStorage: osFactory.New(osConsumer, osConsumerFactory, ConsumerPartitionKeys, objectstorage.CacheTime(time.Second), osLeakDetectionOption), Events: *newEvents(), } + result.branchManager.Events.BranchPreferred.Attach(events.NewClosure(func(cachedBranch *branchmanager.CachedBranch) { + result.propagateBranchPreferredChangesToTransaction(cachedBranch, true) + })) + result.branchManager.Events.BranchUnpreferred.Attach(events.NewClosure(func(cachedBranch *branchmanager.CachedBranch) { + result.propagateBranchPreferredChangesToTransaction(cachedBranch, false) + })) + return } +// propagateBranchPreferredChangesToTransaction updates the preferred flag of a transaction, whenever the preferred +// status of its corresponding branch changes. +func (tangle *Tangle) propagateBranchPreferredChangesToTransaction(cachedBranch *branchmanager.CachedBranch, preferred bool) { + cachedBranch.Consume(func(branch *branchmanager.Branch) { + if !branch.IsAggregated() { + transactionID, _, err := transaction.IDFromBytes(branch.ID().Bytes()) + if err != nil { + panic(err) // this should never ever happen + } + + _, err = tangle.SetTransactionPreferred(transactionID, preferred) + if err != nil { + tangle.Events.Error.Trigger(err) + + return + } + } + }) +} + // BranchManager is the getter for the manager that takes care of creating and updating branches. func (tangle *Tangle) BranchManager() *branchmanager.BranchManager { return tangle.branchManager @@ -110,7 +138,180 @@ func (tangle *Tangle) Attachments(transactionID transaction.ID) CachedAttachment // AttachPayload adds a new payload to the value tangle. func (tangle *Tangle) AttachPayload(payload *payload.Payload) { - tangle.workerPool.Submit(func() { tangle.storePayloadWorker(payload) }) + tangle.workerPool.Submit(func() { tangle.AttachPayloadSync(payload) }) +} + +// SetTransactionFinalized modifies the finalized flag of a transaction. It updates the transactions metadata and +// propagates the changes to the BranchManager if the flag was updated. +func (tangle *Tangle) SetTransactionFinalized(transactionID transaction.ID) (modified bool, err error) { + tangle.TransactionMetadata(transactionID).Consume(func(metadata *TransactionMetadata) { + // update the finalized flag of the transaction + modified = metadata.SetFinalized(true) + + // only propagate the changes if the flag was modified + if modified { + // propagate changes to the branches (UTXO DAG) + if metadata.Conflicting() { + _, err = tangle.branchManager.SetBranchFinalized(metadata.BranchID()) + if err != nil { + tangle.Events.Error.Trigger(err) + + return + } + } + + // propagate changes to future cone of transaction (value tangle) + tangle.propagateValuePayloadConfirmedUpdates(transactionID) + } + }) + + return +} + +// TODO: WRITE COMMENT +func (tangle *Tangle) propagateValuePayloadConfirmedUpdates(transactionID transaction.ID) { + panic("not yet implemented") +} + +// SetTransactionPreferred modifies the preferred flag of a transaction. It updates the transactions metadata and +// propagates the changes to the BranchManager if the flag was updated. +func (tangle *Tangle) SetTransactionPreferred(transactionID transaction.ID, preferred bool) (modified bool, err error) { + tangle.TransactionMetadata(transactionID).Consume(func(metadata *TransactionMetadata) { + // update the preferred flag of the transaction + modified = metadata.setPreferred(preferred) + + // only propagate the changes if the flag was modified + if modified { + // propagate changes to the branches (UTXO DAG) + if metadata.Conflicting() { + _, err = tangle.branchManager.SetBranchPreferred(metadata.BranchID(), preferred) + if err != nil { + tangle.Events.Error.Trigger(err) + + return + } + } + + // propagate changes to future cone of transaction (value tangle) + tangle.propagateValuePayloadLikeUpdates(transactionID, preferred) + } + }) + + return +} + +// propagateValuePayloadLikeUpdates updates the liked status of all value payloads attaching a certain transaction. If +// the transaction that was updated was the entry point to a branch then all value payloads inside this branch get +// updated as well (updates happen from past to presents). +func (tangle *Tangle) propagateValuePayloadLikeUpdates(transactionID transaction.ID, liked bool) { + // initiate stack with the attachments of the passed in transaction + propagationStack := list.New() + tangle.Attachments(transactionID).Consume(func(attachment *Attachment) { + propagationStack.PushBack(&valuePayloadPropagationStackEntry{ + CachedPayload: tangle.Payload(attachment.PayloadID()), + CachedPayloadMetadata: tangle.PayloadMetadata(attachment.PayloadID()), + CachedTransaction: tangle.Transaction(transactionID), + CachedTransactionMetadata: tangle.TransactionMetadata(transactionID), + }) + }) + + // keep track of the seen payloads so we do not process them twice + seenPayloads := make(map[payload.ID]types.Empty) + + // iterate through stack (future cone of transactions) + for propagationStack.Len() >= 1 { + currentAttachmentEntry := propagationStack.Front() + tangle.processValuePayloadLikedUpdateStackEntry(propagationStack, seenPayloads, liked, currentAttachmentEntry.Value.(*valuePayloadPropagationStackEntry)) + propagationStack.Remove(currentAttachmentEntry) + } +} + +// processValuePayloadLikedUpdateStackEntry is an internal utility method that processes a single entry of the +// propagation stack for the update of the liked flag when iterating through the future cone of a transactions +// attachments. It checks if a ValuePayloads has become liked (or disliked), updates the flag an schedules its future +// cone for additional checks. +func (tangle *Tangle) processValuePayloadLikedUpdateStackEntry(propagationStack *list.List, processedPayloads map[payload.ID]types.Empty, liked bool, propagationStackEntry *valuePayloadPropagationStackEntry) { + // release the entry when we are done + defer propagationStackEntry.Release() + + // unpack loaded objects and abort if the entities could not be loaded from the database + currentPayload, currentPayloadMetadata, currentTransaction, currentTransactionMetadata := propagationStackEntry.Unwrap() + if currentPayload == nil || currentPayloadMetadata == nil || currentTransaction == nil || currentTransactionMetadata == nil { + return + } + + // perform different logic depending on the type of the change (liked vs dislike) + switch liked { + case true: + // abort if the transaction is not preferred, the branch of the payload is not liked, the referenced value payloads are not liked or the payload was marked as liked before + if !currentTransactionMetadata.Preferred() || !tangle.BranchManager().IsBranchLiked(currentPayloadMetadata.BranchID()) || !tangle.ValuePayloadsLiked(currentPayload.TrunkID(), currentPayload.BranchID()) || !currentPayloadMetadata.SetLiked(liked) { + return + } + + tangle.Events.PayloadLiked.Trigger(propagationStackEntry.CachedPayload, propagationStackEntry.CachedPayloadMetadata) + case false: + // abort if the payload has been marked as disliked before + if !currentPayloadMetadata.SetLiked(liked) { + return + } + + tangle.Events.PayloadDisliked.Trigger(propagationStackEntry.CachedPayload, propagationStackEntry.CachedPayloadMetadata) + } + + // schedule checks of approvers and consumers + tangle.ForEachConsumersAndApprovers(currentPayload, tangle.createValuePayloadFutureConeIterator(propagationStack, processedPayloads)) +} + +// createValuePayloadFutureConeIterator returns a function that can be handed into the ForEachConsumersAndApprovers +// method, that iterates through the next level of the future cone of the given transaction and adds the found elements +// to the given stack. +func (tangle *Tangle) createValuePayloadFutureConeIterator(propagationStack *list.List, processedPayloads map[payload.ID]types.Empty) func(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *CachedPayloadMetadata, cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) { + return func(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *CachedPayloadMetadata, cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) { + // automatically release cached objects when we terminate + defer cachedPayload.Release() + defer cachedPayloadMetadata.Release() + defer cachedTransaction.Release() + defer cachedTransactionMetadata.Release() + + // abort if the payload could not be unwrapped + unwrappedPayload := cachedPayload.Unwrap() + if unwrappedPayload == nil { + return + } + + // abort if we have scheduled the check of this payload already + if _, payloadProcessedAlready := processedPayloads[unwrappedPayload.ID()]; payloadProcessedAlready { + return + } + processedPayloads[unwrappedPayload.ID()] = types.Void + + // schedule next checks + propagationStack.PushBack(&valuePayloadPropagationStackEntry{ + CachedPayload: cachedPayload.Retain(), + CachedPayloadMetadata: cachedPayloadMetadata.Retain(), + CachedTransaction: cachedTransaction.Retain(), + CachedTransactionMetadata: cachedTransactionMetadata.Retain(), + }) + } +} + +// ValuePayloadsLiked is checking if the Payloads referenced by the passed in IDs are all liked. +func (tangle *Tangle) ValuePayloadsLiked(payloadIDs ...payload.ID) (liked bool) { + for _, payloadID := range payloadIDs { + if payloadID == payload.GenesisID { + continue + } + + payloadMetadataFound := tangle.PayloadMetadata(payloadID).Consume(func(payloadMetadata *PayloadMetadata) { + liked = payloadMetadata.Liked() + }) + + if !payloadMetadataFound || !liked { + return + } + } + + return true } // Payload retrieves a payload from the object storage. @@ -182,8 +383,8 @@ func (tangle *Tangle) Prune() (err error) { return } -// storePayloadWorker is the worker function that stores the payload and calls the corresponding storage events. -func (tangle *Tangle) storePayloadWorker(payloadToStore *payload.Payload) { +// AttachPayloadSync is the worker function that stores the payload and calls the corresponding storage events. +func (tangle *Tangle) AttachPayloadSync(payloadToStore *payload.Payload) { // store the payload models or abort if we have seen the payload already cachedPayload, cachedPayloadMetadata, payloadStored := tangle.storePayload(payloadToStore) if !payloadStored { @@ -272,131 +473,100 @@ func (tangle *Tangle) storePayloadReferences(payload *payload.Payload) { // store branch approver if branchID := payload.BranchID(); branchID != trunkID { - tangle.approverStorage.Store(NewPayloadApprover(branchID, trunkID)).Release() + tangle.approverStorage.Store(NewPayloadApprover(branchID, payload.ID())).Release() } } -func (tangle *Tangle) popElementsFromSolidificationStack(stack *list.List) (*payload.CachedPayload, *CachedPayloadMetadata, *transaction.CachedTransaction, *CachedTransactionMetadata) { - currentSolidificationEntry := stack.Front() - currentCachedPayload := currentSolidificationEntry.Value.([4]interface{})[0].(*payload.CachedPayload) - currentCachedMetadata := currentSolidificationEntry.Value.([4]interface{})[1].(*CachedPayloadMetadata) - currentCachedTransaction := currentSolidificationEntry.Value.([4]interface{})[2].(*transaction.CachedTransaction) - currentCachedTransactionMetadata := currentSolidificationEntry.Value.([4]interface{})[3].(*CachedTransactionMetadata) - stack.Remove(currentSolidificationEntry) - - return currentCachedPayload, currentCachedMetadata, currentCachedTransaction, currentCachedTransactionMetadata -} - // solidifyPayload is the worker function that solidifies the payloads (recursively from past to present). func (tangle *Tangle) solidifyPayload(cachedPayload *payload.CachedPayload, cachedMetadata *CachedPayloadMetadata, cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) { // initialize the stack solidificationStack := list.New() - solidificationStack.PushBack([4]interface{}{cachedPayload, cachedMetadata, cachedTransaction, cachedTransactionMetadata}) + solidificationStack.PushBack(&valuePayloadPropagationStackEntry{ + CachedPayload: cachedPayload, + CachedPayloadMetadata: cachedMetadata, + CachedTransaction: cachedTransaction, + CachedTransactionMetadata: cachedTransactionMetadata, + }) + + // keep track of the added payloads so we do not add them multiple times + processedPayloads := make(map[payload.ID]types.Empty) // process payloads that are supposed to be checked for solidity recursively for solidificationStack.Len() > 0 { - // retrieve cached objects - currentCachedPayload, currentCachedMetadata, currentCachedTransaction, currentCachedTransactionMetadata := tangle.popElementsFromSolidificationStack(solidificationStack) - - // unwrap cached objects - currentPayload := currentCachedPayload.Unwrap() - currentPayloadMetadata := currentCachedMetadata.Unwrap() - currentTransaction := currentCachedTransaction.Unwrap() - currentTransactionMetadata := currentCachedTransactionMetadata.Unwrap() - - // abort if any of the retrieved models are nil - if currentPayload == nil || currentPayloadMetadata == nil || currentTransaction == nil || currentTransactionMetadata == nil { - currentCachedPayload.Release() - currentCachedMetadata.Release() - currentCachedTransaction.Release() - currentCachedTransactionMetadata.Release() - - return - } - - // abort if the transaction is not solid or invalid - transactionSolid, consumedBranches, err := tangle.checkTransactionSolidity(currentTransaction, currentTransactionMetadata) - if err != nil || !transactionSolid { - if err != nil { - // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT - fmt.Println(err, currentTransaction) - } + currentSolidificationEntry := solidificationStack.Front() + tangle.processSolidificationStackEntry(solidificationStack, processedPayloads, currentSolidificationEntry.Value.(*valuePayloadPropagationStackEntry)) + solidificationStack.Remove(currentSolidificationEntry) + } +} - currentCachedPayload.Release() - currentCachedMetadata.Release() - currentCachedTransaction.Release() - currentCachedTransactionMetadata.Release() +// processSolidificationStackEntry processes a single entry of the solidification stack and schedules its approvers and +// consumers if necessary. +func (tangle *Tangle) processSolidificationStackEntry(solidificationStack *list.List, processedPayloads map[payload.ID]types.Empty, solidificationStackEntry *valuePayloadPropagationStackEntry) { + // release stack entry when we are done + defer solidificationStackEntry.Release() - return - } - - // abort if the payload is not solid or invalid - payloadSolid, err := tangle.checkPayloadSolidity(currentPayload, currentPayloadMetadata, consumedBranches) - if err != nil || !payloadSolid { - if err != nil { - // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT - fmt.Println(err, currentTransaction) - } + // unwrap and abort if any of the retrieved models are nil + currentPayload, currentPayloadMetadata, currentTransaction, currentTransactionMetadata := solidificationStackEntry.Unwrap() + if currentPayload == nil || currentPayloadMetadata == nil || currentTransaction == nil || currentTransactionMetadata == nil { + return + } - currentCachedPayload.Release() - currentCachedMetadata.Release() - currentCachedTransaction.Release() - currentCachedTransactionMetadata.Release() + // abort if the transaction is not solid or invalid + transactionSolid, consumedBranches, transactionSolidityErr := tangle.checkTransactionSolidity(currentTransaction, currentTransactionMetadata) + if transactionSolidityErr != nil { + // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT - return - } + return + } + if !transactionSolid { + return + } - // book the solid entities - transactionBooked, payloadBooked, bookingErr := tangle.book(currentCachedPayload.Retain(), currentCachedMetadata.Retain(), currentCachedTransaction.Retain(), currentCachedTransactionMetadata.Retain()) - if bookingErr != nil { - tangle.Events.Error.Trigger(bookingErr) + // abort if the payload is not solid or invalid + payloadSolid, payloadSolidityErr := tangle.checkPayloadSolidity(currentPayload, currentPayloadMetadata, consumedBranches) + if payloadSolidityErr != nil { + // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT - currentCachedPayload.Release() - currentCachedMetadata.Release() - currentCachedTransaction.Release() - currentCachedTransactionMetadata.Release() + return + } + if !payloadSolid { + return + } - return - } + // book the solid entities + transactionBooked, payloadBooked, decisionPending, bookingErr := tangle.book(solidificationStackEntry.Retain()) + if bookingErr != nil { + tangle.Events.Error.Trigger(bookingErr) - if transactionBooked { - tangle.ForEachConsumers(currentTransaction, func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment) { - solidificationStack.PushBack([3]interface{}{cachedTransaction, transactionMetadata, cachedAttachment}) - }) - } + return + } - if payloadBooked { - // ... and schedule check of approvers - tangle.ForeachApprovers(currentPayload.ID(), func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata) { - solidificationStack.PushBack([4]interface{}{payload, payloadMetadata, transaction, transactionMetadata}) - }) - } + // trigger events and schedule check of approvers / consumers + if transactionBooked { + tangle.Events.TransactionBooked.Trigger(solidificationStackEntry.CachedTransaction, solidificationStackEntry.CachedTransactionMetadata, decisionPending) - currentCachedPayload.Release() - currentCachedMetadata.Release() - currentCachedTransaction.Release() - currentCachedTransactionMetadata.Release() + tangle.ForEachConsumers(currentTransaction, tangle.createValuePayloadFutureConeIterator(solidificationStack, processedPayloads)) + } + if payloadBooked { + tangle.ForeachApprovers(currentPayload.ID(), tangle.createValuePayloadFutureConeIterator(solidificationStack, processedPayloads)) } } -func (tangle *Tangle) book(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *CachedPayloadMetadata, cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (transactionBooked bool, payloadBooked bool, err error) { - defer cachedPayload.Release() - defer cachedPayloadMetadata.Release() - defer cachedTransaction.Release() - defer cachedTransactionMetadata.Release() +func (tangle *Tangle) book(entitiesToBook *valuePayloadPropagationStackEntry) (transactionBooked bool, payloadBooked bool, decisionPending bool, err error) { + defer entitiesToBook.Release() - if transactionBooked, err = tangle.bookTransaction(cachedTransaction.Retain(), cachedTransactionMetadata.Retain()); err != nil { + if transactionBooked, decisionPending, err = tangle.bookTransaction(entitiesToBook.CachedTransaction.Retain(), entitiesToBook.CachedTransactionMetadata.Retain()); err != nil { return } - if payloadBooked, err = tangle.bookPayload(cachedPayload.Retain(), cachedPayloadMetadata.Retain(), cachedTransactionMetadata.Retain()); err != nil { + if payloadBooked, err = tangle.bookPayload(entitiesToBook.CachedPayload.Retain(), entitiesToBook.CachedPayloadMetadata.Retain(), entitiesToBook.CachedTransactionMetadata.Retain()); err != nil { return } return } -func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (transactionBooked bool, err error) { +func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (transactionBooked bool, decisionPending bool, err error) { defer cachedTransaction.Release() defer cachedTransactionMetadata.Release() @@ -455,6 +625,9 @@ func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTrans conflictingInputsOfFirstConsumers[firstConsumerID] = append(conflictingInputsOfFirstConsumers[firstConsumerID], outputID) } + // mark input as conflicting + conflictingInputs = append(conflictingInputs, outputID) + return true }) { return @@ -499,7 +672,6 @@ func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTrans }) // fork the conflicting transactions into their own branch - decisionPending := false for consumerID, conflictingInputs := range conflictingInputsOfFirstConsumers { _, decisionFinalized, forkedErr := tangle.Fork(consumerID, conflictingInputs) if forkedErr != nil { @@ -510,10 +682,6 @@ func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTrans decisionPending = decisionPending || !decisionFinalized } - - // trigger events - tangle.Events.TransactionBooked.Trigger(cachedTransaction, cachedTransactionMetadata, cachedTargetBranch, conflictingInputs, decisionPending) - transactionBooked = true return @@ -559,11 +727,10 @@ func (tangle *Tangle) bookPayload(cachedPayload *payload.CachedPayload, cachedPa // ForeachApprovers iterates through the approvers of a payload and calls the passed in consumer function. func (tangle *Tangle) ForeachApprovers(payloadID payload.ID, consume func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata)) { tangle.Approvers(payloadID).Consume(func(approver *PayloadApprover) { - approvingPayloadID := approver.ApprovingPayloadID() - approvingCachedPayload := tangle.Payload(approvingPayloadID) + approvingCachedPayload := tangle.Payload(approver.ApprovingPayloadID()) approvingCachedPayload.Consume(func(payload *payload.Payload) { - consume(approvingCachedPayload, tangle.PayloadMetadata(approvingPayloadID), tangle.Transaction(payload.Transaction().ID()), tangle.TransactionMetadata(payload.Transaction().ID())) + consume(approvingCachedPayload.Retain(), tangle.PayloadMetadata(approver.ApprovingPayloadID()), tangle.Transaction(payload.Transaction().ID()), tangle.TransactionMetadata(payload.Transaction().ID())) }) }) } @@ -686,6 +853,7 @@ func (tangle *Tangle) LoadSnapshot(snapshot map[transaction.ID]map[address.Addre for outputAddress, balances := range addressBalances { input := NewOutput(outputAddress, transactionID, branchmanager.MasterBranchID, balances) input.SetSolid(true) + input.SetBranchID(branchmanager.MasterBranchID) // store output and abort if the snapshot has already been loaded earlier (output exists in the database) cachedOutput, stored := tangle.outputStorage.StoreIfAbsent(input) @@ -878,19 +1046,19 @@ func (tangle *Tangle) Fork(transactionID transaction.ID, conflictingInputs []tra } // update / create new branch - cachedTargetBranch, newBranchCreated := tangle.branchManager.Fork(branchmanager.NewBranchID(tx.ID()), []branchmanager.BranchID{txMetadata.BranchID()}, conflictingInputs) + newBranchID := branchmanager.NewBranchID(tx.ID()) + cachedTargetBranch, newBranchCreated := tangle.branchManager.Fork(newBranchID, []branchmanager.BranchID{txMetadata.BranchID()}, conflictingInputs) defer cachedTargetBranch.Release() - // abort if the branch existed already - if !newBranchCreated { - return + // set branch to be preferred if the underlying transaction was marked as preferred + if txMetadata.Preferred() { + if _, err = tangle.branchManager.SetBranchPreferred(newBranchID, true); err != nil { + return + } } - // unpack branch - targetBranch := cachedTargetBranch.Unwrap() - if targetBranch == nil { - err = fmt.Errorf("failed to unpack branch for transaction '%s'", transactionID) - + // abort if the branch existed already + if !newBranchCreated { return } @@ -900,7 +1068,7 @@ func (tangle *Tangle) Fork(transactionID transaction.ID, conflictingInputs []tra } // trigger events + set result - tangle.Events.Fork.Trigger(cachedTransaction, cachedTransactionMetadata, targetBranch, conflictingInputs) + tangle.Events.Fork.Trigger(cachedTransaction, cachedTransactionMetadata) forked = true return @@ -1064,7 +1232,7 @@ func (tangle *Tangle) calculateBranchOfTransaction(currentTransaction *transacti } // ForEachConsumers iterates through the transactions that are consuming outputs of the given transactions -func (tangle *Tangle) ForEachConsumers(currentTransaction *transaction.Transaction, consume func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment)) { +func (tangle *Tangle) ForEachConsumers(currentTransaction *transaction.Transaction, consume func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata)) { seenTransactions := make(map[transaction.ID]types.Empty) currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool { tangle.Consumers(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(consumer *Consumer) { @@ -1072,13 +1240,60 @@ func (tangle *Tangle) ForEachConsumers(currentTransaction *transaction.Transacti seenTransactions[consumer.TransactionID()] = types.Void cachedTransaction := tangle.Transaction(consumer.TransactionID()) + defer cachedTransaction.Release() + cachedTransactionMetadata := tangle.TransactionMetadata(consumer.TransactionID()) - for _, cachedAttachment := range tangle.Attachments(consumer.TransactionID()) { - consume(cachedTransaction, cachedTransactionMetadata, cachedAttachment) - } + defer cachedTransactionMetadata.Release() + + tangle.Attachments(consumer.TransactionID()).Consume(func(attachment *Attachment) { + consume(tangle.Payload(attachment.PayloadID()), tangle.PayloadMetadata(attachment.PayloadID()), cachedTransaction.Retain(), cachedTransactionMetadata.Retain()) + }) } }) return true }) } + +// ForEachConsumersAndApprovers calls the passed in consumer for all payloads that either approve the given payload or +// that attach a transaction that spends outputs from the transaction inside the given payload. +func (tangle *Tangle) ForEachConsumersAndApprovers(currentPayload *payload.Payload, consume func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata)) { + tangle.ForEachConsumers(currentPayload.Transaction(), consume) + tangle.ForeachApprovers(currentPayload.ID(), consume) +} + +// valuePayloadPropagationStackEntry is a container for the elements in the propagation stack of ValuePayloads +type valuePayloadPropagationStackEntry struct { + CachedPayload *payload.CachedPayload + CachedPayloadMetadata *CachedPayloadMetadata + CachedTransaction *transaction.CachedTransaction + CachedTransactionMetadata *CachedTransactionMetadata +} + +// Retain creates a new container with its contained elements being retained for further use. +func (stackEntry *valuePayloadPropagationStackEntry) Retain() *valuePayloadPropagationStackEntry { + return &valuePayloadPropagationStackEntry{ + CachedPayload: stackEntry.CachedPayload.Retain(), + CachedPayloadMetadata: stackEntry.CachedPayloadMetadata.Retain(), + CachedTransaction: stackEntry.CachedTransaction.Retain(), + CachedTransactionMetadata: stackEntry.CachedTransactionMetadata.Retain(), + } +} + +// Release releases the elements in this container for being written by the objectstorage. +func (stackEntry *valuePayloadPropagationStackEntry) Release() { + stackEntry.CachedPayload.Release() + stackEntry.CachedPayloadMetadata.Release() + stackEntry.CachedTransaction.Release() + stackEntry.CachedTransactionMetadata.Release() +} + +// Unwrap retrieves the underlying StorableObjects from the cached elements in this container. +func (stackEntry *valuePayloadPropagationStackEntry) Unwrap() (payload *payload.Payload, payloadMetadata *PayloadMetadata, transaction *transaction.Transaction, transactionMetadata *TransactionMetadata) { + payload = stackEntry.CachedPayload.Unwrap() + payloadMetadata = stackEntry.CachedPayloadMetadata.Unwrap() + transaction = stackEntry.CachedTransaction.Unwrap() + transactionMetadata = stackEntry.CachedTransactionMetadata.Unwrap() + + return +} diff --git a/dapps/valuetransfers/packages/tangle/transactionmetadata.go b/dapps/valuetransfers/packages/tangle/transactionmetadata.go index 7bda5190..af40906b 100644 --- a/dapps/valuetransfers/packages/tangle/transactionmetadata.go +++ b/dapps/valuetransfers/packages/tangle/transactionmetadata.go @@ -20,12 +20,14 @@ type TransactionMetadata struct { id transaction.ID branchID branchmanager.BranchID solid bool + preferred bool finalized bool solidificationTime time.Time finalizationTime time.Time branchIDMutex sync.RWMutex solidMutex sync.RWMutex + preferredMutex sync.RWMutex finalizedMutex sync.RWMutex solidificationTimeMutex sync.RWMutex } @@ -129,6 +131,11 @@ func (transactionMetadata *TransactionMetadata) SetBranchID(branchID branchmanag return } +// Conflicting returns true if the Transaction has been forked into its own Branch and there is a vote going on. +func (transactionMetadata *TransactionMetadata) Conflicting() bool { + return transactionMetadata.BranchID() == branchmanager.NewBranchID(transactionMetadata.ID()) +} + // Solid returns true if the Transaction has been marked as solid. func (transactionMetadata *TransactionMetadata) Solid() (result bool) { transactionMetadata.solidMutex.RLock() @@ -167,6 +174,40 @@ func (transactionMetadata *TransactionMetadata) SetSolid(solid bool) (modified b return } +// Preferred returns true if the transaction is considered to be the first valid spender of all of its Inputs. +func (transactionMetadata *TransactionMetadata) Preferred() (result bool) { + transactionMetadata.preferredMutex.RLock() + defer transactionMetadata.preferredMutex.RUnlock() + + return transactionMetadata.preferred +} + +// setPreferred updates the preferred flag of the transaction. It is defined as a private setter because updating the +// preferred flag causes changes in other transactions and branches as well. This means that we need additional logic +// in the tangle. To update the preferred flag of a transaction, we need to use Tangle.SetTransactionPreferred(bool). +func (transactionMetadata *TransactionMetadata) setPreferred(preferred bool) (modified bool) { + transactionMetadata.preferredMutex.RLock() + if transactionMetadata.preferred == preferred { + transactionMetadata.preferredMutex.RUnlock() + + return + } + + transactionMetadata.preferredMutex.RUnlock() + transactionMetadata.preferredMutex.Lock() + defer transactionMetadata.preferredMutex.Unlock() + + if transactionMetadata.preferred == preferred { + return + } + + transactionMetadata.preferred = preferred + transactionMetadata.SetModified() + modified = true + + return +} + // SetFinalized allows us to set the finalized flag on the transactions. Finalized transactions will not be forked when // a conflict arrives later. func (transactionMetadata *TransactionMetadata) SetFinalized(finalized bool) (modified bool) { @@ -186,6 +227,7 @@ func (transactionMetadata *TransactionMetadata) SetFinalized(finalized bool) (mo } transactionMetadata.finalized = finalized + transactionMetadata.SetModified() if finalized { transactionMetadata.finalizationTime = time.Now() } @@ -220,12 +262,13 @@ func (transactionMetadata *TransactionMetadata) SoldificationTime() time.Time { // Bytes marshals the TransactionMetadata object into a sequence of bytes. func (transactionMetadata *TransactionMetadata) Bytes() []byte { - return marshalutil.New(branchmanager.BranchIDLength + 2*marshalutil.TIME_SIZE + 2*marshalutil.BOOL_SIZE). + return marshalutil.New(branchmanager.BranchIDLength + 2*marshalutil.TIME_SIZE + 3*marshalutil.BOOL_SIZE). WriteBytes(transactionMetadata.BranchID().Bytes()). - WriteTime(transactionMetadata.solidificationTime). - WriteTime(transactionMetadata.finalizationTime). - WriteBool(transactionMetadata.solid). - WriteBool(transactionMetadata.finalized). + WriteTime(transactionMetadata.SoldificationTime()). + WriteTime(transactionMetadata.FinalizationTime()). + WriteBool(transactionMetadata.Solid()). + WriteBool(transactionMetadata.Preferred()). + WriteBool(transactionMetadata.Finalized()). Bytes() } @@ -271,6 +314,9 @@ func (transactionMetadata *TransactionMetadata) UnmarshalObjectStorageValue(data if transactionMetadata.solid, err = marshalUtil.ReadBool(); err != nil { return } + if transactionMetadata.preferred, err = marshalUtil.ReadBool(); err != nil { + return + } if transactionMetadata.finalized, err = marshalUtil.ReadBool(); err != nil { return } diff --git a/dapps/valuetransfers/packages/test/tangle_test.go b/dapps/valuetransfers/packages/test/tangle_test.go new file mode 100644 index 00000000..14728b8f --- /dev/null +++ b/dapps/valuetransfers/packages/test/tangle_test.go @@ -0,0 +1,116 @@ +package test + +import ( + "testing" + + "github.com/iotaledger/hive.go/events" + "github.com/iotaledger/hive.go/kvstore/mapdb" + "github.com/iotaledger/hive.go/types" + + "github.com/stretchr/testify/assert" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/consensus" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" +) + +func TestTangle_ValueTransfer(t *testing.T) { + // initialize tangle + valueTangle := tangle.New(mapdb.NewMapDB()) + defer valueTangle.Shutdown() + + // initialize ledger state + ledgerState := tangle.NewLedgerState(valueTangle) + + // initialize seed + seed := wallet.NewSeed() + + // setup consensus rules + consensus.NewFCOB(valueTangle, 0) + + // check if ledger empty first + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0))) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1))) + + // load snapshot + valueTangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{ + transaction.GenesisID: { + seed.Address(0): []*balance.Balance{ + balance.New(balance.ColorIOTA, 337), + }, + + seed.Address(1): []*balance.Balance{ + balance.New(balance.ColorIOTA, 1000), + }, + }, + }) + + // check if balance exists after loading snapshot + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 337}, ledgerState.Balances(seed.Address(0))) + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1000}, ledgerState.Balances(seed.Address(1))) + + // introduce logic to record liked payloads + recordedLikedPayloads, resetRecordedLikedPayloads := recordLikedPayloads(valueTangle) + + // attach first spend + outputAddress1 := address.Random() + attachedPayload1 := payload.New(payload.GenesisID, payload.GenesisID, transaction.New( + transaction.NewInputs( + transaction.NewOutputID(seed.Address(0), transaction.GenesisID), + transaction.NewOutputID(seed.Address(1), transaction.GenesisID), + ), + + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + outputAddress1: { + balance.New(balance.ColorIOTA, 1337), + }, + }), + )) + valueTangle.AttachPayloadSync(attachedPayload1) + + // check if old addresses are empty and new addresses are filled + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0))) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1))) + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1337}, ledgerState.Balances(outputAddress1)) + assert.Equal(t, 1, len(recordedLikedPayloads)) + assert.Contains(t, recordedLikedPayloads, attachedPayload1.ID()) + + resetRecordedLikedPayloads() + + // attach double spend + outputAddress2 := address.Random() + valueTangle.AttachPayloadSync(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( + transaction.NewInputs( + transaction.NewOutputID(seed.Address(0), transaction.GenesisID), + transaction.NewOutputID(seed.Address(1), transaction.GenesisID), + ), + + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + outputAddress2: { + balance.New(balance.ColorNew, 1337), + }, + }), + ))) +} + +func recordLikedPayloads(valueTangle *tangle.Tangle) (recordedLikedPayloads map[payload.ID]types.Empty, resetFunc func()) { + recordedLikedPayloads = make(map[payload.ID]types.Empty) + + valueTangle.Events.PayloadLiked.Attach(events.NewClosure(func(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *tangle.CachedPayloadMetadata) { + defer cachedPayloadMetadata.Release() + + cachedPayload.Consume(func(payload *payload.Payload) { + recordedLikedPayloads[payload.ID()] = types.Void + }) + })) + + resetFunc = func() { + recordedLikedPayloads = make(map[payload.ID]types.Empty) + } + + return +} diff --git a/dapps/valuetransfers/packages/test/valuetransfers_test.go b/dapps/valuetransfers/packages/test/valuetransfers_test.go deleted file mode 100644 index aed10000..00000000 --- a/dapps/valuetransfers/packages/test/valuetransfers_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package test - -import ( - "testing" - "time" - - "github.com/iotaledger/hive.go/crypto/ed25519" - "github.com/iotaledger/hive.go/kvstore/mapdb" - "github.com/stretchr/testify/assert" - - "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/tangle" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" -) - -func TestTangle_ValueTransfer(t *testing.T) { - // initialize tangle + ledgerstate - valueTangle := tangle.New(mapdb.NewMapDB()) - if err := valueTangle.Prune(); err != nil { - t.Error(err) - - return - } - ledgerState := tangle.NewLedgerState(valueTangle) - - // - addressKeyPair1 := ed25519.GenerateKeyPair() - addressKeyPair2 := ed25519.GenerateKeyPair() - address1 := address.FromED25519PubKey(addressKeyPair1.PublicKey) - address2 := address.FromED25519PubKey(addressKeyPair2.PublicKey) - - // check if ledger empty first - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address1)) - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address2)) - - // load snapshot - valueTangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{ - transaction.GenesisID: { - address1: []*balance.Balance{ - balance.New(balance.ColorIOTA, 337), - }, - - address2: []*balance.Balance{ - balance.New(balance.ColorIOTA, 1000), - }, - }, - }) - - // check if balance exists after loading snapshot - assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 337}, ledgerState.Balances(address1)) - assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1000}, ledgerState.Balances(address2)) - - // attach first spend - outputAddress1 := address.Random() - valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( - transaction.NewInputs( - transaction.NewOutputID(address1, transaction.GenesisID), - transaction.NewOutputID(address2, transaction.GenesisID), - ), - - transaction.NewOutputs(map[address.Address][]*balance.Balance{ - outputAddress1: { - balance.New(balance.ColorIOTA, 1337), - }, - }), - ))) - - // wait for async task to run (TODO: REPLACE TIME BASED APPROACH WITH A WG) - time.Sleep(500 * time.Millisecond) - - // check if old addresses are empty - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address1)) - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address2)) - - // check if new addresses are filled - assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1337}, ledgerState.Balances(outputAddress1)) - - // attach double spend - outputAddress2 := address.Random() - valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( - transaction.NewInputs( - transaction.NewOutputID(address1, transaction.GenesisID), - transaction.NewOutputID(address2, transaction.GenesisID), - ), - - transaction.NewOutputs(map[address.Address][]*balance.Balance{ - outputAddress2: { - balance.New(balance.ColorNew, 1337), - }, - }), - ))) - - // shutdown tangle - valueTangle.Shutdown() -} diff --git a/dapps/valuetransfers/packages/wallet/seed.go b/dapps/valuetransfers/packages/wallet/seed.go new file mode 100644 index 00000000..9e94c3d7 --- /dev/null +++ b/dapps/valuetransfers/packages/wallet/seed.go @@ -0,0 +1,26 @@ +package wallet + +import ( + "github.com/iotaledger/hive.go/crypto/ed25519" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" +) + +// Seed represents a seed for IOTA wallets. A seed allows us to generate a deterministic sequence of Addresses and their +// corresponding KeyPairs. +type Seed struct { + *ed25519.Seed +} + +// NewSeed is the factory method for an IOTA seed. It either generates a new one or imports an existing marshaled seed. +// before. +func NewSeed(optionalSeedBytes ...[]byte) *Seed { + return &Seed{ + ed25519.NewSeed(optionalSeedBytes...), + } +} + +// Address returns an ed25519 address which can be used for receiving or sending funds. +func (seed *Seed) Address(index uint64) address.Address { + return address.FromED25519PubKey(seed.Seed.KeyPair(index).PublicKey) +} diff --git a/dapps/valuetransfers/packages/wallet/wallet.go b/dapps/valuetransfers/packages/wallet/wallet.go new file mode 100644 index 00000000..88ab072f --- /dev/null +++ b/dapps/valuetransfers/packages/wallet/wallet.go @@ -0,0 +1,20 @@ +package wallet + +// Wallet represents a simple cryptocurrency wallet for the IOTA tangle. It contains the logic to manage the movement of +// funds. +type Wallet struct { + seed *Seed +} + +// New is the factory method of the wallet. It either creates a new wallet or restores the wallet backup that is handed +// in as an optional parameter. +func New(optionalRecoveryBytes ...[]byte) *Wallet { + return &Wallet{ + seed: NewSeed(optionalRecoveryBytes...), + } +} + +// Seed returns the seed of this wallet that is used to generate all of the wallets addresses and private keys. +func (wallet *Wallet) Seed() *Seed { + return wallet.seed +} -- GitLab