diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index 8cd496d2c6e2acdfa1899c85fceed4a112bcbd57..1fd95f1ee284a702cec70d6aa0064e0a93bb608c 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -9,11 +9,9 @@ import ( "github.com/iotaledger/hive.go/node" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/ledgerstate" valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/utxodag" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle" "github.com/iotaledger/goshimmer/packages/database" @@ -37,11 +35,8 @@ var ( // Tangle represents the value tangle that is used to express votes on value transactions. Tangle *tangle.Tangle - // UTXODAG represents the flow of funds that is derived from the value tangle. - UTXODAG *utxodag.UTXODAG - // LedgerState represents the ledger state, that keeps track of the liked branches and offers an API to access funds. - LedgerState *ledgerstate.LedgerState + LedgerState *tangle.LedgerState // log holds a reference to the logger used by this app. log *logger.Logger @@ -54,14 +49,13 @@ func configure(_ *node.Plugin) { // create instances Tangle = tangle.New(database.GetBadgerInstance()) - UTXODAG = utxodag.New(database.GetBadgerInstance(), Tangle) // subscribe to message-layer messagelayer.Tangle.Events.MessageSolid.Attach(events.NewClosure(onReceiveMessageFromMessageLayer)) // setup behavior of package instances - UTXODAG.Events.TransactionBooked.Attach(events.NewClosure(onTransactionBooked)) - UTXODAG.Events.Fork.Attach(events.NewClosure(onForkOfFirstConsumer)) + Tangle.Events.TransactionBooked.Attach(events.NewClosure(onTransactionBooked)) + Tangle.Events.Fork.Attach(events.NewClosure(onForkOfFirstConsumer)) configureFPC() // TODO: DECIDE WHAT WE SHOULD DO IF FPC FAILS -> cry @@ -76,12 +70,12 @@ func configure(_ *node.Plugin) { switch opinion { case vote.Like: - if _, err := UTXODAG.BranchManager().SetBranchPreferred(branchID, true); err != nil { + if _, err := Tangle.BranchManager().SetBranchPreferred(branchID, true); err != nil { panic(err) } // TODO: merge branch mutations into the parent branch case vote.Dislike: - if _, err := UTXODAG.BranchManager().SetBranchPreferred(branchID, false); err != nil { + if _, err := Tangle.BranchManager().SetBranchPreferred(branchID, false); err != nil { panic(err) } // TODO: merge branch mutations into the parent branch / cleanup @@ -93,7 +87,6 @@ func run(*node.Plugin) { _ = daemon.BackgroundWorker("Tangle", func(shutdownSignal <-chan struct{}) { <-shutdownSignal Tangle.Shutdown() - UTXODAG.Shutdown() }, shutdown.PriorityTangle) runFPC() @@ -127,7 +120,7 @@ func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cach Tangle.AttachPayload(valuePayload) } -func onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *utxodag.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID, decisionPending bool) { +func onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID, decisionPending bool) { defer cachedTransaction.Release() defer cachedTransactionMetadata.Release() defer cachedBranch.Release() @@ -174,7 +167,7 @@ func onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cache } // TODO: clarify what we do here -func onForkOfFirstConsumer(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *utxodag.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) { +func onForkOfFirstConsumer(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) { defer cachedTransaction.Release() defer cachedTransactionMetadata.Release() defer cachedBranch.Release() @@ -197,7 +190,7 @@ func onForkOfFirstConsumer(cachedTransaction *transaction.CachedTransaction, cac return } - if _, err := UTXODAG.BranchManager().SetBranchPreferred(branch.ID(), true); err != nil { + if _, err := Tangle.BranchManager().SetBranchPreferred(branch.ID(), true); err != nil { log.Error(err) } diff --git a/dapps/valuetransfers/fpc.go b/dapps/valuetransfers/fpc.go index 8e3c4f9f62c7431199b3c0f4c7a7307a605da6ed..d8daf02a96c3f813297a843f7343043a5ec9c261 100644 --- a/dapps/valuetransfers/fpc.go +++ b/dapps/valuetransfers/fpc.go @@ -108,7 +108,7 @@ func runFPC() { return vote.Unknown } - cachedBranch := UTXODAG.BranchManager().Branch(branchID) + cachedBranch := Tangle.BranchManager().Branch(branchID) defer cachedBranch.Release() branch := cachedBranch.Unwrap() diff --git a/dapps/valuetransfers/packages/branchmanager/branchmanager.go b/dapps/valuetransfers/packages/branchmanager/branchmanager.go index 53c9665ae46e02b28c881220493b1bac3ac3accd..ecade28a4d82b467ecd8ab225788bfd4b1862602 100644 --- a/dapps/valuetransfers/packages/branchmanager/branchmanager.go +++ b/dapps/valuetransfers/packages/branchmanager/branchmanager.go @@ -184,9 +184,9 @@ func (branchManager *BranchManager) ElevateConflictBranch(branchToElevate Branch // BranchesConflicting returns true if the given Branches are part of the same Conflicts and can therefore not be // merged. func (branchManager *BranchManager) BranchesConflicting(branchIds ...BranchID) (branchesConflicting bool, err error) { - // iterate through parameters and collect conflicting branches - conflictingBranches := make(map[BranchID]types.Empty) - processedBranches := make(map[BranchID]types.Empty) + // iterate through branches and collect conflicting branches + traversedBranches := make(map[BranchID]types.Empty) + blacklistedBranches := make(map[BranchID]types.Empty) for _, branchID := range branchIds { // add the current branch to the stack of branches to check ancestorStack := list.New() @@ -196,40 +196,44 @@ func (branchManager *BranchManager) BranchesConflicting(branchIds ...BranchID) ( for ancestorStack.Len() >= 1 { // retrieve branch from stack firstElement := ancestorStack.Front() - ancestorBranchID := firstElement.Value.(BranchID) + currentBranchID := firstElement.Value.(BranchID) ancestorStack.Remove(firstElement) // abort if we have seen this branch already - if _, processedAlready := processedBranches[ancestorBranchID]; processedAlready { + if _, traversedAlready := traversedBranches[currentBranchID]; traversedAlready { continue } - processedBranches[ancestorBranchID] = types.Void + + // abort if this branch was blacklisted by another branch already + if _, branchesConflicting = blacklistedBranches[currentBranchID]; branchesConflicting { + return + } // unpack the branch and abort if we failed to load it - cachedBranch := branchManager.Branch(branchID) - branch := cachedBranch.Unwrap() - if branch == nil { - err = fmt.Errorf("failed to load branch '%s'", ancestorBranchID) + currentCachedBranch := branchManager.Branch(currentBranchID) + currentBranch := currentCachedBranch.Unwrap() + if currentBranch == nil { + err = fmt.Errorf("failed to load branch '%s'", currentBranchID) - cachedBranch.Release() + currentCachedBranch.Release() return } // add the parents of the current branch to the list of branches to check - for _, parentBranchID := range branch.ParentBranches() { + for _, parentBranchID := range currentBranch.ParentBranches() { ancestorStack.PushBack(parentBranchID) } // abort the following checks if the branch is aggregated (aggregated branches have no own conflicts) - if branch.IsAggregated() { - cachedBranch.Release() + if currentBranch.IsAggregated() { + currentCachedBranch.Release() continue } // iterate through the conflicts and take note of its member branches - for conflictID := range branch.Conflicts() { + for conflictID := range currentBranch.Conflicts() { for _, cachedConflictMember := range branchManager.ConflictMembers(conflictID) { // unwrap the current ConflictMember conflictMember := cachedConflictMember.Unwrap() @@ -239,22 +243,28 @@ func (branchManager *BranchManager) BranchesConflicting(branchIds ...BranchID) ( continue } + if conflictMember.BranchID() == currentBranchID { + continue + } + // abort if this branch was found as a conflict of another branch already - if _, branchesConflicting = conflictingBranches[conflictMember.BranchID()]; branchesConflicting { + if _, branchesConflicting = traversedBranches[conflictMember.BranchID()]; branchesConflicting { cachedConflictMember.Release() - cachedBranch.Release() + currentCachedBranch.Release() return } // store the current conflict in the list of seen conflicting branches - conflictingBranches[conflictMember.BranchID()] = types.Void + blacklistedBranches[conflictMember.BranchID()] = types.Void cachedConflictMember.Release() } } - cachedBranch.Release() + currentCachedBranch.Release() + + traversedBranches[currentBranchID] = types.Void } } diff --git a/dapps/valuetransfers/packages/branchmanager/branchmanager_test.go b/dapps/valuetransfers/packages/branchmanager/branchmanager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f868df810ae246bf8613857709ea4474eeb53656 --- /dev/null +++ b/dapps/valuetransfers/packages/branchmanager/branchmanager_test.go @@ -0,0 +1,23 @@ +package branchmanager + +import ( + "fmt" + "testing" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/packages/binary/testutil" +) + +func TestSomething(t *testing.T) { + branchManager := New(testutil.DB(t)) + + cachedBranch1, _ := branchManager.Fork(BranchID{2}, []BranchID{MasterBranchID}, []ConflictID{transaction.OutputID{4}}) + defer cachedBranch1.Release() + _ = cachedBranch1.Unwrap() + + cachedBranch2, _ := branchManager.Fork(BranchID{3}, []BranchID{MasterBranchID}, []ConflictID{transaction.OutputID{4}}) + defer cachedBranch2.Release() + branch2 := cachedBranch2.Unwrap() + + fmt.Println(branchManager.BranchesConflicting(MasterBranchID, branch2.ID())) +} diff --git a/dapps/valuetransfers/packages/ledgerstate/ledgerstate.go b/dapps/valuetransfers/packages/ledgerstate/ledgerstate.go deleted file mode 100644 index 5cb09096b181e83d1f30aee9b242c0db714fd75f..0000000000000000000000000000000000000000 --- a/dapps/valuetransfers/packages/ledgerstate/ledgerstate.go +++ /dev/null @@ -1,18 +0,0 @@ -package ledgerstate - -import ( - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/utxodag" -) - -// LedgerState represents a struct, that allows us to read the balances from the UTXODAG by filtering the existing -// unspent Outputs depending on the liked branches. -type LedgerState struct { - utxoDAG *utxodag.UTXODAG -} - -// New is the constructor of the LedgerState. It creates a new instance with the given UTXODAG. -func New(utxoDAG *utxodag.UTXODAG) *LedgerState { - return &LedgerState{ - utxoDAG: utxoDAG, - } -} diff --git a/dapps/valuetransfers/packages/utxodag/attachment.go b/dapps/valuetransfers/packages/tangle/attachment.go similarity index 99% rename from dapps/valuetransfers/packages/utxodag/attachment.go rename to dapps/valuetransfers/packages/tangle/attachment.go index 73603af593eb4a5df1e74d4c89cae852256e8564..051deee1dae0f6472cda506dce40797d70696e02 100644 --- a/dapps/valuetransfers/packages/utxodag/attachment.go +++ b/dapps/valuetransfers/packages/tangle/attachment.go @@ -1,4 +1,4 @@ -package utxodag +package tangle import ( "github.com/iotaledger/hive.go/marshalutil" diff --git a/dapps/valuetransfers/packages/utxodag/consumer.go b/dapps/valuetransfers/packages/tangle/consumer.go similarity index 99% rename from dapps/valuetransfers/packages/utxodag/consumer.go rename to dapps/valuetransfers/packages/tangle/consumer.go index 212f6523cdebb2aa0b9f41a73429c754eaad0591..201a89a784f438db96ce1ecb74c893614de694b2 100644 --- a/dapps/valuetransfers/packages/utxodag/consumer.go +++ b/dapps/valuetransfers/packages/tangle/consumer.go @@ -1,4 +1,4 @@ -package utxodag +package tangle import ( "github.com/iotaledger/hive.go/marshalutil" diff --git a/dapps/valuetransfers/packages/tangle/events.go b/dapps/valuetransfers/packages/tangle/events.go index a807d72805277840c7e428026d2434c985a67b8f..5eea10a5e6ef33d2c17732f19ed5e03afe812eb8 100644 --- a/dapps/valuetransfers/packages/tangle/events.go +++ b/dapps/valuetransfers/packages/tangle/events.go @@ -3,7 +3,9 @@ package tangle import ( "github.com/iotaledger/hive.go/events" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" ) // Events is a container for the different kind of events of the Tangle. @@ -14,6 +16,18 @@ type Events struct { MissingPayloadReceived *events.Event PayloadMissing *events.Event PayloadUnsolidifiable *events.Event + + // TransactionReceived gets triggered whenever a transaction was received for the first time (not solid yet). + TransactionReceived *events.Event + + // TransactionBooked gets triggered whenever a transactions becomes solid and gets booked into a particular branch. + TransactionBooked *events.Event + + // Fork gets triggered when a previously un-conflicting transaction get's some of its inputs double spend, so that a + // new Branch is created. + Fork *events.Event + + Error *events.Event } func newEvents() *Events { @@ -23,6 +37,10 @@ func newEvents() *Events { MissingPayloadReceived: events.NewEvent(cachedPayloadEvent), PayloadMissing: events.NewEvent(payloadIDEvent), PayloadUnsolidifiable: events.NewEvent(payloadIDEvent), + TransactionReceived: events.NewEvent(cachedTransactionEvent), + TransactionBooked: events.NewEvent(transactionBookedEvent), + Fork: events.NewEvent(forkEvent), + Error: events.NewEvent(events.ErrorCaller), } } @@ -36,3 +54,30 @@ func cachedPayloadEvent(handler interface{}, params ...interface{}) { params[1].(*CachedPayloadMetadata).Retain(), ) } + +func transactionBookedEvent(handler interface{}, params ...interface{}) { + handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *branchmanager.CachedBranch, []transaction.OutputID, bool))( + params[0].(*transaction.CachedTransaction).Retain(), + params[1].(*CachedTransactionMetadata).Retain(), + params[2].(*branchmanager.CachedBranch).Retain(), + params[3].([]transaction.OutputID), + params[4].(bool), + ) +} + +func forkEvent(handler interface{}, params ...interface{}) { + handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *branchmanager.CachedBranch, []transaction.OutputID))( + params[0].(*transaction.CachedTransaction).Retain(), + params[1].(*CachedTransactionMetadata).Retain(), + params[2].(*branchmanager.CachedBranch).Retain(), + params[3].([]transaction.OutputID), + ) +} + +func cachedTransactionEvent(handler interface{}, params ...interface{}) { + handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *CachedAttachment))( + params[0].(*transaction.CachedTransaction).Retain(), + params[1].(*CachedTransactionMetadata).Retain(), + params[2].(*CachedAttachment).Retain(), + ) +} diff --git a/dapps/valuetransfers/packages/tangle/ledgerstate.go b/dapps/valuetransfers/packages/tangle/ledgerstate.go new file mode 100644 index 0000000000000000000000000000000000000000..5c0d8c31a7b7bd0c93dcb01aaa29c35719806c22 --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/ledgerstate.go @@ -0,0 +1,34 @@ +package tangle + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" +) + +// LedgerState represents a struct, that allows us to read the balances from the UTXODAG by filtering the existing +// unspent Outputs depending on the liked branches. +type LedgerState struct { + tangle *Tangle +} + +// NewLedgerState is the constructor of the LedgerState. It creates a new instance with the given UTXODAG. +func NewLedgerState(tangle *Tangle) *LedgerState { + return &LedgerState{ + tangle: tangle, + } +} + +// Balances returns a map containing the balances of the different colors that are unspent on a certain address. +func (ledgerState *LedgerState) Balances(address address.Address) (coloredBalances map[balance.Color]int64) { + coloredBalances = make(map[balance.Color]int64) + + ledgerState.tangle.OutputsOnAddress(address).Consume(func(output *Output) { + if output.ConsumerCount() == 0 { + for _, coloredBalance := range output.Balances() { + coloredBalances[coloredBalance.Color()] += coloredBalance.Value() + } + } + }) + + return +} diff --git a/dapps/valuetransfers/packages/utxodag/missingoutput.go b/dapps/valuetransfers/packages/tangle/missingoutput.go similarity index 99% rename from dapps/valuetransfers/packages/utxodag/missingoutput.go rename to dapps/valuetransfers/packages/tangle/missingoutput.go index 77b466241f1dde497af3b3e803042d986e946a64..d04a3e8f600fc75f84f4a1868b563c1e91c5fe04 100644 --- a/dapps/valuetransfers/packages/utxodag/missingoutput.go +++ b/dapps/valuetransfers/packages/tangle/missingoutput.go @@ -1,4 +1,4 @@ -package utxodag +package tangle import ( "time" diff --git a/dapps/valuetransfers/packages/tangle/objectstorage.go b/dapps/valuetransfers/packages/tangle/objectstorage.go index 4052ae56ac8e1035718e893096eca3cc3b53c4ff..a549f1a0da9e434e28b8cb7fcf21adffb1c5b999 100644 --- a/dapps/valuetransfers/packages/tangle/objectstorage.go +++ b/dapps/valuetransfers/packages/tangle/objectstorage.go @@ -1,9 +1,12 @@ package tangle import ( + "time" + "github.com/iotaledger/hive.go/objectstorage" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" ) const ( @@ -15,6 +18,18 @@ const ( osPayloadMetadata osMissingPayload osApprover + osTransaction + osTransactionMetadata + osAttachment + osOutput + osConsumer +) + +var ( + osLeakDetectionOption = objectstorage.LeakDetectionEnabled(true, objectstorage.LeakDetectionOptions{ + MaxConsumersPerObject: 10, + MaxConsumerHoldTime: 10 * time.Second, + }) ) func osPayloadFactory(key []byte) (objectstorage.StorableObject, int, error) { @@ -32,3 +47,23 @@ func osMissingPayloadFactory(key []byte) (objectstorage.StorableObject, int, err func osPayloadApproverFactory(key []byte) (objectstorage.StorableObject, int, error) { return PayloadApproverFromStorageKey(key) } + +func osTransactionFactory(key []byte) (objectstorage.StorableObject, int, error) { + return transaction.FromStorageKey(key) +} + +func osTransactionMetadataFactory(key []byte) (objectstorage.StorableObject, int, error) { + return TransactionMetadataFromStorageKey(key) +} + +func osAttachmentFactory(key []byte) (objectstorage.StorableObject, int, error) { + return AttachmentFromStorageKey(key) +} + +func osOutputFactory(key []byte) (objectstorage.StorableObject, int, error) { + return OutputFromStorageKey(key) +} + +func osConsumerFactory(key []byte) (objectstorage.StorableObject, int, error) { + return ConsumerFromStorageKey(key) +} diff --git a/dapps/valuetransfers/packages/utxodag/output.go b/dapps/valuetransfers/packages/tangle/output.go similarity index 93% rename from dapps/valuetransfers/packages/utxodag/output.go rename to dapps/valuetransfers/packages/tangle/output.go index 6831c80da31a1faa0b42de5d6251ac741e4320d9..54fe21883bfe1492b398a33193728d5f3600fdc6 100644 --- a/dapps/valuetransfers/packages/utxodag/output.go +++ b/dapps/valuetransfers/packages/tangle/output.go @@ -1,4 +1,4 @@ -package utxodag +package tangle import ( "sync" @@ -212,12 +212,21 @@ func (output *Output) RegisterConsumer(consumer transaction.ID) (consumerCount i output.firstConsumer = consumer } output.consumerCount++ + output.SetModified() firstConsumerID = output.firstConsumer return } +// ConsumerCount returns the number of transactions that have spent this Output. +func (output *Output) ConsumerCount() int { + output.consumerMutex.RLock() + defer output.consumerMutex.RUnlock() + + return output.consumerCount +} + // Balances returns the colored balances (color + balance) that this output contains. func (output *Output) Balances() []*balance.Balance { return output.balances @@ -247,10 +256,12 @@ func (output *Output) ObjectStorageValue() []byte { balanceCount := len(output.balances) // initialize helper - marshalUtil := marshalutil.New(branchmanager.BranchIDLength + marshalutil.BOOL_SIZE + marshalutil.TIME_SIZE + marshalutil.UINT32_SIZE + balanceCount*balance.Length) + marshalUtil := marshalutil.New(branchmanager.BranchIDLength + marshalutil.BOOL_SIZE + marshalutil.TIME_SIZE + transaction.IDLength + marshalutil.UINT32_SIZE + marshalutil.UINT32_SIZE + balanceCount*balance.Length) marshalUtil.WriteBytes(output.branchID.Bytes()) marshalUtil.WriteBool(output.solid) marshalUtil.WriteTime(output.solidificationTime) + marshalUtil.WriteBytes(output.firstConsumer.Bytes()) + marshalUtil.WriteUint32(uint32(output.consumerCount)) marshalUtil.WriteUint32(uint32(balanceCount)) for _, balanceToMarshal := range output.balances { marshalUtil.WriteBytes(balanceToMarshal.Bytes()) @@ -272,8 +283,16 @@ func (output *Output) UnmarshalObjectStorageValue(data []byte) (consumedBytes in if output.solidificationTime, err = marshalUtil.ReadTime(); err != nil { return } - var balanceCount uint32 - if balanceCount, err = marshalUtil.ReadUint32(); err != nil { + if output.firstConsumer, err = transaction.ParseID(marshalUtil); err != nil { + return + } + consumerCount, err := marshalUtil.ReadUint32() + if err != nil { + return + } + output.consumerCount = int(consumerCount) + balanceCount, err := marshalUtil.ReadUint32() + if err != nil { return } output.balances = make([]*balance.Balance, balanceCount) diff --git a/dapps/valuetransfers/packages/tangle/payloadmetadata.go b/dapps/valuetransfers/packages/tangle/payloadmetadata.go index 3fd3d20784628b354bfeb5da23c413e1514dab0f..86d06fd53e6bd3899affced4fe31f54acd18c50b 100644 --- a/dapps/valuetransfers/packages/tangle/payloadmetadata.go +++ b/dapps/valuetransfers/packages/tangle/payloadmetadata.go @@ -8,6 +8,7 @@ import ( "github.com/iotaledger/hive.go/objectstorage" "github.com/iotaledger/hive.go/stringify" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" ) @@ -19,9 +20,11 @@ type PayloadMetadata struct { payloadID payload.ID solid bool solidificationTime time.Time + branchID branchmanager.BranchID solidMutex sync.RWMutex solidificationTimeMutex sync.RWMutex + branchIDMutex sync.RWMutex } // NewPayloadMetadata creates an empty container for the metadata of a value transfer payload. @@ -136,9 +139,41 @@ func (payloadMetadata *PayloadMetadata) SoldificationTime() time.Time { return payloadMetadata.solidificationTime } +// BranchID returns the identifier of the Branch that this Payload was booked into. +func (payloadMetadata *PayloadMetadata) BranchID() branchmanager.BranchID { + payloadMetadata.branchIDMutex.RLock() + defer payloadMetadata.branchIDMutex.RUnlock() + + return payloadMetadata.branchID +} + +// SetBranchID is the setter for the BranchID that the corresponding Payload is booked into. +func (payloadMetadata *PayloadMetadata) SetBranchID(branchID branchmanager.BranchID) (modified bool) { + payloadMetadata.branchIDMutex.RLock() + if branchID == payloadMetadata.branchID { + payloadMetadata.branchIDMutex.RUnlock() + + return + } + + payloadMetadata.branchIDMutex.RUnlock() + payloadMetadata.branchIDMutex.Lock() + defer payloadMetadata.branchIDMutex.Unlock() + + if branchID == payloadMetadata.branchID { + return + } + + payloadMetadata.branchID = branchID + payloadMetadata.SetModified() + modified = true + + return +} + // Bytes marshals the metadata into a sequence of bytes. func (payloadMetadata *PayloadMetadata) Bytes() []byte { - return marshalutil.New(payload.IDLength + marshalutil.TIME_SIZE + marshalutil.BOOL_SIZE). + return marshalutil.New(payload.IDLength + marshalutil.TIME_SIZE + marshalutil.BOOL_SIZE + branchmanager.BranchIDLength). WriteBytes(payloadMetadata.ObjectStorageKey()). WriteBytes(payloadMetadata.ObjectStorageValue()). Bytes() @@ -170,6 +205,7 @@ func (payloadMetadata *PayloadMetadata) ObjectStorageValue() []byte { return marshalutil.New(marshalutil.TIME_SIZE + marshalutil.BOOL_SIZE). WriteTime(payloadMetadata.solidificationTime). WriteBool(payloadMetadata.solid). + WriteBytes(payloadMetadata.branchID.Bytes()). Bytes() } @@ -182,6 +218,9 @@ func (payloadMetadata *PayloadMetadata) UnmarshalObjectStorageValue(data []byte) if payloadMetadata.solid, err = marshalUtil.ReadBool(); err != nil { return } + if payloadMetadata.branchID, err = branchmanager.ParseBranchID(marshalUtil); err != nil { + return + } consumedBytes = marshalUtil.ReadOffset() return diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go index 6332933bff2a6a1bd71749fe53972edc0f4484cf..227944875678d75b4dc8281cdea343c2004cae14 100644 --- a/dapps/valuetransfers/packages/tangle/tangle.go +++ b/dapps/valuetransfers/packages/tangle/tangle.go @@ -2,29 +2,43 @@ package tangle import ( "container/list" + "errors" + "fmt" + "math" "time" "github.com/dgraph-io/badger/v2" "github.com/iotaledger/hive.go/async" "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 { - payloadStorage *objectstorage.ObjectStorage - payloadMetadataStorage *objectstorage.ObjectStorage - approverStorage *objectstorage.ObjectStorage - missingPayloadStorage *objectstorage.ObjectStorage + 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 - storePayloadWorkerPool async.WorkerPool - solidifierWorkerPool async.WorkerPool - cleanupWorkerPool async.WorkerPool + workerPool async.WorkerPool + cleanupWorkerPool async.WorkerPool } // New is the constructor of a Tangle and creates a new Tangle object from the given details. @@ -32,10 +46,17 @@ func New(badgerInstance *badger.DB) (result *Tangle) { osFactory := objectstorage.NewFactory(badgerInstance, storageprefix.ValueTransfers) result = &Tangle{ - payloadStorage: osFactory.New(osPayload, osPayloadFactory, objectstorage.CacheTime(time.Second)), - payloadMetadataStorage: osFactory.New(osPayloadMetadata, osPayloadMetadataFactory, objectstorage.CacheTime(time.Second)), - missingPayloadStorage: osFactory.New(osMissingPayload, osMissingPayloadFactory, objectstorage.CacheTime(time.Second)), - approverStorage: osFactory.New(osApprover, osPayloadApproverFactory, objectstorage.CacheTime(time.Second), objectstorage.PartitionKey(payload.IDLength, payload.IDLength), objectstorage.KeysOnly(true)), + branchManager: branchmanager.New(badgerInstance), + + payloadStorage: osFactory.New(osPayload, osPayloadFactory, objectstorage.CacheTime(time.Second)), + payloadMetadataStorage: osFactory.New(osPayloadMetadata, osPayloadMetadataFactory, objectstorage.CacheTime(time.Second)), + missingPayloadStorage: osFactory.New(osMissingPayload, osMissingPayloadFactory, objectstorage.CacheTime(time.Second)), + approverStorage: osFactory.New(osApprover, osPayloadApproverFactory, objectstorage.CacheTime(time.Second), objectstorage.PartitionKey(payload.IDLength, payload.IDLength), objectstorage.KeysOnly(true)), + transactionStorage: osFactory.New(osTransaction, osTransactionFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), + transactionMetadataStorage: osFactory.New(osTransactionMetadata, osTransactionMetadataFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), + attachmentStorage: osFactory.New(osAttachment, osAttachmentFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), + outputStorage: osFactory.New(osOutput, osOutputFactory, OutputKeyPartitions, objectstorage.CacheTime(time.Second), osLeakDetectionOption), + consumerStorage: osFactory.New(osConsumer, osConsumerFactory, ConsumerPartitionKeys, objectstorage.CacheTime(time.Second), osLeakDetectionOption), Events: *newEvents(), } @@ -43,13 +64,57 @@ func New(badgerInstance *badger.DB) (result *Tangle) { return } +// BranchManager is the getter for the manager that takes care of creating and updating branches. +func (tangle *Tangle) BranchManager() *branchmanager.BranchManager { + return tangle.branchManager +} + +// 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())} +} + +// 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 +} + // AttachPayload adds a new payload to the value tangle. func (tangle *Tangle) AttachPayload(payload *payload.Payload) { - tangle.storePayloadWorkerPool.Submit(func() { tangle.storePayloadWorker(payload) }) + tangle.workerPool.Submit(func() { tangle.storePayloadWorker(payload) }) } -// GetPayload retrieves a payload from the object storage. -func (tangle *Tangle) GetPayload(payloadID payload.ID) *payload.CachedPayload { +// 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())} } @@ -58,8 +123,8 @@ func (tangle *Tangle) PayloadMetadata(payloadID payload.ID) *CachedPayloadMetada return &CachedPayloadMetadata{CachedObject: tangle.payloadMetadataStorage.Load(payloadID.Bytes())} } -// GetApprovers retrieves the approvers of a payload from the object storage. -func (tangle *Tangle) GetApprovers(payloadID payload.ID) CachedApprovers { +// 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}) @@ -72,42 +137,69 @@ func (tangle *Tangle) GetApprovers(payloadID payload.ID) CachedApprovers { // Shutdown stops the worker pools and shuts down the object storage instances. func (tangle *Tangle) Shutdown() *Tangle { - tangle.storePayloadWorkerPool.ShutdownGracefully() - tangle.solidifierWorkerPool.ShutdownGracefully() + tangle.workerPool.ShutdownGracefully() tangle.cleanupWorkerPool.ShutdownGracefully() - tangle.payloadStorage.Shutdown() - tangle.payloadMetadataStorage.Shutdown() - tangle.approverStorage.Shutdown() - tangle.missingPayloadStorage.Shutdown() + 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 } // Prune resets the database and deletes all objects (for testing or "node resets"). -func (tangle *Tangle) Prune() error { +func (tangle *Tangle) Prune() (err error) { + if err = tangle.branchManager.Prune(); err != nil { + return + } + for _, storage := range []*objectstorage.ObjectStorage{ tangle.payloadStorage, tangle.payloadMetadataStorage, - tangle.approverStorage, tangle.missingPayloadStorage, + tangle.approverStorage, + tangle.transactionStorage, + tangle.transactionMetadataStorage, + tangle.attachmentStorage, + tangle.outputStorage, + tangle.consumerStorage, } { - if err := storage.Prune(); err != nil { - return err + if err = storage.Prune(); err != nil { + return } } - return nil + return } // storePayloadWorker is the worker function that stores the payload and calls the corresponding storage events. func (tangle *Tangle) storePayloadWorker(payloadToStore *payload.Payload) { - // store the payload and transaction models + // store the payload models or abort if we have seen the payload already cachedPayload, cachedPayloadMetadata, payloadStored := tangle.storePayload(payloadToStore) if !payloadStored { - // abort if we have seen the payload already 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). @@ -118,11 +210,12 @@ func (tangle *Tangle) storePayloadWorker(payloadToStore *payload.Payload) { tangle.Events.MissingPayloadReceived.Trigger(cachedPayload, cachedPayloadMetadata) } tangle.Events.PayloadAttached.Trigger(cachedPayload, cachedPayloadMetadata) + if transactionIsNew { + tangle.Events.TransactionReceived.Trigger(cachedTransaction, cachedTransactionMetadata, cachedAttachment) + } // check solidity - tangle.solidifierWorkerPool.Submit(func() { - tangle.solidifyPayloadWorker(cachedPayload, cachedPayloadMetadata) - }) + tangle.solidifyPayload(cachedPayload.Retain(), cachedPayloadMetadata.Retain(), cachedTransaction.Retain(), cachedTransactionMetadata.Retain()) } func (tangle *Tangle) storePayload(payloadToStore *payload.Payload) (cachedPayload *payload.CachedPayload, cachedMetadata *CachedPayloadMetadata, payloadStored bool) { @@ -138,6 +231,40 @@ func (tangle *Tangle) storePayload(payloadToStore *payload.Payload) (cachedPaylo 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 trunk approver trunkID := payload.TrunkID() @@ -149,90 +276,310 @@ func (tangle *Tangle) storePayloadReferences(payload *payload.Payload) { } } -func (tangle *Tangle) popElementsFromSolidificationStack(stack *list.List) (*payload.CachedPayload, *CachedPayloadMetadata) { +func (tangle *Tangle) popElementsFromSolidificationStack(stack *list.List) (*payload.CachedPayload, *CachedPayloadMetadata, *transaction.CachedTransaction, *CachedTransactionMetadata) { currentSolidificationEntry := stack.Front() - currentCachedPayload := currentSolidificationEntry.Value.([2]interface{})[0] - currentCachedMetadata := currentSolidificationEntry.Value.([2]interface{})[1] + currentCachedPayload := currentSolidificationEntry.Value.([4]interface{})[0].(*payload.CachedPayload) + currentCachedMetadata := currentSolidificationEntry.Value.([4]interface{})[1].(*CachedPayloadMetadata) + currentCachedTransaction := currentSolidificationEntry.Value.([4]interface{})[2].(*transaction.CachedTransaction) + currentCachedTransactionMetadata := currentSolidificationEntry.Value.([4]interface{})[3].(*CachedTransactionMetadata) stack.Remove(currentSolidificationEntry) - return currentCachedPayload.(*payload.CachedPayload), currentCachedMetadata.(*CachedPayloadMetadata) + return currentCachedPayload, currentCachedMetadata, currentCachedTransaction, currentCachedTransactionMetadata } -// solidifyPayloadWorker is the worker function that solidifies the payloads (recursively from past to present). -func (tangle *Tangle) solidifyPayloadWorker(cachedPayload *payload.CachedPayload, cachedMetadata *CachedPayloadMetadata) { +// 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([2]interface{}{cachedPayload, cachedMetadata}) + solidificationStack.PushBack([4]interface{}{cachedPayload, cachedMetadata, cachedTransaction, cachedTransactionMetadata}) // process payloads that are supposed to be checked for solidity recursively for solidificationStack.Len() > 0 { - // execute logic inside a func, so we can use defer to release the objects - func() { - // retrieve cached objects - currentCachedPayload, currentCachedMetadata := tangle.popElementsFromSolidificationStack(solidificationStack) - defer currentCachedPayload.Release() - defer currentCachedMetadata.Release() - - // unwrap cached objects - currentPayload := currentCachedPayload.Unwrap() - currentPayloadMetadata := currentCachedMetadata.Unwrap() - - // abort if any of the retrieved models is nil or payload is not solid or it was set as solid already - if currentPayload == nil || currentPayloadMetadata == nil || !tangle.isPayloadSolid(currentPayload, currentPayloadMetadata) || !currentPayloadMetadata.SetSolid(true) { - return + // retrieve cached objects + currentCachedPayload, currentCachedMetadata, currentCachedTransaction, currentCachedTransactionMetadata := tangle.popElementsFromSolidificationStack(solidificationStack) + + // unwrap cached objects + currentPayload := currentCachedPayload.Unwrap() + currentPayloadMetadata := currentCachedMetadata.Unwrap() + currentTransaction := currentCachedTransaction.Unwrap() + currentTransactionMetadata := currentCachedTransactionMetadata.Unwrap() + + // abort if any of the retrieved models are nil + if currentPayload == nil || currentPayloadMetadata == nil || currentTransaction == nil || currentTransactionMetadata == nil { + currentCachedPayload.Release() + currentCachedMetadata.Release() + currentCachedTransaction.Release() + currentCachedTransactionMetadata.Release() + + return + } + + // abort if the transaction is not solid or invalid + transactionSolid, consumedBranches, err := tangle.checkTransactionSolidity(currentTransaction, currentTransactionMetadata) + if err != nil || !transactionSolid { + if err != nil { + // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT + fmt.Println(err, currentTransaction) } - // ... trigger solid event ... - tangle.Events.PayloadSolid.Trigger(currentCachedPayload, currentCachedMetadata) + currentCachedPayload.Release() + currentCachedMetadata.Release() + currentCachedTransaction.Release() + currentCachedTransactionMetadata.Release() + + return + } + + // abort if the payload is not solid or invalid + payloadSolid, err := tangle.checkPayloadSolidity(currentPayload, currentPayloadMetadata, consumedBranches) + if err != nil || !payloadSolid { + if err != nil { + // TODO: TRIGGER INVALID TX + REMOVE TXS + PAYLOADS THAT APPROVE IT + fmt.Println(err, currentTransaction) + } + + currentCachedPayload.Release() + currentCachedMetadata.Release() + currentCachedTransaction.Release() + currentCachedTransactionMetadata.Release() + + return + } + + // book the solid entities + transactionBooked, payloadBooked, bookingErr := tangle.book(currentCachedPayload.Retain(), currentCachedMetadata.Retain(), currentCachedTransaction.Retain(), currentCachedTransactionMetadata.Retain()) + if bookingErr != nil { + tangle.Events.Error.Trigger(bookingErr) + + currentCachedPayload.Release() + currentCachedMetadata.Release() + currentCachedTransaction.Release() + currentCachedTransactionMetadata.Release() + return + } + + if transactionBooked { + tangle.ForEachConsumers(currentTransaction, func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment) { + solidificationStack.PushBack([3]interface{}{cachedTransaction, transactionMetadata, cachedAttachment}) + }) + } + + if payloadBooked { // ... and schedule check of approvers - tangle.ForeachApprovers(currentPayload.ID(), func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata) { - solidificationStack.PushBack([2]interface{}{payload, payloadMetadata}) + tangle.ForeachApprovers(currentPayload.ID(), func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata) { + solidificationStack.PushBack([4]interface{}{payload, payloadMetadata, transaction, transactionMetadata}) }) - }() + } + + currentCachedPayload.Release() + currentCachedMetadata.Release() + currentCachedTransaction.Release() + currentCachedTransactionMetadata.Release() } } -// 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)) { - tangle.GetApprovers(payloadID).Consume(func(approver *PayloadApprover) { - approvingPayloadID := approver.ApprovingPayloadID() - approvingCachedPayload := tangle.GetPayload(approvingPayloadID) +func (tangle *Tangle) book(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *CachedPayloadMetadata, cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (transactionBooked bool, payloadBooked bool, err error) { + defer cachedPayload.Release() + defer cachedPayloadMetadata.Release() + defer cachedTransaction.Release() + defer cachedTransactionMetadata.Release() - approvingCachedPayload.Consume(func(payload *payload.Payload) { - consume(approvingCachedPayload, tangle.PayloadMetadata(approvingPayloadID)) - }) - }) + if transactionBooked, err = tangle.bookTransaction(cachedTransaction.Retain(), cachedTransactionMetadata.Retain()); err != nil { + return + } + + if payloadBooked, err = tangle.bookPayload(cachedPayload.Retain(), cachedPayloadMetadata.Retain(), cachedTransactionMetadata.Retain()); err != nil { + return + } + + return } -// isPayloadSolid returns true if the given payload is solid. A payload is considered to be solid solid, if it is either -// already marked as solid or if its referenced payloads are marked as solid. -func (tangle *Tangle) isPayloadSolid(payload *payload.Payload, metadata *PayloadMetadata) bool { - if payload == nil || payload.IsDeleted() { - return false +func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (transactionBooked 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 } - if metadata == nil || metadata.IsDeleted() { - return false + transactionMetadata := cachedTransactionMetadata.Unwrap() + if transactionMetadata == nil { + // TODO: explicit error var + err = errors.New("failed to unwrap transaction metadata") + + return + } + + // abort if this transaction was booked by another process already + if !transactionMetadata.SetSolid(true) { + return + } + + consumedBranches := make(branchmanager.BranchIds) + conflictingInputs := make([]transaction.OutputID, 0) + conflictingInputsOfFirstConsumers := make(map[transaction.ID][]transaction.OutputID) + + 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'", outputID) + + 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) + } + + return true + }) { + return } - if metadata.IsSolid() { + 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()) + + // book outputs into the target branch + transactionToBook.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool { + newOutput := NewOutput(address, transactionToBook.ID(), targetBranch.ID(), balances) + newOutput.SetSolid(true) + tangle.outputStorage.Store(newOutput).Release() + return true + }) + + // fork the conflicting transactions into their own branch + decisionPending := false + for consumerID, conflictingInputs := range conflictingInputsOfFirstConsumers { + _, decisionFinalized, forkedErr := tangle.Fork(consumerID, conflictingInputs) + if forkedErr != nil { + err = forkedErr + + return + } + + decisionPending = decisionPending || !decisionFinalized + } + + // trigger events + tangle.Events.TransactionBooked.Trigger(cachedTransaction, cachedTransactionMetadata, cachedTargetBranch, conflictingInputs, decisionPending) + + 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 + } + + branchBranchID := tangle.payloadBranchID(valueObject.BranchID()) + trunkBranchID := tangle.payloadBranchID(valueObject.TrunkID()) + transactionBranchID := transactionMetadata.BranchID() + + if branchBranchID == branchmanager.UndefinedBranchID || trunkBranchID == branchmanager.UndefinedBranchID || transactionBranchID == branchmanager.UndefinedBranchID { + return + } + + cachedAggregatedBranch, err := tangle.BranchManager().AggregateBranches([]branchmanager.BranchID{branchBranchID, trunkBranchID, transactionBranchID}...) + if err != nil { + return } + defer cachedAggregatedBranch.Release() + + aggregatedBranch := cachedAggregatedBranch.Unwrap() + if aggregatedBranch == nil { + return + } + + payloadBooked = valueObjectMetadata.SetBranchID(aggregatedBranch.ID()) + + return +} - return tangle.isPayloadMarkedAsSolid(payload.TrunkID()) && tangle.isPayloadMarkedAsSolid(payload.BranchID()) +// ForeachApprovers iterates through the approvers of a payload and calls the passed in consumer function. +func (tangle *Tangle) ForeachApprovers(payloadID payload.ID, consume func(payload *payload.CachedPayload, payloadMetadata *CachedPayloadMetadata, transaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata)) { + tangle.Approvers(payloadID).Consume(func(approver *PayloadApprover) { + approvingPayloadID := approver.ApprovingPayloadID() + approvingCachedPayload := tangle.Payload(approvingPayloadID) + + approvingCachedPayload.Consume(func(payload *payload.Payload) { + consume(approvingCachedPayload, tangle.PayloadMetadata(approvingPayloadID), tangle.Transaction(payload.Transaction().ID()), tangle.TransactionMetadata(payload.Transaction().ID())) + }) + }) } -// isPayloadMarkedAsSolid returns true if the payload was marked as solid already (by setting the corresponding flags -// in its metadata. -func (tangle *Tangle) isPayloadMarkedAsSolid(payloadID payload.ID) bool { +// 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 true + return branchmanager.MasterBranchID } - transactionMetadataCached := tangle.PayloadMetadata(payloadID) - if transactionMetadata := transactionMetadataCached.Unwrap(); transactionMetadata == nil { - transactionMetadataCached.Release() + cachedPayloadMetadata := tangle.PayloadMetadata(payloadID) + defer cachedPayloadMetadata.Release() + + payloadMetadata := cachedPayloadMetadata.Unwrap() + if payloadMetadata == nil { + cachedPayloadMetadata.Release() // if transaction is missing and was not reported as missing, yet if cachedMissingPayload, missingPayloadStored := tangle.missingPayloadStorage.StoreIfAbsent(NewMissingPayload(payloadID)); missingPayloadStored { @@ -241,13 +588,497 @@ func (tangle *Tangle) isPayloadMarkedAsSolid(payloadID payload.ID) bool { }) } - return false - } else if !transactionMetadata.IsSolid() { - transactionMetadataCached.Release() + return branchmanager.UndefinedBranchID + } + + // the BranchID is only set if the payload was also marked as solid + return payloadMetadata.BranchID() +} + +// checkPayloadSolidity returns true if the given payload is solid. A payload is considered to be solid solid, if it is either +// already marked as solid or if its referenced payloads are marked as solid. +func (tangle *Tangle) checkPayloadSolidity(payload *payload.Payload, payloadMetadata *PayloadMetadata, transactionBranches []branchmanager.BranchID) (solid bool, err error) { + if payload == nil || payload.IsDeleted() || payloadMetadata == nil || payloadMetadata.IsDeleted() { + return + } + + if solid = payloadMetadata.IsSolid(); solid { + return + } + + combinedBranches := transactionBranches + + trunkBranchID := tangle.payloadBranchID(payload.TrunkID()) + if trunkBranchID == branchmanager.UndefinedBranchID { + return + } + combinedBranches = append(combinedBranches, trunkBranchID) + + branchBranchID := tangle.payloadBranchID(payload.BranchID()) + if branchBranchID == branchmanager.UndefinedBranchID { + return + } + combinedBranches = append(combinedBranches, branchBranchID) + + 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", payload.ID()) + + return + } + + 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 solid = metadata.Solid(); 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'", tx.ID()) + + 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", tx.ID()) + + return + } + + // set the result to be solid and valid + solid = true + + return +} + +// 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) + + // 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() + } + } +} + +// 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 +} + +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 +} + +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") + + 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 a newly colored output (we check the supply later) + 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 } - transactionMetadataCached.Release() - return true + // 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 +} + +// 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'", transactionID) + + return + } + txMetadata := cachedTransactionMetadata.Unwrap() + if txMetadata == nil { + err = fmt.Errorf("failed to load metadata of transaction '%s'", transactionID) + + return + } + + // abort if this transaction was finalized already + if txMetadata.Finalized() { + finalized = true + + return + } + + // update / create new branch + cachedTargetBranch, newBranchCreated := tangle.branchManager.Fork(branchmanager.NewBranchID(tx.ID()), []branchmanager.BranchID{txMetadata.BranchID()}, conflictingInputs) + defer cachedTargetBranch.Release() + + // abort if the branch existed already + if !newBranchCreated { + return + } + + // unpack branch + targetBranch := cachedTargetBranch.Unwrap() + if targetBranch == nil { + err = fmt.Errorf("failed to unpack branch for transaction '%s'", transactionID) + + 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(cachedTransaction, cachedTransactionMetadata, targetBranch, conflictingInputs) + forked = true + + return +} + +// 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 + } + + // 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'", outputID) + + 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 +} + +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'", outputId) + + return false + } + + consumedBranches[transactionOutput.BranchID()] = types.Void + + return true + }) { + return + } + + branch, err = tangle.branchManager.AggregateBranches(consumedBranches.ToList()...) + + return +} + +// ForEachConsumers iterates through the transactions that are consuming outputs of the given transactions +func (tangle *Tangle) ForEachConsumers(currentTransaction *transaction.Transaction, consume func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment)) { + 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()) + cachedTransactionMetadata := tangle.TransactionMetadata(consumer.TransactionID()) + for _, cachedAttachment := range tangle.Attachments(consumer.TransactionID()) { + consume(cachedTransaction, cachedTransactionMetadata, cachedAttachment) + } + } + }) + + return true + }) } diff --git a/dapps/valuetransfers/packages/utxodag/utxodag_test.go b/dapps/valuetransfers/packages/tangle/tangle_test.go similarity index 50% rename from dapps/valuetransfers/packages/utxodag/utxodag_test.go rename to dapps/valuetransfers/packages/tangle/tangle_test.go index 4db41241e32c185c4ea0fe3ba7af431c2ea7b95c..f335593ca963d286f60897d215c4a1c45edcee18 100644 --- a/dapps/valuetransfers/packages/utxodag/utxodag_test.go +++ b/dapps/valuetransfers/packages/tangle/tangle_test.go @@ -1,23 +1,16 @@ -package utxodag +package tangle import ( - "io/ioutil" - "os" "testing" "time" - "github.com/iotaledger/hive.go/crypto/ed25519" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "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/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/packages/database" - "github.com/iotaledger/goshimmer/plugins/config" ) func TestNewOutput(t *testing.T) { @@ -71,72 +64,3 @@ func TestAttachment(t *testing.T) { assert.Equal(t, transactionID, clonedAttachment.TransactionID()) assert.Equal(t, payloadID, clonedAttachment.PayloadID()) } - -func TestTangle_AttachPayload(t *testing.T) { - dir, err := ioutil.TempDir("", t.Name()) - require.NoError(t, err) - defer os.Remove(dir) - - config.Node.Set(database.CFG_DIRECTORY, dir) - - valueTangle := tangle.New(database.GetBadgerInstance()) - if err := valueTangle.Prune(); err != nil { - t.Error(err) - - return - } - - utxoDAG := New(database.GetBadgerInstance(), valueTangle) - - addressKeyPair1 := ed25519.GenerateKeyPair() - addressKeyPair2 := ed25519.GenerateKeyPair() - - transferID1, _ := transaction.IDFromBase58("8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh") - transferID2, _ := transaction.IDFromBase58("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM") - - input1 := NewOutput(address.FromED25519PubKey(addressKeyPair1.PublicKey), transferID1, branchmanager.MasterBranchID, []*balance.Balance{ - balance.New(balance.ColorIOTA, 337), - }) - input1.SetSolid(true) - input2 := NewOutput(address.FromED25519PubKey(addressKeyPair2.PublicKey), transferID2, branchmanager.MasterBranchID, []*balance.Balance{ - balance.New(balance.ColorIOTA, 1000), - }) - input2.SetSolid(true) - - utxoDAG.outputStorage.Store(input1).Release() - utxoDAG.outputStorage.Store(input2).Release() - - outputAddress1 := address.Random() - outputAddress2 := address.Random() - - // attach first spend - valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( - transaction.NewInputs( - input1.ID(), - input2.ID(), - ), - - transaction.NewOutputs(map[address.Address][]*balance.Balance{ - outputAddress1: { - balance.New(balance.ColorNew, 1337), - }, - }), - ))) - - // attach double spend - valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( - transaction.NewInputs( - input1.ID(), - input2.ID(), - ), - - transaction.NewOutputs(map[address.Address][]*balance.Balance{ - outputAddress2: { - balance.New(balance.ColorNew, 1337), - }, - }), - ))) - - valueTangle.Shutdown() - utxoDAG.Shutdown() -} diff --git a/dapps/valuetransfers/packages/utxodag/transactionmetadata.go b/dapps/valuetransfers/packages/tangle/transactionmetadata.go similarity index 99% rename from dapps/valuetransfers/packages/utxodag/transactionmetadata.go rename to dapps/valuetransfers/packages/tangle/transactionmetadata.go index ba9a23b1e91e669ada378854de2077e7af51cc13..7bda5190b319563a46b823e6552461f98e2e6a11 100644 --- a/dapps/valuetransfers/packages/utxodag/transactionmetadata.go +++ b/dapps/valuetransfers/packages/tangle/transactionmetadata.go @@ -1,4 +1,4 @@ -package utxodag +package tangle import ( "sync" diff --git a/dapps/valuetransfers/packages/test/valuetransfers_test.go b/dapps/valuetransfers/packages/test/valuetransfers_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d321c931d23b6b07a3934674626e76f0ec11eccd --- /dev/null +++ b/dapps/valuetransfers/packages/test/valuetransfers_test.go @@ -0,0 +1,97 @@ +package test + +import ( + "testing" + "time" + + "github.com/iotaledger/hive.go/crypto/ed25519" + "github.com/stretchr/testify/assert" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/packages/binary/testutil" +) + +func TestTangle_ValueTransfer(t *testing.T) { + // initialize tangle + ledgerstate + valueTangle := tangle.New(testutil.DB(t)) + if err := valueTangle.Prune(); err != nil { + t.Error(err) + + return + } + ledgerState := tangle.NewLedgerState(valueTangle) + + // + addressKeyPair1 := ed25519.GenerateKeyPair() + addressKeyPair2 := ed25519.GenerateKeyPair() + address1 := address.FromED25519PubKey(addressKeyPair1.PublicKey) + address2 := address.FromED25519PubKey(addressKeyPair2.PublicKey) + + // check if ledger empty first + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address1)) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address2)) + + // load snapshot + valueTangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{ + transaction.GenesisID: { + address1: []*balance.Balance{ + balance.New(balance.ColorIOTA, 337), + }, + + address2: []*balance.Balance{ + balance.New(balance.ColorIOTA, 1000), + }, + }, + }) + + // check if balance exists after loading snapshot + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 337}, ledgerState.Balances(address1)) + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1000}, ledgerState.Balances(address2)) + + // attach first spend + outputAddress1 := address.Random() + valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( + transaction.NewInputs( + transaction.NewOutputID(address1, transaction.GenesisID), + transaction.NewOutputID(address2, transaction.GenesisID), + ), + + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + outputAddress1: { + balance.New(balance.ColorIOTA, 1337), + }, + }), + ))) + + // wait for async task to run (TODO: REPLACE TIME BASED APPROACH WITH A WG) + time.Sleep(500 * time.Millisecond) + + // check if old addresses are empty + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address1)) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(address2)) + + // check if new addresses are filled + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1337}, ledgerState.Balances(outputAddress1)) + + // attach double spend + outputAddress2 := address.Random() + valueTangle.AttachPayload(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( + transaction.NewInputs( + transaction.NewOutputID(address1, transaction.GenesisID), + transaction.NewOutputID(address2, transaction.GenesisID), + ), + + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + outputAddress2: { + balance.New(balance.ColorNew, 1337), + }, + }), + ))) + + // shutdown tangle + valueTangle.Shutdown() +} diff --git a/dapps/valuetransfers/packages/transaction/id.go b/dapps/valuetransfers/packages/transaction/id.go index aa333343d30d345c96e153c920ebc3949f7a92c0..f1acc5e7ef17d5d5702d6472b3dcae0877e2aac8 100644 --- a/dapps/valuetransfers/packages/transaction/id.go +++ b/dapps/valuetransfers/packages/transaction/id.go @@ -82,5 +82,7 @@ func (id ID) String() string { return base58.Encode(id[:]) } +var GenesisID ID + // IDLength contains the amount of bytes that a marshaled version of the ID contains. const IDLength = 32 diff --git a/dapps/valuetransfers/packages/utxodag/events.go b/dapps/valuetransfers/packages/utxodag/events.go deleted file mode 100644 index 3ffdc48b0d59c981a054162b8a4d158f7e0de182..0000000000000000000000000000000000000000 --- a/dapps/valuetransfers/packages/utxodag/events.go +++ /dev/null @@ -1,59 +0,0 @@ -package utxodag - -import ( - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - - "github.com/iotaledger/hive.go/events" -) - -// Events is a container for the different kind of events of the UTXODAG. -type Events struct { - // TransactionReceived gets triggered whenever a transaction was received for the first time (not solid yet). - TransactionReceived *events.Event - - // TransactionBooked gets triggered whenever a transactions becomes solid and gets booked into a particular branch. - TransactionBooked *events.Event - - // Fork gets triggered when a previously un-conflicting transaction get's some of its inputs double spend, so that a - // new Branch is created. - Fork *events.Event - - Error *events.Event -} - -func newEvents() *Events { - return &Events{ - TransactionReceived: events.NewEvent(cachedTransactionEvent), - TransactionBooked: events.NewEvent(transactionBookedEvent), - Fork: events.NewEvent(forkEvent), - Error: events.NewEvent(events.ErrorCaller), - } -} - -func transactionBookedEvent(handler interface{}, params ...interface{}) { - handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *branchmanager.CachedBranch, []transaction.OutputID, bool))( - params[0].(*transaction.CachedTransaction).Retain(), - params[1].(*CachedTransactionMetadata).Retain(), - params[2].(*branchmanager.CachedBranch).Retain(), - params[3].([]transaction.OutputID), - params[4].(bool), - ) -} - -func forkEvent(handler interface{}, params ...interface{}) { - handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *branchmanager.CachedBranch, []transaction.OutputID))( - params[0].(*transaction.CachedTransaction).Retain(), - params[1].(*CachedTransactionMetadata).Retain(), - params[2].(*branchmanager.CachedBranch).Retain(), - params[3].([]transaction.OutputID), - ) -} - -func cachedTransactionEvent(handler interface{}, params ...interface{}) { - handler.(func(*transaction.CachedTransaction, *CachedTransactionMetadata, *CachedAttachment))( - params[0].(*transaction.CachedTransaction).Retain(), - params[1].(*CachedTransactionMetadata).Retain(), - params[2].(*CachedAttachment).Retain(), - ) -} diff --git a/dapps/valuetransfers/packages/utxodag/objectstorage.go b/dapps/valuetransfers/packages/utxodag/objectstorage.go deleted file mode 100644 index 3cee1878bdd43fc6c858012bab9cf94bd9c54a64..0000000000000000000000000000000000000000 --- a/dapps/valuetransfers/packages/utxodag/objectstorage.go +++ /dev/null @@ -1,48 +0,0 @@ -package utxodag - -import ( - "time" - - "github.com/iotaledger/hive.go/objectstorage" - - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" -) - -const ( - // the following values are a list of prefixes defined as an enum - _ byte = iota - - // prefixes used for the objectstorage - osTransaction - osTransactionMetadata - osAttachment - osOutput - osConsumer -) - -var ( - osLeakDetectionOption = objectstorage.LeakDetectionEnabled(true, objectstorage.LeakDetectionOptions{ - MaxConsumersPerObject: 10, - MaxConsumerHoldTime: 10 * time.Second, - }) -) - -func osTransactionFactory(key []byte) (objectstorage.StorableObject, int, error) { - return transaction.FromStorageKey(key) -} - -func osTransactionMetadataFactory(key []byte) (objectstorage.StorableObject, int, error) { - return TransactionMetadataFromStorageKey(key) -} - -func osAttachmentFactory(key []byte) (objectstorage.StorableObject, int, error) { - return AttachmentFromStorageKey(key) -} - -func osOutputFactory(key []byte) (objectstorage.StorableObject, int, error) { - return OutputFromStorageKey(key) -} - -func osConsumerFactory(key []byte) (objectstorage.StorableObject, int, error) { - return ConsumerFromStorageKey(key) -} diff --git a/dapps/valuetransfers/packages/utxodag/utxodag.go b/dapps/valuetransfers/packages/utxodag/utxodag.go deleted file mode 100644 index 399e3305eabfcfdae118c6b568106f7b9294f6ab..0000000000000000000000000000000000000000 --- a/dapps/valuetransfers/packages/utxodag/utxodag.go +++ /dev/null @@ -1,794 +0,0 @@ -package utxodag - -import ( - "container/list" - "errors" - "fmt" - "math" - "time" - - "github.com/dgraph-io/badger/v2" - "github.com/iotaledger/hive.go/async" - "github.com/iotaledger/hive.go/events" - "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/tangle" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/packages/binary/storageprefix" -) - -// UTXODAG represents the DAG of funds that are flowing from the genesis, to the addresses that have balance now, that -// is embedded as another layer in the message tangle. -type UTXODAG struct { - tangle *tangle.Tangle - branchManager *branchmanager.BranchManager - - 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 the UTXODAG and creates a new DAG on top a tangle. -func New(badgerInstance *badger.DB, tangle *tangle.Tangle) (result *UTXODAG) { - osFactory := objectstorage.NewFactory(badgerInstance, storageprefix.ValueTransfers) - - result = &UTXODAG{ - tangle: tangle, - branchManager: branchmanager.New(badgerInstance), - - transactionStorage: osFactory.New(osTransaction, osTransactionFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - transactionMetadataStorage: osFactory.New(osTransactionMetadata, osTransactionMetadataFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - attachmentStorage: osFactory.New(osAttachment, osAttachmentFactory, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - outputStorage: osFactory.New(osOutput, osOutputFactory, OutputKeyPartitions, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - consumerStorage: osFactory.New(osConsumer, osConsumerFactory, ConsumerPartitionKeys, objectstorage.CacheTime(time.Second), osLeakDetectionOption), - - Events: newEvents(), - } - - tangle.Events.PayloadSolid.Attach(events.NewClosure(result.ProcessSolidPayload)) - - return -} - -// BranchManager is the getter for the manager that takes care of creating and updating branches. -func (utxoDAG *UTXODAG) BranchManager() *branchmanager.BranchManager { - return utxoDAG.branchManager -} - -// ProcessSolidPayload is the main method of this struct. It is used to add new solid Payloads to the DAG. -func (utxoDAG *UTXODAG) ProcessSolidPayload(cachedPayload *payload.CachedPayload, cachedMetadata *tangle.CachedPayloadMetadata) { - utxoDAG.workerPool.Submit(func() { utxoDAG.storeTransactionWorker(cachedPayload, cachedMetadata) }) -} - -// Transaction loads the given transaction from the objectstorage. -func (utxoDAG *UTXODAG) Transaction(transactionID transaction.ID) *transaction.CachedTransaction { - return &transaction.CachedTransaction{CachedObject: utxoDAG.transactionStorage.Load(transactionID.Bytes())} -} - -// TransactionMetadata retrieves the metadata of a value payload from the object storage. -func (utxoDAG *UTXODAG) TransactionMetadata(transactionID transaction.ID) *CachedTransactionMetadata { - return &CachedTransactionMetadata{CachedObject: utxoDAG.transactionMetadataStorage.Load(transactionID.Bytes())} -} - -// TransactionOutput loads the given output from the objectstorage. -func (utxoDAG *UTXODAG) TransactionOutput(outputID transaction.OutputID) *CachedOutput { - return &CachedOutput{CachedObject: utxoDAG.outputStorage.Load(outputID.Bytes())} -} - -// GetConsumers retrieves the approvers of a payload from the object storage. -func (utxoDAG *UTXODAG) GetConsumers(outputID transaction.OutputID) CachedConsumers { - consumers := make(CachedConsumers, 0) - utxoDAG.consumerStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool { - consumers = append(consumers, &CachedConsumer{CachedObject: cachedObject}) - - return true - }, outputID.Bytes()) - - return consumers -} - -// GetAttachments retrieves the att of a payload from the object storage. -func (utxoDAG *UTXODAG) GetAttachments(transactionID transaction.ID) CachedAttachments { - attachments := make(CachedAttachments, 0) - utxoDAG.attachmentStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool { - attachments = append(attachments, &CachedAttachment{CachedObject: cachedObject}) - - return true - }, transactionID.Bytes()) - - return attachments -} - -// Shutdown stops the worker pools and shuts down the object storage instances. -func (utxoDAG *UTXODAG) Shutdown() *UTXODAG { - utxoDAG.workerPool.ShutdownGracefully() - - utxoDAG.transactionStorage.Shutdown() - utxoDAG.transactionMetadataStorage.Shutdown() - utxoDAG.outputStorage.Shutdown() - utxoDAG.consumerStorage.Shutdown() - - return utxoDAG -} - -// Prune resets the database and deletes all objects (for testing or "node resets"). -func (utxoDAG *UTXODAG) Prune() (err error) { - if err = utxoDAG.branchManager.Prune(); err != nil { - return - } - - for _, storage := range []*objectstorage.ObjectStorage{ - utxoDAG.transactionStorage, - utxoDAG.transactionMetadataStorage, - utxoDAG.outputStorage, - utxoDAG.consumerStorage, - } { - if err = storage.Prune(); err != nil { - return - } - } - - return -} - -func (utxoDAG *UTXODAG) storeTransactionWorker(cachedPayload *payload.CachedPayload, cachedPayloadMetadata *tangle.CachedPayloadMetadata) { - defer cachedPayload.Release() - defer cachedPayloadMetadata.Release() - - // abort if the parameters are empty - solidPayload := cachedPayload.Unwrap() - if solidPayload == nil || cachedPayloadMetadata.Unwrap() == nil { - return - } - - // store objects in database - cachedTransaction, cachedTransactionMetadata, cachedAttachment, transactionIsNew := utxoDAG.storeTransactionModels(solidPayload) - - // abort if the attachment was previously processed already (nil == was not stored) - if cachedAttachment == nil { - cachedTransaction.Release() - cachedTransactionMetadata.Release() - - return - } - - // trigger events for a new transaction - if transactionIsNew { - utxoDAG.Events.TransactionReceived.Trigger(cachedTransaction, cachedTransactionMetadata, cachedAttachment) - } - - // check solidity of transaction and its corresponding attachment - utxoDAG.solidifyTransactionWorker(cachedTransaction, cachedTransactionMetadata, cachedAttachment) -} - -func (utxoDAG *UTXODAG) storeTransactionModels(solidPayload *payload.Payload) (cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment, transactionIsNew bool) { - cachedTransaction = &transaction.CachedTransaction{CachedObject: utxoDAG.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: utxoDAG.transactionMetadataStorage.Store(NewTransactionMetadata(solidPayload.Transaction().ID()))} - - // store references to the consumed outputs - solidPayload.Transaction().Inputs().ForEach(func(outputId transaction.OutputID) bool { - utxoDAG.consumerStorage.Store(NewConsumer(outputId, solidPayload.Transaction().ID())).Release() - - return true - }) - } else { - cachedTransactionMetadata = &CachedTransactionMetadata{CachedObject: utxoDAG.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 := utxoDAG.attachmentStorage.StoreIfAbsent(NewAttachment(solidPayload.Transaction().ID(), solidPayload.ID())) - if !stored { - return - } - cachedAttachment = &CachedAttachment{CachedObject: attachment} - - return -} - -func (utxoDAG *UTXODAG) solidifyTransactionWorker(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata, attachment *CachedAttachment) { - // initialize the stack - solidificationStack := list.New() - solidificationStack.PushBack([3]interface{}{cachedTransaction, cachedTransactionMetadata, attachment}) - - // process payloads that are supposed to be checked for solidity recursively - for solidificationStack.Len() > 0 { - // execute logic inside a func, so we can use defer to release the objects - func() { - // retrieve cached objects - currentCachedTransaction, currentCachedTransactionMetadata, currentCachedAttachment := utxoDAG.popElementsFromSolidificationStack(solidificationStack) - defer currentCachedTransaction.Release() - defer currentCachedTransactionMetadata.Release() - defer currentCachedAttachment.Release() - - // unwrap cached objects - currentTransaction := currentCachedTransaction.Unwrap() - currentTransactionMetadata := currentCachedTransactionMetadata.Unwrap() - currentAttachment := currentCachedAttachment.Unwrap() - - // abort if any of the retrieved models is nil or payload is not solid or it was set as solid already - if currentTransaction == nil || currentTransactionMetadata == nil || currentAttachment == nil { - return - } - - // abort if the transaction is not solid or invalid - if transactionSolid, err := utxoDAG.isTransactionSolid(currentTransaction, currentTransactionMetadata); !transactionSolid || err != nil { - if err != nil { - // TODO: TRIGGER INVALID TX + REMOVE TXS THAT APPROVE IT - fmt.Println(err, currentTransaction) - } - - return - } - - transactionBecameNewlySolid := currentTransactionMetadata.SetSolid(true) - if !transactionBecameNewlySolid { - // TODO: book attachment / create reference from the value message to the corresponding branch - - return - } - - // ... and schedule check of approvers - utxoDAG.ForEachConsumers(currentTransaction, func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment) { - solidificationStack.PushBack([3]interface{}{cachedTransaction, transactionMetadata, cachedAttachment}) - }) - - // book transaction - if err := utxoDAG.bookTransaction(cachedTransaction.Retain(), cachedTransactionMetadata.Retain()); err != nil { - utxoDAG.Events.Error.Trigger(err) - } - }() - } -} - -func (utxoDAG *UTXODAG) popElementsFromSolidificationStack(stack *list.List) (*transaction.CachedTransaction, *CachedTransactionMetadata, *CachedAttachment) { - currentSolidificationEntry := stack.Front() - cachedTransaction := currentSolidificationEntry.Value.([3]interface{})[0].(*transaction.CachedTransaction) - cachedTransactionMetadata := currentSolidificationEntry.Value.([3]interface{})[1].(*CachedTransactionMetadata) - cachedAttachment := currentSolidificationEntry.Value.([3]interface{})[2].(*CachedAttachment) - stack.Remove(currentSolidificationEntry) - - return cachedTransaction, cachedTransactionMetadata, cachedAttachment -} - -func (utxoDAG *UTXODAG) isTransactionSolid(tx *transaction.Transaction, metadata *TransactionMetadata) (bool, error) { - // abort if any of the models are nil or has been deleted - if tx == nil || tx.IsDeleted() || metadata == nil || metadata.IsDeleted() { - return false, nil - } - - // abort if we have previously determined the solidity status of the transaction already - if metadata.Solid() { - return true, nil - } - - // get outputs that were referenced in the transaction inputs - cachedInputs := utxoDAG.getCachedOutputsFromTransactionInputs(tx) - defer cachedInputs.Release() - - // check the solidity of the inputs and retrieve the consumed balances - inputsSolid, consumedBalances, err := utxoDAG.checkTransactionInputs(cachedInputs) - - // abort if an error occurred or the inputs are not solid, yet - if !inputsSolid || err != nil { - return false, err - } - - if !utxoDAG.checkTransactionOutputs(consumedBalances, tx.Outputs()) { - return false, fmt.Errorf("the outputs do not match the inputs in transaction with id '%s'", tx.ID()) - } - - return true, nil -} - -func (utxoDAG *UTXODAG) getCachedOutputsFromTransactionInputs(tx *transaction.Transaction) (result CachedOutputs) { - result = make(CachedOutputs) - tx.Inputs().ForEach(func(inputId transaction.OutputID) bool { - result[inputId] = utxoDAG.TransactionOutput(inputId) - - return true - }) - - return -} - -func (utxoDAG *UTXODAG) checkTransactionInputs(cachedInputs CachedOutputs) (inputsSolid bool, consumedBalances map[balance.Color]int64, err error) { - inputsSolid = true - consumedBalances = make(map[balance.Color]int64) - consumedBranches := make([]branchmanager.BranchID, 0) - - for _, cachedInput := range cachedInputs { - if !cachedInput.Exists() { - inputsSolid = false - - continue - } - - // should never be nil as we check Exists() before - input := cachedInput.Unwrap() - - // update solid status - inputsSolid = inputsSolid && input.Solid() - - consumedBranches = append(consumedBranches, input.BranchID()) - - // 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") - - return - } - - newBalance = currentBalance + inputBalance.Value() - } else { - newBalance = inputBalance.Value() - } - consumedBalances[inputBalance.Color()] = newBalance - } - } - - branchesConflicting, err := utxoDAG.BranchManager().BranchesConflicting(consumedBranches...) - if branchesConflicting { - // TODO: make it an explicit error var - err = fmt.Errorf("the transaction combines conflicting branches") - } - - 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 (utxoDAG *UTXODAG) 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 a newly colored output (we check the supply later) - 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 -} - -// ForEachConsumers iterates through the transactions that are consuming outputs of the given transactions -func (utxoDAG *UTXODAG) ForEachConsumers(currentTransaction *transaction.Transaction, consume func(cachedTransaction *transaction.CachedTransaction, transactionMetadata *CachedTransactionMetadata, cachedAttachment *CachedAttachment)) { - seenTransactions := make(map[transaction.ID]types.Empty) - currentTransaction.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool { - utxoDAG.GetConsumers(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(consumer *Consumer) { - if _, transactionSeen := seenTransactions[consumer.TransactionID()]; !transactionSeen { - seenTransactions[consumer.TransactionID()] = types.Void - - cachedTransaction := utxoDAG.Transaction(consumer.TransactionID()) - cachedTransactionMetadata := utxoDAG.TransactionMetadata(consumer.TransactionID()) - for _, cachedAttachment := range utxoDAG.GetAttachments(consumer.TransactionID()) { - consume(cachedTransaction, cachedTransactionMetadata, cachedAttachment) - } - } - }) - - return true - }) -} - -func (utxoDAG *UTXODAG) bookTransaction(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *CachedTransactionMetadata) (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 - } - - consumedBranches := make(branchmanager.BranchIds) - conflictingInputs := make([]transaction.OutputID, 0) - conflictingInputsOfFirstConsumers := make(map[transaction.ID][]transaction.OutputID) - - if !transactionToBook.Inputs().ForEach(func(outputID transaction.OutputID) bool { - cachedOutput := utxoDAG.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'", outputID) - - 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) - } - - // keep track of the conflicting inputs - conflictingInputs = append(conflictingInputs, outputID) - - return true - }) { - return - } - - // TODO: handle error - cachedTargetBranch, _ := utxoDAG.branchManager.AggregateBranches(consumedBranches.ToList()...) - defer cachedTargetBranch.Release() - - targetBranch := cachedTargetBranch.Unwrap() - if targetBranch == nil { - return errors.New("failed to unwrap target branch") - } - targetBranch.Persist() - - if len(conflictingInputs) >= 1 { - cachedTargetBranch, _ = utxoDAG.branchManager.Fork(branchmanager.NewBranchID(transactionToBook.ID()), []branchmanager.BranchID{targetBranch.ID()}, conflictingInputs) - defer cachedTargetBranch.Release() - - targetBranch = cachedTargetBranch.Unwrap() - if targetBranch == nil { - return errors.New("failed to inherit branches") - } - } - - // book transaction into target branch - transactionMetadata.SetBranchID(targetBranch.ID()) - - // book outputs into the target branch - transactionToBook.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool { - newOutput := NewOutput(address, transactionToBook.ID(), targetBranch.ID(), balances) - newOutput.SetSolid(true) - utxoDAG.outputStorage.Store(newOutput).Release() - - return true - }) - - // fork the conflicting transactions into their own branch - decisionPending := false - for consumerID, conflictingInputs := range conflictingInputsOfFirstConsumers { - _, decisionFinalized, forkedErr := utxoDAG.Fork(consumerID, conflictingInputs) - if forkedErr != nil { - err = forkedErr - - return - } - - decisionPending = decisionPending || !decisionFinalized - } - - // trigger events - utxoDAG.Events.TransactionBooked.Trigger(cachedTransaction, cachedTransactionMetadata, cachedTargetBranch, conflictingInputs, decisionPending) - - // TODO: BOOK ATTACHMENT - - return -} - -func (utxoDAG *UTXODAG) calculateBranchOfTransaction(currentTransaction *transaction.Transaction) (branch *branchmanager.CachedBranch, err error) { - consumedBranches := make(branchmanager.BranchIds) - if !currentTransaction.Inputs().ForEach(func(outputId transaction.OutputID) bool { - cachedTransactionOutput := utxoDAG.TransactionOutput(outputId) - defer cachedTransactionOutput.Release() - - transactionOutput := cachedTransactionOutput.Unwrap() - if transactionOutput == nil { - err = fmt.Errorf("failed to load output '%s'", outputId) - - return false - } - - consumedBranches[transactionOutput.BranchID()] = types.Void - - return true - }) { - return - } - - branch, err = utxoDAG.branchManager.AggregateBranches(consumedBranches.ToList()...) - - return -} - -// TODO: write comment what it does -func (utxoDAG *UTXODAG) 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 := utxoDAG.branchManager.ElevateConflictBranch(currentTransactionMetadata.BranchID(), targetBranch.ID()) - if elevateErr != nil || isConflictBranch { - return elevateErr - } - - // determine the new branch of the transaction - newCachedTargetBranch, branchErr := utxoDAG.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 - } - - // 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 := utxoDAG.TransactionOutput(outputID) - defer cachedOutput.Release() - - // unwrap output - output := cachedOutput.Unwrap() - if output == nil { - err = fmt.Errorf("failed to load output '%s'", outputID) - - 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) - utxoDAG.GetConsumers(transaction.NewOutputID(address, currentTransaction.ID())).Consume(func(consumer *Consumer) { - consumingTransactions[consumer.TransactionID()] = types.Void - }) - for transactionID := range consumingTransactions { - transactionStack.PushBack([2]interface{}{utxoDAG.Transaction(transactionID), utxoDAG.TransactionMetadata(transactionID)}) - } - - return true - }) - - return nil - }(); err != nil { - return err - } - } - - return nil - }(); err != nil { - return - } - } - - return -} - -// Fork creates a new branch from an existing transaction. -func (utxoDAG *UTXODAG) Fork(transactionID transaction.ID, conflictingInputs []transaction.OutputID) (forked bool, finalized bool, err error) { - cachedTransaction := utxoDAG.Transaction(transactionID) - cachedTransactionMetadata := utxoDAG.TransactionMetadata(transactionID) - defer cachedTransaction.Release() - defer cachedTransactionMetadata.Release() - - tx := cachedTransaction.Unwrap() - if tx == nil { - err = fmt.Errorf("failed to load transaction '%s'", transactionID) - - return - } - txMetadata := cachedTransactionMetadata.Unwrap() - if txMetadata == nil { - err = fmt.Errorf("failed to load metadata of transaction '%s'", transactionID) - - return - } - - // abort if this transaction was finalized already - if txMetadata.Finalized() { - finalized = true - - return - } - - // update / create new branch - cachedTargetBranch, newBranchCreated := utxoDAG.branchManager.Fork(branchmanager.NewBranchID(tx.ID()), []branchmanager.BranchID{txMetadata.BranchID()}, conflictingInputs) - defer cachedTargetBranch.Release() - - // abort if the branch existed already - if !newBranchCreated { - return - } - - // unpack branch - targetBranch := cachedTargetBranch.Unwrap() - if targetBranch == nil { - err = fmt.Errorf("failed to unpack branch for transaction '%s'", transactionID) - - return - } - - // move transactions to new branch - if err = utxoDAG.moveTransactionToBranch(cachedTransaction.Retain(), cachedTransactionMetadata.Retain(), cachedTargetBranch.Retain()); err != nil { - return - } - - // trigger events + set result - utxoDAG.Events.Fork.Trigger(cachedTransaction, cachedTransactionMetadata, targetBranch, conflictingInputs) - forked = true - - return -}