diff --git a/packages/unbreakable_consensus/otv/approvers.go b/packages/unbreakable_consensus/otv/approvers.go
new file mode 100644
index 0000000000000000000000000000000000000000..cb89154c13b01fad24274576cd69ddd578406b7b
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/approvers.go
@@ -0,0 +1,57 @@
+package social_consensus
+
+import (
+	"sync"
+)
+
+type Approvers struct {
+	transactionID        int
+	transactionLikers    map[int]*Transaction
+	transactionDislikers map[int]*Transaction
+	realityLikers        map[int]*Transaction
+	realityDislikers     map[int]*Transaction
+
+	mutex sync.RWMutex
+}
+
+func NewApprovers(transactionID int) *Approvers {
+	return &Approvers{
+		transactionID:        transactionID,
+		transactionLikers:    make(map[int]*Transaction),
+		transactionDislikers: make(map[int]*Transaction),
+		realityLikers:        make(map[int]*Transaction),
+		realityDislikers:     make(map[int]*Transaction),
+	}
+}
+
+func (approvers *Approvers) GetTransactionID() int {
+	return approvers.transactionID
+}
+
+func (approvers *Approvers) AddTransactionLiker(transaction *Transaction) {
+	approvers.mutex.Lock()
+	defer approvers.mutex.Unlock()
+
+	approvers.transactionLikers[transaction.GetID()] = transaction
+}
+
+func (approvers *Approvers) AddTransactionDisliker(transaction *Transaction) {
+	approvers.mutex.Lock()
+	defer approvers.mutex.Unlock()
+
+	approvers.transactionDislikers[transaction.GetID()] = transaction
+}
+
+func (approvers *Approvers) AddRealityLiker(transaction *Transaction) {
+	approvers.mutex.Lock()
+	defer approvers.mutex.Unlock()
+
+	approvers.realityLikers[transaction.GetID()] = transaction
+}
+
+func (approvers *Approvers) AddRealityDisliker(transaction *Transaction) {
+	approvers.mutex.Lock()
+	defer approvers.mutex.Unlock()
+
+	approvers.realityDislikers[transaction.GetID()] = transaction
+}
diff --git a/packages/unbreakable_consensus/otv/approvers_database.go b/packages/unbreakable_consensus/otv/approvers_database.go
new file mode 100644
index 0000000000000000000000000000000000000000..7fa0385916f96e3b1efe9002a5a5546b4f1b79b1
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/approvers_database.go
@@ -0,0 +1,31 @@
+package social_consensus
+
+import (
+	"sync"
+)
+
+type ApproversDatabase struct {
+	approvers map[int]*Approvers
+
+	mutex sync.RWMutex
+}
+
+func NewApproversDatabase() *ApproversDatabase {
+	return &ApproversDatabase{
+		approvers: make(map[int]*Approvers),
+	}
+}
+
+func (approversDatabase *ApproversDatabase) StoreApprovers(approvers *Approvers) {
+	approversDatabase.mutex.Lock()
+	defer approversDatabase.mutex.Unlock()
+
+	approversDatabase.approvers[approvers.GetTransactionID()] = approvers
+}
+
+func (approversDatabase *ApproversDatabase) LoadApprovers(transactionID int) *Approvers {
+	approversDatabase.mutex.RLock()
+	defer approversDatabase.mutex.RUnlock()
+
+	return approversDatabase.approvers[transactionID]
+}
diff --git a/packages/unbreakable_consensus/otv/conflict_set.go b/packages/unbreakable_consensus/otv/conflict_set.go
new file mode 100644
index 0000000000000000000000000000000000000000..c6201f85cfb93de0dda3ea2379f80f478d6142f3
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/conflict_set.go
@@ -0,0 +1,29 @@
+package social_consensus
+
+type ConflictSet struct {
+	realities map[int]*Reality
+}
+
+func NewConflictSet() *ConflictSet {
+	return &ConflictSet{
+		realities: make(map[int]*Reality),
+	}
+}
+
+func (conflictSet *ConflictSet) GetRealities() map[int]*Reality {
+	return conflictSet.realities
+}
+
+func (conflictSet *ConflictSet) GetReality(id int) *Reality {
+	return conflictSet.realities[id]
+}
+
+func (conflictSet *ConflictSet) AddReality(id int) (result *Reality) {
+	if _, exists := conflictSet.realities[id]; !exists {
+		result = NewReality(conflictSet, id)
+
+		conflictSet.realities[id] = result
+	}
+
+	return
+}
diff --git a/packages/unbreakable_consensus/otv/elder_mask.go b/packages/unbreakable_consensus/otv/elder_mask.go
new file mode 100644
index 0000000000000000000000000000000000000000..8a251de0fa661555f8e70f22bd3fe910679b6158
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/elder_mask.go
@@ -0,0 +1,19 @@
+package social_consensus
+
+import (
+	"fmt"
+)
+
+type ElderMask uint64
+
+func (elderMask ElderMask) Union(otherElderMask ElderMask) ElderMask {
+	return elderMask | otherElderMask
+}
+
+func (elderMask ElderMask) Contains(otherMask ElderMask) bool {
+	return elderMask&otherMask != 0
+}
+
+func (elderMask ElderMask) String() string {
+	return "ElderMask(" + fmt.Sprintf("%064b", elderMask) + ")"
+}
diff --git a/packages/unbreakable_consensus/otv/node.go b/packages/unbreakable_consensus/otv/node.go
new file mode 100644
index 0000000000000000000000000000000000000000..488c2b296503c4b7000da777e2ddaff81e5717fc
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/node.go
@@ -0,0 +1,164 @@
+package social_consensus
+
+import (
+	"math/rand"
+	"time"
+
+	"github.com/iotaledger/goshimmer/packages/events"
+)
+
+type Node struct {
+	id                  int
+	weight              int
+	transactionCounter  int
+	tipSelector         *TipSelector
+	solidifier          *Solidifier
+	approversDatabase   *ApproversDatabase
+	transactionDatabase *TransactionDatabase
+	gossipNeighbors     []*Node
+	conflictSet         *ConflictSet
+	Events              NodeEvents
+
+	favoredReality int
+}
+
+var nodeIdCounter = 0
+
+func NewNode(weight int) *Node {
+	node := &Node{
+		id:                  nodeIdCounter,
+		weight:              weight,
+		approversDatabase:   NewApproversDatabase(),
+		transactionDatabase: NewTransactionDatabase(),
+		gossipNeighbors:     make([]*Node, 0),
+		conflictSet:         NewConflictSet(),
+		Events: NodeEvents{
+			TransactionSolid: events.NewEvent(TransactionEventHandler),
+		},
+	}
+
+	node.Events.TransactionSolid.Attach(events.NewClosure(node.ClassifyTransaction))
+
+	node.tipSelector = NewTipSelector(node)
+	node.solidifier = NewSolidifier(node)
+
+	nodeIdCounter++
+
+	return node
+}
+
+func (node *Node) GetWeight() int {
+	return node.weight
+}
+
+func (node *Node) GetID() int {
+	return node.id
+}
+
+func (node *Node) Peer(nodes ...*Node) {
+	node.gossipNeighbors = nodes
+}
+
+func (node *Node) GossipTransaction(transaction *Transaction) {
+	node.ReceiveTransaction(transaction)
+
+	for _, neighbor := range node.gossipNeighbors {
+		go func(neighbor *Node, transaction *Transaction) {
+			time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond)
+
+			neighbor.ReceiveTransaction(transaction.Clone())
+		}(neighbor, transaction)
+	}
+}
+
+func (node *Node) StoreTransaction(transaction *Transaction) {
+	if transaction != nil {
+		node.transactionDatabase.StoreTransaction(transaction)
+	}
+}
+
+func (node *Node) GetTransaction(id int) *Transaction {
+	return node.transactionDatabase.LoadTransaction(id)
+}
+
+func (node *Node) CreateTransaction() *Transaction {
+	trunk, branch, transactionLiked, realityLiked := node.tipSelector.GetTipsToApprove()
+
+	newTransaction := NewTransaction()
+	newTransaction.SetCounter(node.transactionCounter)
+	newTransaction.SetNodeID(node.GetID())
+	newTransaction.SetTrunkID(trunk.GetID())
+	newTransaction.SetBranchID(branch.GetID())
+	newTransaction.SetBranchTransactionLiked(transactionLiked)
+	newTransaction.SetBranchRealityLiked(realityLiked)
+
+	node.transactionCounter++
+
+	return newTransaction
+}
+
+func (node *Node) IssueTransaction() {
+	node.GossipTransaction(node.CreateTransaction())
+}
+
+func (node *Node) updateApprovers(transaction *Transaction) {
+	if trunkID := transaction.GetTrunkID(); trunkID != -1 {
+		approvers := node.approversDatabase.LoadApprovers(trunkID)
+		if approvers == nil {
+			approvers = NewApprovers(trunkID)
+
+			node.approversDatabase.StoreApprovers(approvers)
+		}
+
+		approvers.AddTransactionLiker(transaction)
+		approvers.AddRealityLiker(transaction)
+	}
+
+	if branchID := transaction.GetBranchID(); branchID != -1 {
+		approvers := node.approversDatabase.LoadApprovers(branchID)
+		if approvers == nil {
+			approvers = NewApprovers(branchID)
+
+			node.approversDatabase.StoreApprovers(approvers)
+		}
+
+		if transaction.IsBranchTransactionLiked() {
+			approvers.AddTransactionLiker(transaction)
+		} else {
+			approvers.AddTransactionDisliker(transaction)
+		}
+
+		if transaction.IsBranchRealityLiked() {
+			approvers.AddRealityLiker(transaction)
+		} else {
+			approvers.AddRealityDisliker(transaction)
+		}
+	}
+}
+
+func (node *Node) ReceiveTransaction(transaction *Transaction) {
+	if node.transactionDatabase.LoadTransaction(transaction.GetID()) == nil {
+		if transaction.claimedReality != 0 {
+			node.conflictSet.AddReality(transaction.claimedReality)
+
+			if node.favoredReality == 0 {
+				node.favoredReality = transaction.claimedReality
+			}
+		}
+
+		node.StoreTransaction(transaction)
+		node.updateApprovers(transaction)
+
+		node.solidifier.ProcessTransaction(transaction)
+	}
+}
+
+func (node *Node) ClassifyTransaction(transaction *Transaction) {
+	transactionLocallyLiked := transaction.claimedReality == node.favoredReality || transaction.claimedReality == 0
+	realityLocallyLiked := transactionLocallyLiked && (transaction.GetMetadata().GetReality() == 0 || transaction.GetMetadata().GetReality() == node.favoredReality)
+
+	transaction.GetMetadata().SetTransactionLocallyLiked(transactionLocallyLiked)
+	transaction.GetMetadata().SetRealityLocallyLiked(realityLocallyLiked)
+
+	node.tipSelector.ProcessClassifiedTransaction(transaction)
+}
diff --git a/packages/unbreakable_consensus/otv/node_events.go b/packages/unbreakable_consensus/otv/node_events.go
new file mode 100644
index 0000000000000000000000000000000000000000..e1a67a09d1a80f0f7f31a40839db95a656cd4139
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/node_events.go
@@ -0,0 +1,13 @@
+package social_consensus
+
+import (
+	"github.com/iotaledger/goshimmer/packages/events"
+)
+
+type NodeEvents struct {
+	TransactionSolid *events.Event
+}
+
+func TransactionEventHandler(handler interface{}, params ...interface{}) {
+	handler.(func(*Transaction))(params[0].(*Transaction))
+}
diff --git a/packages/unbreakable_consensus/otv/otv_test.go b/packages/unbreakable_consensus/otv/otv_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..68c625a80ccf2b2f5fc380b5634d63139079661c
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/otv_test.go
@@ -0,0 +1,138 @@
+package social_consensus
+
+import (
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/iotaledger/goshimmer/packages/events"
+)
+
+func GetRealityFLC(node *Node, transaction *Transaction) (result []*Transaction) {
+	result = make([]*Transaction, 1)
+	result[0] = transaction
+
+	traversalStack := make([]*Transaction, 0)
+	seenTransactions := make(map[int]bool)
+
+	if approvers := node.approversDatabase.LoadApprovers(transaction.GetID()); approvers != nil {
+		for _, approver := range approvers.transactionLikers {
+			if _, exists := seenTransactions[approver.GetID()]; !exists {
+				traversalStack = append(traversalStack, approver)
+				seenTransactions[approver.GetID()] = true
+			}
+		}
+
+		for _, approver := range approvers.realityLikers {
+			if _, exists := seenTransactions[approver.GetID()]; !exists {
+				traversalStack = append(traversalStack, approver)
+				seenTransactions[approver.GetID()] = true
+			}
+		}
+	}
+
+	for len(traversalStack) != 0 {
+		currentTransaction := traversalStack[0]
+
+		result = append(result, currentTransaction)
+
+		if approvers := node.approversDatabase.LoadApprovers(currentTransaction.GetID()); approvers != nil {
+			for _, approver := range approvers.realityLikers {
+				if _, seen := seenTransactions[approver.GetID()]; !seen {
+					traversalStack = append(traversalStack, approver)
+					seenTransactions[approver.GetID()] = true
+				}
+			}
+		}
+
+		traversalStack = traversalStack[1:]
+	}
+
+	return result
+}
+
+func BenchmarkTPS(b *testing.B) {
+	transactionIdCounter = 0
+
+	genesisTransaction := NewTransaction()
+	genesisTransaction.SetTrunkID(-1)
+	genesisTransaction.SetBranchID(-1)
+	genesisTransaction.GetMetadata().SetSolid(true)
+
+	node := NewNode(100)
+	node.StoreTransaction(genesisTransaction)
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		node.IssueTransaction()
+	}
+}
+
+func TestTipSelector_GetTipsToApprove(t *testing.T) {
+	// region initialize nodes /////////////////////////////////////////////////////////////////////////////////////////
+
+	genesisTransaction := NewTransaction()
+	genesisTransaction.SetTrunkID(-1)
+	genesisTransaction.SetBranchID(-1)
+	genesisTransaction.GetMetadata().SetSolid(true)
+
+	node0 := NewNode(15)
+	node0.StoreTransaction(genesisTransaction)
+
+	node1 := NewNode(17)
+	node1.StoreTransaction(genesisTransaction)
+
+	node2 := NewNode(19)
+	node2.StoreTransaction(genesisTransaction)
+
+	node0.Peer(node1, node2)
+	node1.Peer(node0, node2)
+	node2.Peer(node0, node1)
+
+	// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	var wg sync.WaitGroup
+
+	node0.Events.TransactionSolid.Attach(events.NewClosure(func(transaction *Transaction) {
+		wg.Done()
+	}))
+
+	// region initialize consensus test conditions /////////////////////////////////////////////////////////////////////
+
+	// create first conflicting transaction
+	wg.Add(1)
+	conflictingTransaction0 := node0.CreateTransaction()
+	conflictingTransaction0.claimedReality = 1
+
+	// create second conflicting transaction
+	wg.Add(1)
+	conflictingTransaction1 := node1.CreateTransaction()
+	conflictingTransaction1.claimedReality = 2
+
+	// make nodes see the conflicts in different order
+	node0.GossipTransaction(conflictingTransaction0)
+	node1.GossipTransaction(conflictingTransaction1)
+	node2.GossipTransaction(conflictingTransaction0)
+
+	// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	// issue a few transactions
+	for i := 0; i < 30; i++ {
+		wg.Add(3)
+
+		node0.IssueTransaction()
+		node1.IssueTransaction()
+		node2.IssueTransaction()
+
+		time.Sleep(50 * time.Millisecond)
+	}
+
+	wg.Wait()
+
+	// check if consensus reached
+	assert.Equal(t, node0.favoredReality, node1.favoredReality)
+	assert.Equal(t, node1.favoredReality, node2.favoredReality)
+}
diff --git a/packages/unbreakable_consensus/otv/reality.go b/packages/unbreakable_consensus/otv/reality.go
new file mode 100644
index 0000000000000000000000000000000000000000..b069111f3e6ce4bdc8423cab2c32e31c3fb83a37
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/reality.go
@@ -0,0 +1,71 @@
+package social_consensus
+
+import (
+	"sync"
+)
+
+type Reality struct {
+	id          int
+	conflictSet *ConflictSet
+	supporters  map[int]int
+	weight      int
+	mutex       sync.RWMutex
+}
+
+func NewReality(conflictSet *ConflictSet, id int) *Reality {
+	return &Reality{
+		id:          id,
+		conflictSet: conflictSet,
+		supporters:  make(map[int]int),
+	}
+}
+
+func (reality *Reality) GetId() int {
+	return reality.id
+}
+
+func (reality *Reality) GetConflictSet() *ConflictSet {
+	return reality.conflictSet
+}
+
+func (reality *Reality) ContainsSupporter(nodeId int) (transactionCounter int) {
+	if currentTransactionCounter, supporterExists := reality.supporters[nodeId]; supporterExists {
+		transactionCounter = currentTransactionCounter
+	}
+
+	return
+}
+
+func (reality *Reality) AddSupporter(nodeId int, transactionCounter int) {
+	reality.mutex.Lock()
+	defer reality.mutex.Unlock()
+
+	if oldTransactionCounter, supporterExists := reality.supporters[nodeId]; !supporterExists {
+		// remove supporter from the alternative reality
+		for otherRealityId, otherReality := range reality.conflictSet.GetRealities() {
+			if otherTransactionCounter := otherReality.ContainsSupporter(nodeId); otherRealityId != reality.id && otherTransactionCounter != 0 {
+				otherReality.RemoveSupporter(nodeId, transactionCounter)
+			}
+		}
+
+		// add supporter to new reality
+		reality.supporters[nodeId] = transactionCounter
+
+		// update weight
+		reality.weight += 20
+	} else if oldTransactionCounter < transactionCounter {
+		// update transaction counter
+		reality.supporters[nodeId] = transactionCounter
+	}
+}
+
+func (reality *Reality) RemoveSupporter(nodeId int, transactionCounter int) {
+	reality.mutex.Lock()
+	defer reality.mutex.Unlock()
+
+	if oldTransactionCounter, supporterExists := reality.supporters[nodeId]; supporterExists && oldTransactionCounter < transactionCounter {
+		delete(reality.supporters, nodeId)
+
+		reality.weight -= 20
+	}
+}
diff --git a/packages/unbreakable_consensus/otv/solidifier.go b/packages/unbreakable_consensus/otv/solidifier.go
new file mode 100644
index 0000000000000000000000000000000000000000..93620cb351e65b5b76147ba09d0f563bdfd3c77d
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/solidifier.go
@@ -0,0 +1,92 @@
+package social_consensus
+
+type Solidifier struct {
+	node *Node
+}
+
+func NewSolidifier(node *Node) *Solidifier {
+	return &Solidifier{
+		node: node,
+	}
+}
+
+func (solidifier *Solidifier) IsTransactionSolid(transaction *Transaction) (isSolid bool) {
+	isSolid = true
+
+	if trunkID := transaction.GetTrunkID(); trunkID != -1 {
+		if trunk := solidifier.node.GetTransaction(trunkID); trunk == nil {
+			isSolid = false
+		} else if !trunk.GetMetadata().IsSolid() {
+			isSolid = false
+		}
+	}
+
+	if branchID := transaction.GetBranchID(); branchID != -1 {
+		if trunk := solidifier.node.GetTransaction(branchID); trunk == nil {
+			isSolid = false
+		} else if !trunk.GetMetadata().IsSolid() {
+			isSolid = false
+		}
+	}
+
+	return
+}
+
+func (solidifier *Solidifier) PropagateSolidity(transaction *Transaction) {
+	if !transaction.GetMetadata().IsSolid() {
+		transaction.GetMetadata().SetSolid(true)
+
+		if transaction.claimedReality != 0 {
+			transaction.GetMetadata().SetReality(transaction.claimedReality)
+		} else {
+			trunk := solidifier.node.GetTransaction(transaction.GetTrunkID())
+			trunkReality := trunk.GetMetadata().GetReality()
+			branch := solidifier.node.GetTransaction(transaction.GetBranchID())
+			branchReality := branch.GetMetadata().GetReality()
+
+			if transaction.IsBranchRealityLiked() && branchReality != 0 {
+				transaction.GetMetadata().SetReality(branchReality)
+
+				solidifier.node.conflictSet.GetReality(branchReality).AddSupporter(transaction.GetNodeID(), transaction.counter)
+
+				var heaviestReality *Reality
+				heaviestRealityWeight := 0
+				for _, reality := range solidifier.node.conflictSet.GetRealities() {
+					if reality.weight > heaviestRealityWeight {
+						heaviestReality = reality
+						heaviestRealityWeight = reality.weight
+					}
+				}
+
+				if solidifier.node.favoredReality != heaviestReality.id {
+					solidifier.node.favoredReality = heaviestReality.id
+				}
+			} else {
+				transaction.GetMetadata().SetReality(trunkReality)
+			}
+		}
+
+		solidifier.node.Events.TransactionSolid.Trigger(transaction)
+
+		if approvers := solidifier.node.approversDatabase.LoadApprovers(transaction.GetID()); approvers != nil {
+			for _, approver := range approvers.transactionLikers {
+				solidifier.PropagateSolidity(approver)
+			}
+			for _, approver := range approvers.transactionDislikers {
+				solidifier.PropagateSolidity(approver)
+			}
+			for _, approver := range approvers.realityLikers {
+				solidifier.PropagateSolidity(approver)
+			}
+			for _, approver := range approvers.realityDislikers {
+				solidifier.PropagateSolidity(approver)
+			}
+		}
+	}
+}
+
+func (solidifier *Solidifier) ProcessTransaction(transaction *Transaction) {
+	if solidifier.IsTransactionSolid(transaction) {
+		solidifier.PropagateSolidity(transaction)
+	}
+}
diff --git a/packages/unbreakable_consensus/otv/tip_selector.go b/packages/unbreakable_consensus/otv/tip_selector.go
new file mode 100644
index 0000000000000000000000000000000000000000..b2cd8efd039f894ed69df1fbfa639c7270e47396
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/tip_selector.go
@@ -0,0 +1,118 @@
+package social_consensus
+
+import (
+	"sync"
+
+	"github.com/iotaledger/goshimmer/packages/datastructure"
+)
+
+type TipSelector struct {
+	node          *Node
+	likedTips     *datastructure.RandomMap // like transaction / like reality
+	semiLikedTips *datastructure.RandomMap // like transaction / dislike reality
+	dislikedTips  *datastructure.RandomMap // dislike transaction / dislike reality
+	// semiDislikedTips *datastructure.RandomMap // dislike transaction / like reality (doesn't occur: the only reason
+	// to dislike a transaction is if it is conflicting. This however means it spawns a reality we can not both like and
+	// dislike it at the same time.
+
+	mutex sync.RWMutex
+}
+
+func NewTipSelector(node *Node) *TipSelector {
+	return &TipSelector{
+		node:          node,
+		likedTips:     datastructure.NewRandomMap(),
+		semiLikedTips: datastructure.NewRandomMap(),
+		dislikedTips:  datastructure.NewRandomMap(),
+	}
+}
+
+func (tipSelector *TipSelector) GetTipsToApprove() (trunkTransaction *Transaction, branchTransaction *Transaction, transactionLiked bool, realityLiked bool) {
+	tipSelector.mutex.RLock()
+	defer tipSelector.mutex.RUnlock()
+
+	trunkTransaction = tipSelector.getRandomLikedTip()
+
+	if branchTransaction = tipSelector.getRandomSemiLikedTip(); branchTransaction != nil {
+		transactionLiked = true
+		realityLiked = false
+
+		return
+	}
+
+	if branchTransaction = tipSelector.getRandomDislikedTip(); branchTransaction != nil {
+		transactionLiked = false
+		realityLiked = false
+
+		return
+	}
+
+	branchTransaction = tipSelector.getRandomLikedTip()
+	transactionLiked = true
+	realityLiked = true
+
+	return
+}
+
+func (tipSelector *TipSelector) ProcessClassifiedTransaction(transaction *Transaction) {
+	tipSelector.mutex.Lock()
+	defer tipSelector.mutex.Unlock()
+
+	transactionMetadata := transaction.GetMetadata()
+
+	if transactionLiked := transactionMetadata.IsTransactionLocallyLiked(); transactionLiked {
+		if realityLiked := transactionMetadata.IsRealityLocallyLiked(); realityLiked {
+			tipSelector.likedTips.Set(transaction.GetID(), transaction)
+
+			if trunk := tipSelector.node.GetTransaction(transaction.GetTrunkID()); trunk != nil {
+				trunkId := trunk.GetID()
+
+				tipSelector.likedTips.Delete(trunkId)
+				tipSelector.semiLikedTips.Delete(trunkId)
+				tipSelector.dislikedTips.Delete(trunkId)
+			}
+
+			if branch := tipSelector.node.GetTransaction(transaction.GetBranchID()); branch != nil {
+				branchId := branch.GetID()
+
+				tipSelector.likedTips.Delete(branchId)
+				tipSelector.semiLikedTips.Delete(branchId)
+				tipSelector.dislikedTips.Delete(branchId)
+			}
+		} else {
+			tipSelector.semiLikedTips.Set(transaction.GetID(), transaction)
+		}
+	} else {
+		if realityLiked := transactionMetadata.IsRealityLocallyLiked(); realityLiked {
+			panic("we should never dislike a transaction and at the same time like its reality")
+		} else {
+			tipSelector.dislikedTips.Set(transaction.GetID(), transaction)
+		}
+	}
+
+	//fmt.Println("TIP COUNT:", tipSelector.dislikedTips.Size()+tipSelector.semiLikedTips.Size()+tipSelector.likedTips.Size())
+}
+
+func (tipSelector *TipSelector) getRandomLikedTip() *Transaction {
+	if result := tipSelector.likedTips.RandomEntry(); result == nil {
+		return tipSelector.node.GetTransaction(0)
+	} else {
+		return result.(*Transaction)
+	}
+}
+
+func (tipSelector *TipSelector) getRandomSemiLikedTip() *Transaction {
+	if randomSemiLikedTip := tipSelector.semiLikedTips.RandomEntry(); randomSemiLikedTip != nil {
+		return randomSemiLikedTip.(*Transaction)
+	} else {
+		return nil
+	}
+}
+
+func (tipSelector *TipSelector) getRandomDislikedTip() *Transaction {
+	if randomDislikedTip := tipSelector.dislikedTips.RandomEntry(); randomDislikedTip != nil {
+		return randomDislikedTip.(*Transaction)
+	} else {
+		return nil
+	}
+}
diff --git a/packages/unbreakable_consensus/otv/transaction.go b/packages/unbreakable_consensus/otv/transaction.go
new file mode 100644
index 0000000000000000000000000000000000000000..67d360e3b61e0198da893d542094b5018b18e042
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/transaction.go
@@ -0,0 +1,119 @@
+package social_consensus
+
+import (
+	"github.com/iotaledger/goshimmer/packages/stringify"
+)
+
+type Transaction struct {
+	id                     int
+	nodeId                 int
+	trunkId                int
+	branchId               int
+	counter                int
+	branchRealityLiked     bool
+	branchTransactionLiked bool
+
+	metadata       *TransactionMetadata
+	claimedReality int
+}
+
+var transactionIdCounter = 0
+
+func NewTransaction() *Transaction {
+	transaction := &Transaction{
+		id: transactionIdCounter,
+	}
+
+	transactionIdCounter++
+
+	return transaction
+}
+
+func (transaction *Transaction) SetNodeID(nodeID int) {
+	transaction.nodeId = nodeID
+}
+
+func (transaction *Transaction) GetNodeID() int {
+	return transaction.nodeId
+}
+
+func (transaction *Transaction) GetID() int {
+	return transaction.id
+}
+
+func (transaction *Transaction) SetTrunkID(id int) {
+	transaction.trunkId = id
+}
+
+func (transaction *Transaction) GetTrunkID() int {
+	return transaction.trunkId
+}
+
+func (transaction *Transaction) SetBranchID(id int) {
+	transaction.branchId = id
+}
+
+func (transaction *Transaction) GetBranchID() int {
+	return transaction.branchId
+}
+
+func (transaction *Transaction) SetCounter(counter int) {
+	transaction.counter = counter
+}
+
+func (transaction *Transaction) GetCounter() int {
+	return transaction.counter
+}
+
+func (transaction *Transaction) SetBranchRealityLiked(liked bool) {
+	transaction.branchRealityLiked = liked
+}
+
+func (transaction *Transaction) IsBranchRealityLiked() bool {
+	return transaction.branchRealityLiked
+}
+
+func (transaction *Transaction) SetBranchTransactionLiked(liked bool) {
+	transaction.branchTransactionLiked = liked
+}
+
+func (transaction *Transaction) IsBranchTransactionLiked() bool {
+	return transaction.branchTransactionLiked
+}
+
+func (transaction *Transaction) GetMetadata() *TransactionMetadata {
+	if transaction.metadata == nil {
+		transaction.metadata = NewTransactionMetadata()
+	}
+
+	return transaction.metadata
+}
+
+func (transaction *Transaction) Clone() *Transaction {
+	return &Transaction{
+		id:                     transaction.id,
+		nodeId:                 transaction.nodeId,
+		trunkId:                transaction.GetTrunkID(),
+		branchId:               transaction.GetBranchID(),
+		counter:                transaction.counter,
+		branchRealityLiked:     transaction.branchRealityLiked,
+		branchTransactionLiked: transaction.branchTransactionLiked,
+		metadata:               nil,
+		claimedReality:         transaction.claimedReality,
+	}
+}
+
+func (transaction *Transaction) String() string {
+	return stringify.Struct("Transaction",
+		stringify.StructField("id", transaction.GetID()),
+		stringify.StructField("nodeID", transaction.GetNodeID()),
+		stringify.StructField("trunkID", transaction.GetTrunkID()),
+		stringify.StructField("branchID", transaction.GetBranchID()),
+		stringify.StructField("branchTransactionLiked", transaction.IsBranchTransactionLiked()),
+		stringify.StructField("branchRealityLiked", transaction.IsBranchRealityLiked()),
+		stringify.StructField("claimedReality", transaction.claimedReality),
+		stringify.StructField("reality", transaction.GetMetadata().GetReality()),
+		stringify.StructField("isTransactionLocallyLiked", transaction.GetMetadata().IsTransactionLocallyLiked()),
+		stringify.StructField("isRealityLocallyLiked", transaction.GetMetadata().IsRealityLocallyLiked()),
+	)
+}
diff --git a/packages/unbreakable_consensus/otv/transaction_database.go b/packages/unbreakable_consensus/otv/transaction_database.go
new file mode 100644
index 0000000000000000000000000000000000000000..a17ca59bdde2a580147929eccf97be6718b880ed
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/transaction_database.go
@@ -0,0 +1,30 @@
+package social_consensus
+
+import (
+	"sync"
+)
+
+type TransactionDatabase struct {
+	transactions map[int]*Transaction
+	mutex        sync.RWMutex
+}
+
+func NewTransactionDatabase() *TransactionDatabase {
+	return &TransactionDatabase{
+		transactions: make(map[int]*Transaction),
+	}
+}
+
+func (transactionDatabase *TransactionDatabase) StoreTransaction(transaction *Transaction) {
+	transactionDatabase.mutex.Lock()
+	defer transactionDatabase.mutex.Unlock()
+
+	transactionDatabase.transactions[transaction.GetID()] = transaction
+}
+
+func (transactionDatabase *TransactionDatabase) LoadTransaction(id int) *Transaction {
+	transactionDatabase.mutex.RLock()
+	defer transactionDatabase.mutex.RUnlock()
+
+	return transactionDatabase.transactions[id]
+}
diff --git a/packages/unbreakable_consensus/otv/transaction_metadata.go b/packages/unbreakable_consensus/otv/transaction_metadata.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab285cd6cde8228a0d4271f512edccf09dc4b537
--- /dev/null
+++ b/packages/unbreakable_consensus/otv/transaction_metadata.go
@@ -0,0 +1,44 @@
+package social_consensus
+
+type TransactionMetadata struct {
+	solid                   bool
+	transactionLocallyLiked bool
+	realityLocallyLiked     bool
+	reality                 int
+}
+
+func NewTransactionMetadata() *TransactionMetadata {
+	return &TransactionMetadata{}
+}
+
+func (transactionMetadata *TransactionMetadata) SetSolid(solid bool) {
+	transactionMetadata.solid = solid
+}
+
+func (transactionMetadata *TransactionMetadata) IsSolid() bool {
+	return transactionMetadata.solid
+}
+
+func (transactionMetadata *TransactionMetadata) SetTransactionLocallyLiked(liked bool) {
+	transactionMetadata.transactionLocallyLiked = liked
+}
+
+func (transactionMetadata *TransactionMetadata) IsTransactionLocallyLiked() bool {
+	return transactionMetadata.transactionLocallyLiked
+}
+
+func (transactionMetadata *TransactionMetadata) SetRealityLocallyLiked(liked bool) {
+	transactionMetadata.realityLocallyLiked = liked
+}
+
+func (transactionMetadata *TransactionMetadata) IsRealityLocallyLiked() bool {
+	return transactionMetadata.realityLocallyLiked
+}
+
+func (transactionMetadata *TransactionMetadata) SetReality(reality int) {
+	transactionMetadata.reality = reality
+}
+
+func (transactionMetadata *TransactionMetadata) GetReality() int {
+	return transactionMetadata.reality
+}