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