Skip to content
Snippets Groups Projects
  • Ching-Hua (Vivian) Lin's avatar
    50295559
    Change event parameters to struct (#704) · 50295559
    Ching-Hua (Vivian) Lin authored
    * refactor: Refactor messagelayer events
    
    * refactor: Refactor drng events
    
    * fix: Fix messagelayer unit tests
    
    * refactor: Refactor value events
    
    * fix: Fix wrong parameter in TransactionBooked event
    
    * refactor: Fix :dog:
    
    * refactor: Rename event parameter structure for consistency
    
    * refactor: Rename parameters
    
    * fix: Fix message tangle unit tests
    
    * refactor: Refactor messagerequester events
    
    * fix: Minor fix
    
    * refactor: Rename Txn to Transaction
    
    * refactor: Rename OutputIDs in ForkEvent to InputIDs
    
    * refactor: Use the pointer of events structure for consistency
    
    * refactor: Minor fix
    Change event parameters to struct (#704)
    Ching-Hua (Vivian) Lin authored
    * refactor: Refactor messagelayer events
    
    * refactor: Refactor drng events
    
    * fix: Fix messagelayer unit tests
    
    * refactor: Refactor value events
    
    * fix: Fix wrong parameter in TransactionBooked event
    
    * refactor: Fix :dog:
    
    * refactor: Rename event parameter structure for consistency
    
    * refactor: Rename parameters
    
    * fix: Fix message tangle unit tests
    
    * refactor: Refactor messagerequester events
    
    * fix: Minor fix
    
    * refactor: Rename Txn to Transaction
    
    * refactor: Rename OutputIDs in ForkEvent to InputIDs
    
    * refactor: Use the pointer of events structure for consistency
    
    * refactor: Minor fix
tangle.go 79.74 KiB
package tangle

import (
	"container/list"
	"errors"
	"fmt"
	"math"

	"github.com/iotaledger/hive.go/async"
	"github.com/iotaledger/hive.go/events"
	"github.com/iotaledger/hive.go/kvstore"
	"github.com/iotaledger/hive.go/marshalutil"
	"github.com/iotaledger/hive.go/objectstorage"
	"github.com/iotaledger/hive.go/types"

	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
	"github.com/iotaledger/goshimmer/packages/binary/storageprefix"
)

// Tangle represents the value tangle that consists out of value payloads.
// It is an independent ontology, that lives inside the tangle.
type Tangle struct {
	branchManager *branchmanager.BranchManager

	payloadStorage             *objectstorage.ObjectStorage
	payloadMetadataStorage     *objectstorage.ObjectStorage
	approverStorage            *objectstorage.ObjectStorage
	missingPayloadStorage      *objectstorage.ObjectStorage
	transactionStorage         *objectstorage.ObjectStorage
	transactionMetadataStorage *objectstorage.ObjectStorage
	attachmentStorage          *objectstorage.ObjectStorage
	outputStorage              *objectstorage.ObjectStorage
	consumerStorage            *objectstorage.ObjectStorage

	Events *Events

	workerPool async.WorkerPool
}

// New is the constructor of a Tangle and creates a new Tangle object from the given details.
func New(store kvstore.KVStore) (tangle *Tangle) {
	osFactory := objectstorage.NewFactory(store, storageprefix.ValueTransfers)

	tangle = &Tangle{
		branchManager: branchmanager.New(store),

		payloadStorage:             osFactory.New(osPayload, osPayloadFactory, objectstorage.CacheTime(cacheTime)),
		payloadMetadataStorage:     osFactory.New(osPayloadMetadata, osPayloadMetadataFactory, objectstorage.CacheTime(cacheTime)),
		missingPayloadStorage:      osFactory.New(osMissingPayload, osMissingPayloadFactory, objectstorage.CacheTime(cacheTime)),
		approverStorage:            osFactory.New(osApprover, osPayloadApproverFactory, objectstorage.CacheTime(cacheTime), objectstorage.PartitionKey(payload.IDLength, payload.IDLength), objectstorage.KeysOnly(true)),
		transactionStorage:         osFactory.New(osTransaction, osTransactionFactory, objectstorage.CacheTime(cacheTime), osLeakDetectionOption),
		transactionMetadataStorage: osFactory.New(osTransactionMetadata, osTransactionMetadataFactory, objectstorage.CacheTime(cacheTime), osLeakDetectionOption),
		attachmentStorage:          osFactory.New(osAttachment, osAttachmentFactory, objectstorage.CacheTime(cacheTime), objectstorage.PartitionKey(transaction.IDLength, payload.IDLength), osLeakDetectionOption),
		outputStorage:              osFactory.New(osOutput, osOutputFactory, OutputKeyPartitions, objectstorage.CacheTime(cacheTime), osLeakDetectionOption),
		consumerStorage:            osFactory.New(osConsumer, osConsumerFactory, ConsumerPartitionKeys, objectstorage.CacheTime(cacheTime), osLeakDetectionOption),

		Events: newEvents(),
	}
	tangle.setupDAGSynchronization()

	// TODO: CHANGE BACK TO MULTI THREADING ONCE WE FIXED LOGICAL RACE CONDITIONS
	tangle.workerPool.Tune(1)

	return
}
// region MAIN PUBLIC API //////////////////////////////////////////////////////////////////////////////////////////////

// AttachPayload adds a new payload to the value tangle.
func (tangle *Tangle) AttachPayload(payload *payload.Payload) {
	tangle.workerPool.Submit(func() { tangle.AttachPayloadSync(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 {
		return
	}
	defer cachedPayload.Release()
	defer cachedPayloadMetadata.Release()

	// store transaction models or abort if we have seen this attachment already  (nil == was not stored)
	cachedTransaction, cachedTransactionMetadata, cachedAttachment, transactionIsNew := tangle.storeTransactionModels(payloadToStore)
	defer cachedTransaction.Release()
	defer cachedTransactionMetadata.Release()
	if cachedAttachment == nil {
		return
	}
	defer cachedAttachment.Release()

	// store the references between the different entities (we do this after the actual entities were stored, so that
	// all the metadata models exist in the database as soon as the entities are reachable by walks).
	tangle.storePayloadReferences(payloadToStore)

	// trigger events
	if tangle.missingPayloadStorage.DeleteIfPresent(payloadToStore.ID().Bytes()) {
		tangle.Events.MissingPayloadReceived.Trigger(&CachedPayloadEvent{
			Payload:         cachedPayload,
			PayloadMetadata: cachedPayloadMetadata})
	}
	tangle.Events.PayloadAttached.Trigger(&CachedPayloadEvent{
		Payload:         cachedPayload,
		PayloadMetadata: cachedPayloadMetadata})
	if transactionIsNew {
		tangle.Events.TransactionReceived.Trigger(&CachedAttachmentsEvent{
			Transaction:         cachedTransaction,
			TransactionMetadata: cachedTransactionMetadata,
			Attachments:         cachedAttachment})
	}

	// check solidity
	tangle.solidifyPayload(cachedPayload.Retain(), cachedPayloadMetadata.Retain(), cachedTransaction.Retain(), cachedTransactionMetadata.Retain())
}

// SetTransactionPreferred modifies the preferred flag of a transaction. It updates the transactions metadata,
// propagates the changes to the branch DAG and triggers an update of the liked flags in the value tangle.
func (tangle *Tangle) SetTransactionPreferred(transactionID transaction.ID, preferred bool) (modified bool, err error) {
	return tangle.setTransactionPreferred(transactionID, preferred, EventSourceTangle)
}

// 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) {
	return tangle.setTransactionFinalized(transactionID, EventSourceTangle)
}

// 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 false
		}
	}

	return true
}

// ValuePayloadsConfirmed is checking if the Payloads referenced by the passed in IDs are all confirmed.
func (tangle *Tangle) ValuePayloadsConfirmed(payloadIDs ...payload.ID) (confirmed bool) {
	for _, payloadID := range payloadIDs {
		if payloadID == payload.GenesisID {
			continue
		}

		payloadMetadataFound := tangle.PayloadMetadata(payloadID).Consume(func(payloadMetadata *PayloadMetadata) {
			confirmed = payloadMetadata.Confirmed()
		})

		if !payloadMetadataFound || !confirmed {
			return false
		}
	}

	return true
}

// BranchManager is the getter for the manager that takes care of creating and updating branches.
func (tangle *Tangle) BranchManager() *branchmanager.BranchManager {
	return tangle.branchManager
}

// LoadSnapshot creates a set of outputs in the value tangle, that are forming the genesis for future transactions.
func (tangle *Tangle) LoadSnapshot(snapshot map[transaction.ID]map[address.Address][]*balance.Balance) {
	for transactionID, addressBalances := range snapshot {
		for outputAddress, balances := range addressBalances {
			input := NewOutput(outputAddress, transactionID, branchmanager.MasterBranchID, balances)
			input.setSolid(true)
			input.setBranchID(branchmanager.MasterBranchID)
			input.setLiked(true)
			input.setConfirmed(true)
			input.setFinalized(true)

			// store output and abort if the snapshot has already been loaded earlier (output exists in the database)
			cachedOutput, stored := tangle.outputStorage.StoreIfAbsent(input)
			if !stored {
				return
			}

			cachedOutput.Release()
		}
	}
}

// Fork creates a new branch from an existing transaction.
func (tangle *Tangle) Fork(transactionID transaction.ID, conflictingInputs []transaction.OutputID) (forked bool, finalized bool, err error) {
	cachedTransaction := tangle.Transaction(transactionID)
	cachedTransactionMetadata := tangle.TransactionMetadata(transactionID)
	defer cachedTransaction.Release()
	defer cachedTransactionMetadata.Release()

	tx := cachedTransaction.Unwrap()
	if tx == nil {
		err = fmt.Errorf("failed to load transaction '%s': %w", transactionID, ErrFatal)

		return
	}
	txMetadata := cachedTransactionMetadata.Unwrap()
	if txMetadata == nil {
		err = fmt.Errorf("failed to load metadata of transaction '%s': %w", transactionID, ErrFatal)

		return
	}

	// abort if this transaction was finalized already
	if txMetadata.Finalized() {
		finalized = true

		return
	}

	// update / create new branch
	newBranchID := branchmanager.NewBranchID(tx.ID())
	cachedTargetBranch, newBranchCreated := tangle.branchManager.Fork(newBranchID, []branchmanager.BranchID{txMetadata.BranchID()}, conflictingInputs)
	defer cachedTargetBranch.Release()

	// 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
		}
	}

	// abort if the branch existed already
	if !newBranchCreated {
		return
	}

	// move transactions to new branch
	if err = tangle.moveTransactionToBranch(cachedTransaction.Retain(), cachedTransactionMetadata.Retain(), cachedTargetBranch.Retain()); err != nil {
		return
	}

	// trigger events + set result
	tangle.Events.Fork.Trigger(&ForkEvent{
		Transaction:         cachedTransaction,
		TransactionMetadata: cachedTransactionMetadata,
		Branch:              cachedTargetBranch,
		InputIDs:            conflictingInputs})
	forked = true

	return
}

