-
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
* 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 fixChing-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
* 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
}