// Prune resets the database and deletes all objects (for testing or "node resets").
func (tangle *Tangle) Prune() (err error) {
	if err = tangle.branchManager.Prune(); err != nil {
		return
	}

	for _, storage := range []*objectstorage.ObjectStorage{
		tangle.payloadStorage,
		tangle.payloadMetadataStorage,
		tangle.missingPayloadStorage,
		tangle.approverStorage,
		tangle.transactionStorage,
		tangle.transactionMetadataStorage,
		tangle.attachmentStorage,
		tangle.outputStorage,
		tangle.consumerStorage,
	} {
		if err = storage.Prune(); err != nil {
			return
		}
	}

	return
}

// Shutdown stops the worker pools and shuts down the object storage instances.
func (tangle *Tangle) Shutdown() *Tangle {
	tangle.workerPool.ShutdownGracefully()

	for _, storage := range []*objectstorage.ObjectStorage{
		tangle.payloadStorage,
		tangle.payloadMetadataStorage,
		tangle.missingPayloadStorage,
		tangle.approverStorage,
		tangle.transactionStorage,
		tangle.transactionMetadataStorage,
		tangle.attachmentStorage,
		tangle.outputStorage,
		tangle.consumerStorage,
	} {
		storage.Shutdown()
	}

	return tangle
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region GETTERS/ITERATORS FOR THE STORED MODELS //////////////////////////////////////////////////////////////////////

// Transaction loads the given transaction from the objectstorage.
func (tangle *Tangle) Transaction(transactionID transaction.ID) *transaction.CachedTransaction {
	return &transaction.CachedTransaction{CachedObject: tangle.transactionStorage.Load(transactionID.Bytes())}
}

// TransactionMetadata retrieves the metadata of a value payload from the object storage.
func (tangle *Tangle) TransactionMetadata(transactionID transaction.ID) *CachedTransactionMetadata {
	return &CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Load(transactionID.Bytes())}
}

// TransactionOutput loads the given output from the objectstorage.
func (tangle *Tangle) TransactionOutput(outputID transaction.OutputID) *CachedOutput {
	return &CachedOutput{CachedObject: tangle.outputStorage.Load(outputID.Bytes())}
}

// OutputsOnAddress retrieves all the Outputs that are associated with an address.
func (tangle *Tangle) OutputsOnAddress(address address.Address) (result CachedOutputs) {
	result = make(CachedOutputs)
	tangle.outputStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool {
		outputID, _, err := transaction.OutputIDFromBytes(key)
		if err != nil {
			panic(err)
		}

		result[outputID] = &CachedOutput{CachedObject: cachedObject}

		return true
	}, address.Bytes())

	return
}

// Consumers retrieves the approvers of a payload from the object storage.
func (tangle *Tangle) Consumers(outputID transaction.OutputID) CachedConsumers {
	consumers := make(CachedConsumers, 0)
	tangle.consumerStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool {
		consumers = append(consumers, &CachedConsumer{CachedObject: cachedObject})

		return true
	}, outputID.Bytes())

	return consumers
}

// Attachments retrieves the attachment of a payload from the object storage.
func (tangle *Tangle) Attachments(transactionID transaction.ID) CachedAttachments {
	attachments := make(CachedAttachments, 0)
	tangle.attachmentStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool {
		attachments = append(attachments, &CachedAttachment{CachedObject: cachedObject})

		return true
	}, transactionID.Bytes())

	return attachments
}

// Payload retrieves a payload from the object storage.
func (tangle *Tangle) Payload(payloadID payload.ID) *payload.CachedPayload {
	return &payload.CachedPayload{CachedObject: tangle.payloadStorage.Load(payloadID.Bytes())}
}

// PayloadMetadata retrieves the metadata of a value payload from the object storage.
func (tangle *Tangle) PayloadMetadata(payloadID payload.ID) *CachedPayloadMetadata {
	return &CachedPayloadMetadata{CachedObject: tangle.payloadMetadataStorage.Load(payloadID.Bytes())}
}

// Approvers retrieves the approvers of a payload from the object storage.
func (tangle *Tangle) Approvers(payloadID payload.ID) CachedApprovers {
	approvers := make(CachedApprovers, 0)
	tangle.approverStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool {
		approvers = append(approvers, &CachedPayloadApprover{CachedObject: cachedObject})

		return true
	}, payloadID.Bytes())

	return approvers
}

// 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) {
		approvingCachedPayload := tangle.Payload(approver.ApprovingPayloadID())

		approvingCachedPayload.Consume(func(payload *payload.Payload) {
			consume(approvingCachedPayload.Retain(), tangle.PayloadMetadata(approver.ApprovingPayloadID()), tangle.Transaction(payload.Transaction().ID()), tangle.TransactionMetadata(payload.Transaction().ID()))
		})
	})
}

// ForEachConsumers iterates through the transactions that are consuming outputs of the given transactions
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) {
			if _, transactionSeen := seenTransactions[consumer.TransactionID()]; !transactionSeen {
				seenTransactions[consumer.TransactionID()] = types.Void

				cachedTransaction := tangle.Transaction(consumer.TransactionID())
				defer cachedTransaction.Release()

				cachedTransactionMetadata := tangle.TransactionMetadata(consumer.TransactionID())
				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)
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region DAG SYNCHRONIZATION //////////////////////////////////////////////////////////////////////////////////////////

// setupDAGSynchronization sets up the behavior how the branch dag and the value tangle and UTXO dag are connected.
func (tangle *Tangle) setupDAGSynchronization() {
	tangle.branchManager.Events.BranchPreferred.Attach(events.NewClosure(tangle.onBranchPreferred))
	tangle.branchManager.Events.BranchUnpreferred.Attach(events.NewClosure(tangle.onBranchUnpreferred))
	tangle.branchManager.Events.BranchLiked.Attach(events.NewClosure(tangle.onBranchLiked))
	tangle.branchManager.Events.BranchDisliked.Attach(events.NewClosure(tangle.onBranchDisliked))
	tangle.branchManager.Events.BranchFinalized.Attach(events.NewClosure(tangle.onBranchFinalized))
	tangle.branchManager.Events.BranchConfirmed.Attach(events.NewClosure(tangle.onBranchConfirmed))
	tangle.branchManager.Events.BranchRejected.Attach(events.NewClosure(tangle.onBranchRejected))
}

// onBranchPreferred gets triggered when a branch in the branch DAG is marked as preferred.
func (tangle *Tangle) onBranchPreferred(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchPreferredChangesToTangle(cachedBranch, true)
}

// onBranchUnpreferred gets triggered when a branch in the branch DAG is marked as NOT preferred.
func (tangle *Tangle) onBranchUnpreferred(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchPreferredChangesToTangle(cachedBranch, false)
}

// onBranchLiked gets triggered when a branch in the branch DAG is marked as liked.
func (tangle *Tangle) onBranchLiked(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchedLikedChangesToTangle(cachedBranch, true)
}

// onBranchDisliked gets triggered when a branch in the branch DAG is marked as disliked.
func (tangle *Tangle) onBranchDisliked(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchedLikedChangesToTangle(cachedBranch, false)
}

// onBranchFinalized gets triggered when a branch in the branch DAG is marked as finalized.
func (tangle *Tangle) onBranchFinalized(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchFinalizedChangesToTangle(cachedBranch)
}

// onBranchConfirmed gets triggered when a branch in the branch DAG is marked as confirmed.
func (tangle *Tangle) onBranchConfirmed(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchConfirmedRejectedChangesToTangle(cachedBranch, true)
}

// onBranchRejected gets triggered when a branch in the branch DAG is marked as rejected.
func (tangle *Tangle) onBranchRejected(cachedBranch *branchmanager.CachedBranch) {
	tangle.propagateBranchConfirmedRejectedChangesToTangle(cachedBranch, false)
}

// propagateBranchPreferredChangesToTangle triggers the propagation of preferred status changes of a branch to the value
// tangle and its UTXO DAG.
func (tangle *Tangle) propagateBranchPreferredChangesToTangle(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, EventSourceBranchManager)
			if err != nil {
				tangle.Events.Error.Trigger(err)

				return
			}
		}
	})
}

// propagateBranchFinalizedChangesToTangle triggers the propagation of finalized status changes of a branch to the value
// tangle and its UTXO DAG.
func (tangle *Tangle) propagateBranchFinalizedChangesToTangle(cachedBranch *branchmanager.CachedBranch) {
	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.setTransactionFinalized(transactionID, EventSourceBranchManager)
			if err != nil {
				tangle.Events.Error.Trigger(err)

				return
			}
		}
	})
}

// propagateBranchedLikedChangesToTangle triggers the propagation of liked status changes of a branch to the value
// tangle and its UTXO DAG.
func (tangle *Tangle) propagateBranchedLikedChangesToTangle(cachedBranch *branchmanager.CachedBranch, liked 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
			}

			// propagate changes to future cone of transaction (value tangle)
			tangle.propagateValuePayloadLikeUpdates(transactionID, liked)
		}
	})
}

// propagateBranchConfirmedRejectedChangesToTangle triggers the propagation of confirmed and rejected status changes of
// a branch to the value tangle and its UTXO DAG.
func (tangle *Tangle) propagateBranchConfirmedRejectedChangesToTangle(cachedBranch *branchmanager.CachedBranch, confirmed 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
			}

			// propagate changes to future cone of transaction (value tangle)
			tangle.propagateValuePayloadConfirmedRejectedUpdates(transactionID, confirmed)
		}
	})
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region PRIVATE UTILITY METHODS //////////////////////////////////////////////////////////////////////////////////////

func (tangle *Tangle) setTransactionFinalized(transactionID transaction.ID, eventSource EventSource) (modified bool, err error) {
	defer debugger.FunctionCall("setTransactionFinalized", transactionID, eventSource).Return()

	// retrieve metadata and consume
	cachedTransactionMetadata := tangle.TransactionMetadata(transactionID)
	cachedTransactionMetadata.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 {
			// set outputs to be finalized as well
			tangle.Transaction(transactionID).Consume(func(tx *transaction.Transaction) {
				tx.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
					tangle.TransactionOutput(transaction.NewOutputID(address, transactionID)).Consume(func(output *Output) {
						output.setFinalized(true)
					})

					return true
				})
			})

			// retrieve transaction from the database (for the events)
			cachedTransaction := tangle.Transaction(transactionID)
			defer cachedTransaction.Release()
			if !cachedTransaction.Exists() {
				return
			}

			// trigger the corresponding event
			tangle.Events.TransactionFinalized.Trigger(&CachedTransactionEvent{
				Transaction:         cachedTransaction,
				TransactionMetadata: cachedTransactionMetadata})

			// propagate the rejected flag
			if !metadata.Preferred() && !metadata.Rejected() {
				tangle.propagateRejectedToTransactions(metadata.ID())
			}

			// propagate changes to value tangle and branch DAG if we were called from the tangle
			// Note: if the update was triggered by a change in the branch DAG then we do not propagate the confirmed
			//       and rejected changes yet as those require the branch to be liked before (we instead do it in the
			//       BranchLiked event)
			if eventSource == EventSourceTangle {
				// 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.propagateValuePayloadConfirmedRejectedUpdates(transactionID, metadata.Preferred())
			}
		}
	})

	return
}

// propagateRejectedToTransactions propagates the rejected flag to a transaction, its outputs and to its consumers.
func (tangle *Tangle) propagateRejectedToTransactions(transactionID transaction.ID) {
	defer debugger.FunctionCall("propagateRejectedToTransactions", transactionID).Return()

	// initialize stack with first transaction
	rejectedPropagationStack := list.New()
	rejectedPropagationStack.PushBack(transactionID)

	// keep track of the added transactions so we don't add them multiple times
	addedTransaction := make(map[transaction.ID]types.Empty)

	// work through stack
	for rejectedPropagationStack.Len() >= 1 {
		// pop the first element from the stack
		firstElement := rejectedPropagationStack.Front()
		rejectedPropagationStack.Remove(firstElement)
		currentTransactionID := firstElement.Value.(transaction.ID)

		debugger.Print("rejectedPropagationStack.Front()", currentTransactionID)

		cachedTransactionMetadata := tangle.TransactionMetadata(currentTransactionID)
		cachedTransactionMetadata.Consume(func(metadata *TransactionMetadata) {
			cachedTransaction := tangle.Transaction(currentTransactionID)
			cachedTransaction.Consume(func(tx *transaction.Transaction) {
				if !metadata.setRejected(true) {
					return
				}

				if metadata.setPreferred(false) {
					// set outputs to be not preferred as well
					tangle.Transaction(currentTransactionID).Consume(func(tx *transaction.Transaction) {
						tx.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
							tangle.TransactionOutput(transaction.NewOutputID(address, currentTransactionID)).Consume(func(output *Output) {
								output.setPreferred(false)
							})

							return true
						})
					})

					tangle.Events.TransactionUnpreferred.Trigger(&CachedTransactionEvent{
						Transaction:         cachedTransaction,
						TransactionMetadata: cachedTransactionMetadata})
				}

				// if the transaction is not finalized, yet then we set it to finalized
				if !metadata.Finalized() {
					if _, err := tangle.setTransactionFinalized(metadata.ID(), EventSourceTangle); err != nil {
						tangle.Events.Error.Trigger(err)

						return
					}
				}

				// process all outputs
				tx.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
					outputID := transaction.NewOutputID(address, currentTransactionID)

					// mark the output to be rejected
					tangle.TransactionOutput(outputID).Consume(func(output *Output) {
						output.setRejected(true)
					})

					// queue consumers to also be rejected
					tangle.Consumers(outputID).Consume(func(consumer *Consumer) {
						if _, transactionAdded := addedTransaction[consumer.TransactionID()]; transactionAdded {
							return
						}
						addedTransaction[consumer.TransactionID()] = types.Void

						rejectedPropagationStack.PushBack(consumer.TransactionID())
					})

					return true
				})

				// trigger event
				tangle.Events.TransactionRejected.Trigger(&CachedTransactionEvent{
					Transaction:         cachedTransaction,
					TransactionMetadata: cachedTransactionMetadata})
			})
		})
	}
}

// TODO: WRITE COMMENT
func (tangle *Tangle) propagateValuePayloadConfirmedRejectedUpdates(transactionID transaction.ID, confirmed bool) {
	defer debugger.FunctionCall("propagateValuePayloadConfirmedRejectedUpdates", transactionID, confirmed).Return()

	// 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),
		})
	})

	// iterate through stack (future cone of transactions)
	for propagationStack.Len() >= 1 {
		currentAttachmentEntry := propagationStack.Front()
		tangle.propagateValuePayloadConfirmedRejectedUpdateStackEntry(propagationStack, currentAttachmentEntry.Value.(*valuePayloadPropagationStackEntry), confirmed)
		propagationStack.Remove(currentAttachmentEntry)
	}
}

func (tangle *Tangle) propagateValuePayloadConfirmedRejectedUpdateStackEntry(propagationStack *list.List, propagationStackEntry *valuePayloadPropagationStackEntry, confirmed bool) {
	// 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
	}

	defer debugger.FunctionCall("propagateValuePayloadConfirmedRejectedUpdateStackEntry", currentPayload.ID(), currentTransaction.ID()).Return()

	// perform different logic depending on the type of the change (liked vs dislike)
	switch confirmed {
	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() || !currentTransactionMetadata.Finalized() || !tangle.BranchManager().IsBranchConfirmed(currentPayloadMetadata.BranchID()) || !tangle.ValuePayloadsConfirmed(currentPayload.Parent1ID(), currentPayload.Parent2ID()) || !currentPayloadMetadata.setConfirmed(true) {
			return
		}

		// trigger payload event
		tangle.Events.PayloadConfirmed.Trigger(&CachedPayloadEvent{
			Payload:         propagationStackEntry.CachedPayload,
			PayloadMetadata: propagationStackEntry.CachedPayloadMetadata})

		// propagate confirmed status to transaction and its outputs
		if currentTransactionMetadata.setConfirmed(true) {
			currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
				tangle.TransactionOutput(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(output *Output) {
					output.setConfirmed(true)
				})

				return true
			})

			tangle.Events.TransactionConfirmed.Trigger(&CachedTransactionEvent{
				Transaction:         propagationStackEntry.CachedTransaction,
				TransactionMetadata: propagationStackEntry.CachedTransactionMetadata})
		}
	case false:
		// abort if transaction is not finalized and neither of parents is rejected
		if !currentTransactionMetadata.Finalized() && !(tangle.payloadRejected(currentPayload.Parent2ID()) || tangle.payloadRejected(currentPayload.Parent1ID())) {
			return
		}

		// abort if the payload has been marked as disliked before
		if !currentPayloadMetadata.setRejected(true) {
			return
		}

		tangle.Events.PayloadRejected.Trigger(&CachedPayloadEvent{
			Payload:         propagationStackEntry.CachedPayload,
			PayloadMetadata: propagationStackEntry.CachedPayloadMetadata})
	}

	// schedule checks of approvers and consumers
	tangle.ForEachConsumersAndApprovers(currentPayload, tangle.createValuePayloadFutureConeIterator(propagationStack, make(map[payload.ID]types.Empty)))
}

// setTransactionPreferred is an internal utility method that updates the preferred flag and triggers changes to the
// branch DAG and triggers an updates of the liked flags in the value tangle
func (tangle *Tangle) setTransactionPreferred(transactionID transaction.ID, preferred bool, eventSource EventSource) (modified bool, err error) {
	// retrieve metadata and consume
	cachedTransactionMetadata := tangle.TransactionMetadata(transactionID)
	cachedTransactionMetadata.Consume(func(metadata *TransactionMetadata) {
		// update the preferred flag of the transaction
		modified = metadata.setPreferred(preferred)

		// only do something if the flag was modified
		if modified {
			// update outputs as well
			tangle.Transaction(transactionID).Consume(func(tx *transaction.Transaction) {
				tx.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
					tangle.TransactionOutput(transaction.NewOutputID(address, transactionID)).Consume(func(output *Output) {
						output.setPreferred(preferred)
					})

					return true
				})
			})

			// retrieve transaction from the database (for the events)
			cachedTransaction := tangle.Transaction(transactionID)
			defer cachedTransaction.Release()
			if !cachedTransaction.Exists() {
				return
			}

			// trigger the correct event
			if preferred {
				tangle.Events.TransactionPreferred.Trigger(&CachedTransactionEvent{
					Transaction:         cachedTransaction,
					TransactionMetadata: cachedTransactionMetadata})
			} else {
				tangle.Events.TransactionUnpreferred.Trigger(&CachedTransactionEvent{
					Transaction:         cachedTransaction,
					TransactionMetadata: cachedTransactionMetadata})
			}

			// propagate changes to value tangle and branch DAG if we were called from the tangle
			// Note: if the update was triggered by a change in the branch DAG then we do not propagate the value
			//       payload changes yet as those require the branch to be liked before (we instead do it in the
			//       BranchLiked event)
			if eventSource == EventSourceTangle {
				// 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),
		})
	})

	// iterate through stack (future cone of transactions)
	for propagationStack.Len() >= 1 {
		currentAttachmentEntry := propagationStack.Front()
		tangle.processValuePayloadLikedUpdateStackEntry(propagationStack, 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, 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.Parent1ID(), currentPayload.Parent2ID()) || !currentPayloadMetadata.setLiked(liked) {
			return
		}

		// trigger payload event
		tangle.Events.PayloadLiked.Trigger(&CachedPayloadEvent{
			Payload:         propagationStackEntry.CachedPayload,
			PayloadMetadata: propagationStackEntry.CachedPayloadMetadata})

		// propagate liked to transaction and its outputs
		if currentTransactionMetadata.setLiked(true) {
			currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
				tangle.TransactionOutput(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(output *Output) {
					output.setLiked(true)
				})

				return true
			})

			// trigger event
			tangle.Events.TransactionLiked.Trigger(&CachedTransactionEvent{
				Transaction:         propagationStackEntry.CachedTransaction,
				TransactionMetadata: propagationStackEntry.CachedTransactionMetadata})
		}
	case false:
		// abort if the payload has been marked as disliked before
		if !currentPayloadMetadata.setLiked(liked) {
			return
		}

		tangle.Events.PayloadDisliked.Trigger(&CachedPayloadEvent{
			Payload:         propagationStackEntry.CachedPayload,
			PayloadMetadata: propagationStackEntry.CachedPayloadMetadata})

		// look if we still have any liked attachments of this transaction
		likedAttachmentFound := false
		tangle.Attachments(currentTransaction.ID()).Consume(func(attachment *Attachment) {
			tangle.PayloadMetadata(attachment.PayloadID()).Consume(func(payloadMetadata *PayloadMetadata) {
				likedAttachmentFound = likedAttachmentFound || payloadMetadata.Liked()
			})
		})

		// if there are no other liked attachments of this transaction then also set it to disliked
		if !likedAttachmentFound {
			// propagate disliked to transaction and its outputs
			if currentTransactionMetadata.setLiked(false) {
				currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
					tangle.TransactionOutput(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(output *Output) {
						output.setLiked(false)
					})

					return true
				})

				// trigger event
				tangle.Events.TransactionDisliked.Trigger(&CachedTransactionEvent{
					Transaction:         propagationStackEntry.CachedTransaction,
					TransactionMetadata: propagationStackEntry.CachedTransactionMetadata})
			}
		}
	}

	// schedule checks of approvers and consumers
	tangle.ForEachConsumersAndApprovers(currentPayload, tangle.createValuePayloadFutureConeIterator(propagationStack, make(map[payload.ID]types.Empty)))
}

// 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(),
		})
	}
}

func (tangle *Tangle) payloadRejected(payloadID payload.ID) (rejected bool) {
	tangle.PayloadMetadata(payloadID).Consume(func(payloadMetadata *PayloadMetadata) {
		rejected = payloadMetadata.Rejected()
	})
	return
}

func (tangle *Tangle) storePayload(payloadToStore *payload.Payload) (cachedPayload *payload.CachedPayload, cachedMetadata *CachedPayloadMetadata, payloadStored bool) {
	storedPayload, newPayload := tangle.payloadStorage.StoreIfAbsent(payloadToStore)
	if !newPayload {
		return
	}

	cachedPayload = &payload.CachedPayload{CachedObject: storedPayload}
	cachedMetadata = &CachedPayloadMetadata{CachedObject: tangle.payloadMetadataStorage.Store(NewPayloadMetadata(payloadToStore.ID()))}
	payloadStored = true

	return
}

func (tangle *Tangle) storeTransactionModels(solidPayload *payload.Payload) (cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment, transactionIsNew bool) {
	cachedTransaction = &transaction.CachedTransaction{CachedObject: tangle.transactionStorage.ComputeIfAbsent(solidPayload.Transaction().ID().Bytes(), func(key []byte) objectstorage.StorableObject {
		transactionIsNew = true

		result := solidPayload.Transaction()
		result.Persist()
		result.SetModified()

		return result
	})}

	if transactionIsNew {
		cachedTransactionMetadata = &CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Store(NewTransactionMetadata(solidPayload.Transaction().ID()))}

		// store references to the consumed outputs
		solidPayload.Transaction().Inputs().ForEach(func(outputId transaction.OutputID) bool {
			tangle.consumerStorage.Store(NewConsumer(outputId, solidPayload.Transaction().ID())).Release()

			return true
		})
	} else {
		cachedTransactionMetadata = &CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Load(solidPayload.Transaction().ID().Bytes())}
	}

	// store a reference from the transaction to the payload that attached it or abort, if we have processed this attachment already
	attachment, stored := tangle.attachmentStorage.StoreIfAbsent(NewAttachment(solidPayload.Transaction().ID(), solidPayload.ID()))
	if !stored {
		return
	}
	cachedAttachment = &CachedAttachment{CachedObject: attachment}

	return
}

func (tangle *Tangle) storePayloadReferences(payload *payload.Payload) {
	// store parent1 approver
	parent1ID := payload.Parent1ID()
	tangle.approverStorage.Store(NewPayloadApprover(parent1ID, payload.ID())).Release()

	// store parent2 approver
	if parent2ID := payload.Parent2ID(); parent2ID != parent1ID {
		tangle.approverStorage.Store(NewPayloadApprover(parent2ID, payload.ID())).Release()
	}
}

// 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(&valuePayloadPropagationStackEntry{
		CachedPayload:             cachedPayload,
		CachedPayloadMetadata:     cachedMetadata,
		CachedTransaction:         cachedTransaction,
		CachedTransactionMetadata: cachedTransactionMetadata,
	})

	// process payloads that are supposed to be checked for solidity recursively
	for solidificationStack.Len() > 0 {
		currentSolidificationEntry := solidificationStack.Front()
		tangle.processSolidificationStackEntry(solidificationStack, currentSolidificationEntry.Value.(*valuePayloadPropagationStackEntry))
		solidificationStack.Remove(currentSolidificationEntry)
	}
}

// deleteTransactionFutureCone removes a transaction and its whole future cone from the database (including all of the
// reference models).
func (tangle *Tangle) deleteTransactionFutureCone(transactionID transaction.ID, cause error) {
	// initialize stack with current transaction
	deleteStack := list.New()
	deleteStack.PushBack(transactionID)

	// iterate through stack
	for deleteStack.Len() >= 1 {
		// pop first element from stack
		currentTransactionIDEntry := deleteStack.Front()
		deleteStack.Remove(currentTransactionIDEntry)
		currentTransactionID := currentTransactionIDEntry.Value.(transaction.ID)

		// delete the transaction
		consumers, attachments := tangle.deleteTransaction(currentTransactionID, cause)

		// queue consumers to also be deleted
		for _, consumer := range consumers {
			deleteStack.PushBack(consumer)
		}

		// remove payload future cone
		for _, attachingPayloadID := range attachments {
			tangle.deletePayloadFutureCone(attachingPayloadID, cause)
		}
	}
}

// deleteTransaction deletes a single transaction and all of its related models from the database.
// Note: We do not immediately remove the attachments as this is related to the Payloads and is therefore left to the
//       caller to clean this up.
func (tangle *Tangle) deleteTransaction(transactionID transaction.ID, cause error) (consumers []transaction.ID, attachments []payload.ID) {
	// create result
	consumers = make([]transaction.ID, 0)
	attachments = make([]payload.ID, 0)

	cachedTransaction := tangle.Transaction(transactionID)
	cachedTransactionMetadata := tangle.TransactionMetadata(transactionID)

	// process transaction and its models
	cachedTransaction.Consume(func(tx *transaction.Transaction) {
		// if the removal was triggered by an invalid Transaction
		if errors.Is(cause, ErrTransactionInvalid) {
			tangle.Events.TransactionInvalid.Trigger(&CachedTransactionEvent{
				Transaction:         cachedTransaction,
				TransactionMetadata: cachedTransactionMetadata}, cause)
		}
		// mark transaction as deleted
		tx.Delete()

		// cleanup inputs
		tx.Inputs().ForEach(func(outputId transaction.OutputID) bool {
			// delete consumer pointers of the inputs of the current transaction
			tangle.consumerStorage.Delete(marshalutil.New(transaction.OutputIDLength + transaction.IDLength).WriteBytes(outputId.Bytes()).WriteBytes(transactionID.Bytes()).Bytes())

			return true
		})

		// introduce map to keep track of seen consumers (so we don't process them twice)
		seenConsumers := make(map[transaction.ID]types.Empty)
		seenConsumers[transactionID] = types.Void

		// cleanup outputs
		tx.Outputs().ForEach(func(addr address.Address, balances []*balance.Balance) bool {
			// delete outputs
			tangle.outputStorage.Delete(marshalutil.New(address.Length + transaction.IDLength).WriteBytes(addr.Bytes()).WriteBytes(transactionID.Bytes()).Bytes())

			// process consumers
			tangle.Consumers(transaction.NewOutputID(addr, transactionID)).Consume(func(consumer *Consumer) {
				// check if the transaction has been queued already
				if _, consumerSeenAlready := seenConsumers[consumer.TransactionID()]; consumerSeenAlready {
					return
				}
				seenConsumers[consumer.TransactionID()] = types.Void

				// queue consumers for deletion
				consumers = append(consumers, consumer.TransactionID())
			})

			return true
		})
	})

	// delete transaction metadata
	cachedTransactionMetadata.Consume(func(metadata *TransactionMetadata) {
		metadata.Delete()
	})

	// process attachments
	tangle.Attachments(transactionID).Consume(func(attachment *Attachment) {
		attachments = append(attachments, attachment.PayloadID())
	})

	return
}

// deletePayloadFutureCone removes a payload and its whole future cone from the database (including all of the reference
// models).
func (tangle *Tangle) deletePayloadFutureCone(payloadID payload.ID, cause error) {
	// initialize stack with current transaction
	deleteStack := list.New()
	deleteStack.PushBack(payloadID)

	// iterate through stack
	for deleteStack.Len() >= 1 {
		// pop first element from stack
		currentTransactionIDEntry := deleteStack.Front()
		deleteStack.Remove(currentTransactionIDEntry)
		currentPayloadID := currentTransactionIDEntry.Value.(payload.ID)

		cachedPayload := tangle.Payload(currentPayloadID)
		cachedPayloadMetadata := tangle.PayloadMetadata(currentPayloadID)

		// process payload
		cachedPayload.Consume(func(currentPayload *payload.Payload) {
			// trigger payload invalid if it was called with an "invalid cause"
			if errors.Is(cause, ErrPayloadInvalid) || errors.Is(cause, ErrTransactionInvalid) {
				tangle.Events.PayloadInvalid.Trigger(&CachedPayloadEvent{
					Payload:         cachedPayload,
					PayloadMetadata: cachedPayloadMetadata}, cause)
			}

			// delete payload
			currentPayload.Delete()

			// delete approvers
			tangle.approverStorage.Delete(marshalutil.New(2 * payload.IDLength).WriteBytes(currentPayload.Parent2ID().Bytes()).WriteBytes(currentPayloadID.Bytes()).Bytes())
			if currentPayload.Parent1ID() != currentPayload.Parent2ID() {
				tangle.approverStorage.Delete(marshalutil.New(2 * payload.IDLength).WriteBytes(currentPayload.Parent1ID().Bytes()).WriteBytes(currentPayloadID.Bytes()).Bytes())
			}

			// delete attachment
			tangle.attachmentStorage.Delete(marshalutil.New(transaction.IDLength + payload.IDLength).WriteBytes(currentPayload.Transaction().ID().Bytes()).WriteBytes(currentPayloadID.Bytes()).Bytes())

			// if this was the last attachment of the transaction then we also delete the transaction
			if !tangle.Attachments(currentPayload.Transaction().ID()).Consume(func(attachment *Attachment) {}) {
				tangle.deleteTransaction(currentPayload.Transaction().ID(), nil)
			}
		})

		// delete payload metadata
		cachedPayloadMetadata.Consume(func(payloadMetadata *PayloadMetadata) {
			payloadMetadata.Delete()
		})

		// queue approvers
		tangle.Approvers(currentPayloadID).Consume(func(approver *PayloadApprover) {
			deleteStack.PushBack(approver.ApprovingPayloadID())
		})
	}
}

// processSolidificationStackEntry processes a single entry of the solidification stack and schedules its approvers and
// consumers if necessary.
func (tangle *Tangle) processSolidificationStackEntry(solidificationStack *list.List, solidificationStackEntry *valuePayloadPropagationStackEntry) {
	// release stack entry when we are done
	defer solidificationStackEntry.Release()

	// 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
	}

	// abort if the transaction is not solid or invalid
	transactionSolid, consumedBranches, transactionSolidityErr := tangle.checkTransactionSolidity(currentTransaction, currentTransactionMetadata)
	if transactionSolidityErr != nil {
		tangle.deleteTransactionFutureCone(currentTransaction.ID(), transactionSolidityErr)

		return
	}
	if !transactionSolid {
		return
	}

	// abort if the payload is not solid or invalid
	payloadSolid, payloadSolidityErr := tangle.payloadBecameNewlySolid(currentPayload, currentPayloadMetadata, consumedBranches)
	if payloadSolidityErr != nil {
		tangle.deletePayloadFutureCone(currentPayload.ID(), payloadSolidityErr)

		return
	}
	if !payloadSolid {
		return
	}

	// book the solid entities
	transactionBooked, payloadBooked, decisionPending, bookingErr := tangle.book(solidificationStackEntry.Retain())
	if bookingErr != nil {
		tangle.Events.Error.Trigger(bookingErr)

		return
	}

	// keep track of the added payloads so we do not add them multiple times
	processedPayloads := make(map[payload.ID]types.Empty)

	// trigger events and schedule check of approvers / consumers
	if transactionBooked {
		tangle.Events.TransactionBooked.Trigger(&CachedTransactionBookEvent{
			Transaction:         solidificationStackEntry.CachedTransaction,
			TransactionMetadata: solidificationStackEntry.CachedTransactionMetadata,
			Pending:             decisionPending})

		tangle.ForEachConsumers(currentTransaction, tangle.createValuePayloadFutureConeIterator(solidificationStack, processedPayloads))
	}
	if payloadBooked {
		tangle.ForeachApprovers(currentPayload.ID(), tangle.createValuePayloadFutureConeIterator(solidificationStack, processedPayloads))
	}
}

func (tangle *Tangle) book(entitiesToBook *valuePayloadPropagationStackEntry) (transactionBooked bool, payloadBooked bool, decisionPending bool, err error) {
	defer entitiesToBook.Release()

	if transactionBooked, decisionPending, err = tangle.bookTransaction(entitiesToBook.CachedTransaction.Retain(), entitiesToBook.CachedTransactionMetadata.Retain()); err != nil {
		return
	}

	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, decisionPending bool, err error) {
	defer cachedTransaction.Release()
	defer cachedTransactionMetadata.Release()

	transactionToBook := cachedTransaction.Unwrap()
	if transactionToBook == nil {
		// TODO: explicit error var
		err = errors.New("failed to unwrap transaction")

		return
	}

	transactionMetadata := cachedTransactionMetadata.Unwrap()
	if transactionMetadata == nil {
		// TODO: explicit error var
		err = errors.New("failed to unwrap transaction metadata")

		return
	}

	// abort if transaction was marked as solid before
	if !transactionMetadata.setSolid(true) {
		return
	}

	// trigger event if transaction became solid
	tangle.Events.TransactionSolid.Trigger(&CachedTransactionEvent{
		Transaction:         cachedTransaction,
		TransactionMetadata: cachedTransactionMetadata})

	consumedBranches := make(branchmanager.BranchIds)
	conflictingInputs := make([]transaction.OutputID, 0)
	conflictingInputsOfFirstConsumers := make(map[transaction.ID][]transaction.OutputID)

	finalizedConflictingSpenderFound := false
	if !transactionToBook.Inputs().ForEach(func(outputID transaction.OutputID) bool {
		cachedOutput := tangle.TransactionOutput(outputID)
		defer cachedOutput.Release()

		// abort if the output could not be found
		output := cachedOutput.Unwrap()
		if output == nil {
			err = fmt.Errorf("could not load output '%s': %w", outputID, ErrFatal)

			return false
		}

		consumedBranches[output.BranchID()] = types.Void

		// register the current consumer and check if the input has been consumed before
		consumerCount, firstConsumerID := output.RegisterConsumer(transactionToBook.ID())
		switch consumerCount {
		// continue if we are the first consumer and there is no double spend
		case 0:
			return true

		// if the input has been consumed before but not been forked, yet
		case 1:
			// keep track of the conflicting inputs so we can fork them
			if _, conflictingInputsExist := conflictingInputsOfFirstConsumers[firstConsumerID]; !conflictingInputsExist {
				conflictingInputsOfFirstConsumers[firstConsumerID] = make([]transaction.OutputID, 0)
			}
			conflictingInputsOfFirstConsumers[firstConsumerID] = append(conflictingInputsOfFirstConsumers[firstConsumerID], outputID)
		}

		// check if any of the consumers were finalized already
		if !finalizedConflictingSpenderFound {
			tangle.Consumers(outputID).Consume(func(consumer *Consumer) {
				if !finalizedConflictingSpenderFound {
					tangle.TransactionMetadata(consumer.TransactionID()).Consume(func(metadata *TransactionMetadata) {
						finalizedConflictingSpenderFound = metadata.Preferred() && metadata.Finalized()
					})
				}
			})
		}

		// mark input as conflicting
		conflictingInputs = append(conflictingInputs, outputID)

		return true
	}) {
		return
	}

	cachedTargetBranch, err := tangle.branchManager.AggregateBranches(consumedBranches.ToList()...)
	if err != nil {
		return
	}
	defer cachedTargetBranch.Release()

	targetBranch := cachedTargetBranch.Unwrap()
	if targetBranch == nil {
		err = errors.New("failed to unwrap target branch")

		return
	}
	targetBranch.Persist()

	if len(conflictingInputs) >= 1 {
		cachedTargetBranch, _ = tangle.branchManager.Fork(branchmanager.NewBranchID(transactionToBook.ID()), []branchmanager.BranchID{targetBranch.ID()}, conflictingInputs)
		defer cachedTargetBranch.Release()
		targetBranch = cachedTargetBranch.Unwrap()
		if targetBranch == nil {
			err = errors.New("failed to inherit branches")

			return
		}
	}

	// book transaction into target branch
	transactionMetadata.setBranchID(targetBranch.ID())

	// create color for newly minted coins
	mintedColor, _, err := balance.ColorFromBytes(transactionToBook.ID().Bytes())
	if err != nil {
		panic(err) // this should never happen (a transaction id is always a valid color)
	}

	// book outputs into the target branch
	transactionToBook.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
		// create correctly colored balances (replacing color of newly minted coins with color of transaction id)
		coloredBalances := make([]*balance.Balance, len(balances))
		for i, currentBalance := range balances {
			if currentBalance.Color == balance.ColorNew {
				coloredBalances[i] = balance.New(mintedColor, currentBalance.Value)
			} else {
				coloredBalances[i] = currentBalance
			}
		}

		// store output
		newOutput := NewOutput(address, transactionToBook.ID(), targetBranch.ID(), coloredBalances)
		newOutput.setSolid(true)
		tangle.outputStorage.Store(newOutput).Release()

		return true
	})

	// fork the conflicting transactions into their own branch if a decision is still pending
	decisionPending = !finalizedConflictingSpenderFound
	if decisionPending {
		for consumerID, conflictingInputs := range conflictingInputsOfFirstConsumers {
			_, decisionFinalized, forkedErr := tangle.Fork(consumerID, conflictingInputs)
			if forkedErr != nil {
				err = forkedErr

				return
			}

			decisionPending = decisionPending || !decisionFinalized
		}
	}
	transactionBooked = true

	return
}

func (tangle *Tangle) bookPayload(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *CachedPayloadMetadata, cachedTransactionMetadata *CachedTransactionMetadata) (payloadBooked bool, err error) {
	defer cachedPayload.Release()
	defer cachedPayloadMetadata.Release()
	defer cachedTransactionMetadata.Release()

	valueObject := cachedPayload.Unwrap()
	valueObjectMetadata := cachedPayloadMetadata.Unwrap()
	transactionMetadata := cachedTransactionMetadata.Unwrap()

	if valueObject == nil || valueObjectMetadata == nil || transactionMetadata == nil {
		return
	}

	parent2BranchID := tangle.payloadBranchID(valueObject.Parent2ID())
	parent1BranchID := tangle.payloadBranchID(valueObject.Parent1ID())
	transactionBranchID := transactionMetadata.BranchID()

	if parent2BranchID == branchmanager.UndefinedBranchID || parent1BranchID == branchmanager.UndefinedBranchID || transactionBranchID == branchmanager.UndefinedBranchID {
		return
	}

	// abort if the payload has been marked as solid before
	if !valueObjectMetadata.setSolid(true) {
		return
	}

	// trigger event if payload became solid
	tangle.Events.PayloadSolid.Trigger(&CachedPayloadEvent{
		Payload:         cachedPayload,
		PayloadMetadata: cachedPayloadMetadata})

	cachedAggregatedBranch, err := tangle.BranchManager().AggregateBranches([]branchmanager.BranchID{parent2BranchID, parent1BranchID, transactionBranchID}...)
	if err != nil {
		return
	}
	defer cachedAggregatedBranch.Release()

	aggregatedBranch := cachedAggregatedBranch.Unwrap()
	if aggregatedBranch == nil {
		return
	}

	payloadBooked = valueObjectMetadata.setBranchID(aggregatedBranch.ID())

	return
}

// payloadBranchID returns the BranchID that the referenced Payload was booked into.
func (tangle *Tangle) payloadBranchID(payloadID payload.ID) branchmanager.BranchID {
	if payloadID == payload.GenesisID {
		return branchmanager.MasterBranchID
	}

	cachedPayloadMetadata := tangle.PayloadMetadata(payloadID)
	defer cachedPayloadMetadata.Release()

	payloadMetadata := cachedPayloadMetadata.Unwrap()
	if payloadMetadata == nil {

		// if payload is missing and was not reported as missing, yet
		if cachedMissingPayload, missingPayloadStored := tangle.missingPayloadStorage.StoreIfAbsent(NewMissingPayload(payloadID)); missingPayloadStored {
			cachedMissingPayload.Consume(func(object objectstorage.StorableObject) {
				tangle.Events.PayloadMissing.Trigger(object.(*MissingPayload).ID())
			})
		}

		return branchmanager.UndefinedBranchID
	}

	// the BranchID is only set if the payload was also marked as solid
	return payloadMetadata.BranchID()
}

// payloadBecameNewlySolid returns true if the given payload is solid but was not marked as solid. yet.
func (tangle *Tangle) payloadBecameNewlySolid(p *payload.Payload, payloadMetadata *PayloadMetadata, transactionBranches []branchmanager.BranchID) (solid bool, err error) {
	// abort if the payload was deleted
	if p == nil || p.IsDeleted() || payloadMetadata == nil || payloadMetadata.IsDeleted() {
		return
	}

	// abort if the payload was marked as solid already
	if payloadMetadata.IsSolid() {
		return
	}

	combinedBranches := transactionBranches

	parent1BranchID := tangle.payloadBranchID(p.Parent1ID())
	if parent1BranchID == branchmanager.UndefinedBranchID {
		return false, nil
	}
	combinedBranches = append(combinedBranches, parent1BranchID)

	parent2BranchID := tangle.payloadBranchID(p.Parent2ID())
	if parent2BranchID == branchmanager.UndefinedBranchID {
		return false, nil
	}
	combinedBranches = append(combinedBranches, parent2BranchID)

	branchesConflicting, err := tangle.branchManager.BranchesConflicting(combinedBranches...)
	if err != nil {
		return
	}
	if branchesConflicting {
		err = fmt.Errorf("the payload '%s' combines conflicting versions of the ledger state: %w", p.ID(), ErrPayloadInvalid)

		return false, err
	}

	solid = true

	return
}

func (tangle *Tangle) checkTransactionSolidity(tx *transaction.Transaction, metadata *TransactionMetadata) (solid bool, consumedBranches []branchmanager.BranchID, err error) {
	// abort if any of the models are nil or has been deleted
	if tx == nil || tx.IsDeleted() || metadata == nil || metadata.IsDeleted() {
		return
	}

	// abort if we have previously determined the solidity status of the transaction already
	if metadata.Solid() {
		if solid = metadata.BranchID() != branchmanager.UndefinedBranchID; solid {
			consumedBranches = []branchmanager.BranchID{metadata.BranchID()}
		}

		return
	}

	// determine the consumed inputs and balances of the transaction
	inputsSolid, cachedInputs, consumedBalances, consumedBranchesMap, err := tangle.retrieveConsumedInputDetails(tx)
	if err != nil || !inputsSolid {
		return
	}
	defer cachedInputs.Release()

	// abort if the outputs are not matching the inputs
	if !tangle.checkTransactionOutputs(consumedBalances, tx.Outputs()) {
		err = fmt.Errorf("the outputs do not match the inputs in transaction with id '%s': %w", tx.ID(), ErrTransactionInvalid)

		return
	}

	// abort if the branches are conflicting or we faced an error when checking the validity
	consumedBranches = consumedBranchesMap.ToList()
	branchesConflicting, err := tangle.branchManager.BranchesConflicting(consumedBranches...)
	if err != nil {
		return
	}
	if branchesConflicting {
		err = fmt.Errorf("the transaction '%s' spends conflicting inputs: %w", tx.ID(), ErrTransactionInvalid)

		return
	}

	// set the result to be solid and valid
	solid = true

	return
}

func (tangle *Tangle) getCachedOutputsFromTransactionInputs(tx *transaction.Transaction) (result CachedOutputs) {
	result = make(CachedOutputs)
	tx.Inputs().ForEach(func(inputId transaction.OutputID) bool {
		result[inputId] = tangle.TransactionOutput(inputId)

		return true
	})

	return
}

// ValidateTransactionToAttach checks that the given transaction spends all funds from its inputs and
// that its the signature is valid.
func (tangle *Tangle) ValidateTransactionToAttach(tx *transaction.Transaction) (err error) {
	_, cachedInputs, consumedBalances, _, err := tangle.retrieveConsumedInputDetails(tx)
	defer cachedInputs.Release()
	if err != nil {
		return
	}
	if !tangle.checkTransactionOutputs(consumedBalances, tx.Outputs()) {
		err = ErrTransactionDoesNotSpendAllFunds
		return
	}

	if !tx.InputsCountValid() {
		err = ErrMaxTransactionInputCountExceeded
		return
	}

	if !tx.SignaturesValid() {
		err = ErrInvalidTransactionSignature
		return
	}
	return
}

// retrieveConsumedInputDetails retrieves the details of the consumed inputs of a transaction.
func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction) (inputsSolid bool, cachedInputs CachedOutputs, consumedBalances map[balance.Color]int64, consumedBranches branchmanager.BranchIds, err error) {
	cachedInputs = tangle.getCachedOutputsFromTransactionInputs(tx)
	consumedBalances = make(map[balance.Color]int64)
	consumedBranches = make(branchmanager.BranchIds)
	for _, cachedInput := range cachedInputs {
		input := cachedInput.Unwrap()
		if input == nil || !input.Solid() {
			cachedInputs.Release()

			return
		}

		consumedBranches[input.BranchID()] = types.Void

		// calculate the input balances
		for _, inputBalance := range input.Balances() {
			var newBalance int64
			if currentBalance, balanceExists := consumedBalances[inputBalance.Color]; balanceExists {
				// check overflows in the numbers
				if inputBalance.Value > math.MaxInt64-currentBalance {
					// TODO: make it an explicit error var
					err = fmt.Errorf("buffer overflow in balances of inputs: %w", ErrTransactionInvalid)

					cachedInputs.Release()

					return
				}

				newBalance = currentBalance + inputBalance.Value
			} else {
				newBalance = inputBalance.Value
			}
			consumedBalances[inputBalance.Color] = newBalance
		}
	}
	inputsSolid = true

	return
}

// checkTransactionOutputs is a utility function that returns true, if the outputs are consuming all of the given inputs
// (the sum of all the balance changes is 0). It also accounts for the ability to "recolor" coins during the creating of
// outputs. If this function returns false, then the outputs that are defined in the transaction are invalid and the
// transaction should be removed from the ledger state.
func (tangle *Tangle) checkTransactionOutputs(inputBalances map[balance.Color]int64, outputs *transaction.Outputs) bool {
	// create a variable to keep track of outputs that create a new color
	var newlyColoredCoins int64
	var uncoloredCoins int64

	// iterate through outputs and check them one by one
	aborted := !outputs.ForEach(func(address address.Address, balances []*balance.Balance) bool {
		for _, outputBalance := range balances {
			// abort if the output creates a negative or empty output
			if outputBalance.Value <= 0 {
				return false
			}

			// sidestep logic if we have a newly colored output (we check the supply later)
			if outputBalance.Color == balance.ColorNew {
				// catch overflows
				if newlyColoredCoins > math.MaxInt64-outputBalance.Value {
					return false
				}

				newlyColoredCoins += outputBalance.Value

				continue
			}

			// sidestep logic if we have ColorIOTA
			if outputBalance.Color == balance.ColorIOTA {
				// catch overflows
				if uncoloredCoins > math.MaxInt64-outputBalance.Value {
					return false
				}

				uncoloredCoins += outputBalance.Value

				continue
			}

			// check if the used color does not exist in our supply
			availableBalance, spentColorExists := inputBalances[outputBalance.Color]
			if !spentColorExists {
				return false
			}

			// abort if we spend more coins of the given color than we have
			if availableBalance < outputBalance.Value {
				return false
			}

			// subtract the spent coins from the supply of this color
			inputBalances[outputBalance.Color] -= outputBalance.Value

			// cleanup empty map entries (we have exhausted our funds)
			if inputBalances[outputBalance.Color] == 0 {
				delete(inputBalances, outputBalance.Color)
			}
		}

		return true
	})

	// abort if the previous checks failed
	if aborted {
		return false
	}

	// determine the unspent inputs
	var unspentCoins int64
	for _, unspentBalance := range inputBalances {
		// catch overflows
		if unspentCoins > math.MaxInt64-unspentBalance {
			return false
		}

		unspentCoins += unspentBalance
	}

	// the outputs are valid if they spend all consumed funds
	return unspentCoins == newlyColoredCoins+uncoloredCoins
}

// TODO: write comment what it does
func (tangle *Tangle) moveTransactionToBranch(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata, cachedTargetBranch *branchmanager.CachedBranch) (err error) {
	// push transaction that shall be moved to the stack
	transactionStack := list.New()
	branchStack := list.New()
	branchStack.PushBack([3]interface{}{cachedTransactionMetadata.Unwrap().BranchID(), cachedTargetBranch, transactionStack})
	transactionStack.PushBack([2]interface{}{cachedTransaction, cachedTransactionMetadata})

	// iterate through all transactions (grouped by their branch)
	for branchStack.Len() >= 1 {
		if err = func() error {
			// retrieve branch details from stack
			currentSolidificationEntry := branchStack.Front()
			currentSourceBranch := currentSolidificationEntry.Value.([3]interface{})[0].(branchmanager.BranchID)
			currentCachedTargetBranch := currentSolidificationEntry.Value.([3]interface{})[1].(*branchmanager.CachedBranch)
			transactionStack := currentSolidificationEntry.Value.([3]interface{})[2].(*list.List)
			branchStack.Remove(currentSolidificationEntry)
			defer currentCachedTargetBranch.Release()

			// unpack target branch
			targetBranch := currentCachedTargetBranch.Unwrap()
			if targetBranch == nil {
				return errors.New("failed to unpack branch")
			}

			// iterate through transactions
			for transactionStack.Len() >= 1 {
				if err = func() error {
					// retrieve transaction details from stack
					currentSolidificationEntry := transactionStack.Front()
					currentCachedTransaction := currentSolidificationEntry.Value.([2]interface{})[0].(*transaction.CachedTransaction)
					currentCachedTransactionMetadata := currentSolidificationEntry.Value.([2]interface{})[1].(*CachedTransactionMetadata)
					transactionStack.Remove(currentSolidificationEntry)
					defer currentCachedTransaction.Release()
					defer currentCachedTransactionMetadata.Release()

					// unwrap transaction
					currentTransaction := currentCachedTransaction.Unwrap()
					if currentTransaction == nil {
						return errors.New("failed to unwrap transaction")
					}
					// unwrap transaction metadata
					currentTransactionMetadata := currentCachedTransactionMetadata.Unwrap()
					if currentTransactionMetadata == nil {
						return errors.New("failed to unwrap transaction metadata")
					}

					// if we arrived at a nested branch
					if currentTransactionMetadata.BranchID() != currentSourceBranch {
						// abort if we the branch is a conflict branch or an error occurred while trying to elevate
						isConflictBranch, _, elevateErr := tangle.branchManager.ElevateConflictBranch(currentTransactionMetadata.BranchID(), targetBranch.ID())
						if elevateErr != nil || isConflictBranch {
							return elevateErr
						}

						// determine the new branch of the transaction
						newCachedTargetBranch, branchErr := tangle.calculateBranchOfTransaction(currentTransaction)
						if branchErr != nil {
							return branchErr
						}
						defer newCachedTargetBranch.Release()

						// unwrap the branch
						newTargetBranch := newCachedTargetBranch.Unwrap()
						if newTargetBranch == nil {
							return errors.New("failed to unwrap branch")
						}
						newTargetBranch.Persist()

						// add the new branch (with the current transaction as a starting point to the branch stack)
						newTransactionStack := list.New()
						newTransactionStack.PushBack([2]interface{}{currentCachedTransaction.Retain(), currentCachedTransactionMetadata.Retain()})
						branchStack.PushBack([3]interface{}{currentTransactionMetadata.BranchID(), newCachedTargetBranch.Retain(), newTransactionStack})

						return nil
					}

					// abort if we did not modify the branch of the transaction
					if !currentTransactionMetadata.setBranchID(targetBranch.ID()) {
						return nil
					}

					// update the payloads
					tangle.updateBranchOfValuePayloadsAttachingTransaction(currentTransactionMetadata.ID())

					// iterate through the outputs of the moved transaction
					currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool {
						// create reference to the output
						outputID := transaction.NewOutputID(address, currentTransaction.ID())

						// load output from database
						cachedOutput := tangle.TransactionOutput(outputID)
						defer cachedOutput.Release()

						// unwrap output
						output := cachedOutput.Unwrap()
						if output == nil {
							err = fmt.Errorf("failed to load output '%s': %w", outputID, ErrFatal)

							return false
						}

						// abort if the output was moved already
						if !output.setBranchID(targetBranch.ID()) {
							return true
						}

						// schedule consumers for further checks
						consumingTransactions := make(map[transaction.ID]types.Empty)
						tangle.Consumers(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(consumer *Consumer) {
							consumingTransactions[consumer.TransactionID()] = types.Void
						})
						for transactionID := range consumingTransactions {
							transactionStack.PushBack([2]interface{}{tangle.Transaction(transactionID), tangle.TransactionMetadata(transactionID)})
						}

						return true
					})

					return nil
				}(); err != nil {
					return err
				}
			}

			return nil
		}(); err != nil {
			return
		}
	}

	return
}

// updateBranchOfValuePayloadsAttachingTransaction updates the BranchID of all payloads that attach a certain
// transaction (and its approvers).
func (tangle *Tangle) updateBranchOfValuePayloadsAttachingTransaction(transactionID transaction.ID) {
	// initialize stack with the attachments of the given transaction
	payloadStack := list.New()
	tangle.Attachments(transactionID).Consume(func(attachment *Attachment) {
		payloadStack.PushBack(tangle.Payload(attachment.PayloadID()))
	})

	// iterate through the stack to update all payloads we found
	for payloadStack.Len() >= 1 {
		// pop the first element from the stack
		currentPayloadElement := payloadStack.Front()
		payloadStack.Remove(currentPayloadElement)

		// process the found element
		currentPayloadElement.Value.(*payload.CachedPayload).Consume(func(currentPayload *payload.Payload) {
			// determine branches of referenced payloads
			branchIDofParent2 := tangle.branchIDofPayload(currentPayload.Parent2ID())
			branchIDofParent1 := tangle.branchIDofPayload(currentPayload.Parent1ID())

			// determine branch of contained transaction
			var branchIDofTransaction branchmanager.BranchID
			if !tangle.TransactionMetadata(currentPayload.Transaction().ID()).Consume(func(metadata *TransactionMetadata) {
				branchIDofTransaction = metadata.BranchID()
			}) {
				return
			}

			// abort if any of the branches is undefined
			if branchIDofParent2 == branchmanager.UndefinedBranchID || branchIDofParent1 == branchmanager.UndefinedBranchID || branchIDofTransaction == branchmanager.UndefinedBranchID {
				return
			}

			// aggregate the branches or abort if we face an error
			cachedAggregatedBranch, err := tangle.branchManager.AggregateBranches(branchIDofParent2, branchIDofParent1, branchIDofTransaction)
			if err != nil {
				tangle.Events.Error.Trigger(err)

				return
			}

			// try to update the metadata of the payload and queue its approvers
			cachedAggregatedBranch.Consume(func(branch *branchmanager.Branch) {
				tangle.PayloadMetadata(currentPayload.ID()).Consume(func(payloadMetadata *PayloadMetadata) {
					if !payloadMetadata.setBranchID(branch.ID()) {
						return
					}

					// queue approvers for recursive updates
					tangle.ForeachApprovers(currentPayload.ID(), func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata) {
						payloadMetadata.Release()
						transaction.Release()
						transactionMetadata.Release()

						payloadStack.PushBack(payload)
					})
				})

			})
		})
	}
}

// branchIDofPayload returns the BranchID that a payload is booked into.
func (tangle *Tangle) branchIDofPayload(payloadID payload.ID) (branchID branchmanager.BranchID) {
	if payloadID == payload.GenesisID {
		return branchmanager.MasterBranchID
	}

	tangle.PayloadMetadata(payloadID).Consume(func(payloadMetadata *PayloadMetadata) {
		branchID = payloadMetadata.BranchID()
	})

	return
}

func (tangle *Tangle) calculateBranchOfTransaction(currentTransaction *transaction.Transaction) (branch *branchmanager.CachedBranch, err error) {
	consumedBranches := make(branchmanager.BranchIds)
	if !currentTransaction.Inputs().ForEach(func(outputId transaction.OutputID) bool {
		cachedTransactionOutput := tangle.TransactionOutput(outputId)
		defer cachedTransactionOutput.Release()

		transactionOutput := cachedTransactionOutput.Unwrap()
		if transactionOutput == nil {
			err = fmt.Errorf("failed to load output '%s': %w", outputId, ErrFatal)

			return false
		}

		consumedBranches[transactionOutput.BranchID()] = types.Void

		return true
	}) {
		return
	}

	branch, err = tangle.branchManager.AggregateBranches(consumedBranches.ToList()...)

	return
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// 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
}