Skip to content
Snippets Groups Projects
  • Hans Moog's avatar
    0355aee5
    Feat: Refactor Preferred / Liked logic (#428) · 0355aee5
    Hans Moog authored
    
    * Feat: initial commit
    
    * Feat: added setPreferred to TransactionMetadata
    
    * Feat: added a Conflicting() method to the transactionMetadata
    
    * Fix: fixed logic bug
    
    * Feat: refactored fcob
    
    * Refactor: refactored additional code
    
    * Fix: fixed a bug in ForeachConsumers
    
    * Refactor: cleaned up code
    
    * Feat: implemented FCOB consensus into the valuetransfer dapp
    
    * Refactor: refactored FCOB
    
    * Docs: added some additional comments
    
    * Docs: fixed comments
    
    * Refactor: commit before branch change
    
    * Feat: added PayloadLiked Event
    
    * Refactor: fixed some missing comments + added liked to marshal
    
    * Feat: reworked the preferred and liked propagation
    
    * Refactor: cleaned up some logic
    
    * Refactor: simplified code
    
    * Refactor: cleaned up more stuff :P
    
    * Refactor: refactor
    
    * Feat: moved test + refactored fcob
    
    * Fix: fixed a few bugs in liked propagation
    
    * adapt to new hive.go version
    
    * upgrade hive.go
    
    * Feat: started implementing a wallet
    
    * Feat: extended wallet files
    
    * use store backed sequence
    
    * add option to use in-memory database
    
    * address review comments
    
    * Fix: fixed missing events in branchmanaer
    
    * Feat: propagate changes from branch to transaction
    
    * Feat: started implementing confirmed propagation
    
    * Fix: fixed unreachable code
    
    * Refactor: refactored some code according to wolfgangs review
    
    * Refactor: cleaned up the code according to DRY
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored the code to make it more readable
    
    * Refactor: added some doc comments + cleaned up some more code
    
    Co-authored-by: default avatarWolfgang Welz <welzwo@gmail.com>
    Feat: Refactor Preferred / Liked logic (#428)
    Hans Moog authored
    
    * Feat: initial commit
    
    * Feat: added setPreferred to TransactionMetadata
    
    * Feat: added a Conflicting() method to the transactionMetadata
    
    * Fix: fixed logic bug
    
    * Feat: refactored fcob
    
    * Refactor: refactored additional code
    
    * Fix: fixed a bug in ForeachConsumers
    
    * Refactor: cleaned up code
    
    * Feat: implemented FCOB consensus into the valuetransfer dapp
    
    * Refactor: refactored FCOB
    
    * Docs: added some additional comments
    
    * Docs: fixed comments
    
    * Refactor: commit before branch change
    
    * Feat: added PayloadLiked Event
    
    * Refactor: fixed some missing comments + added liked to marshal
    
    * Feat: reworked the preferred and liked propagation
    
    * Refactor: cleaned up some logic
    
    * Refactor: simplified code
    
    * Refactor: cleaned up more stuff :P
    
    * Refactor: refactor
    
    * Feat: moved test + refactored fcob
    
    * Fix: fixed a few bugs in liked propagation
    
    * adapt to new hive.go version
    
    * upgrade hive.go
    
    * Feat: started implementing a wallet
    
    * Feat: extended wallet files
    
    * use store backed sequence
    
    * add option to use in-memory database
    
    * address review comments
    
    * Fix: fixed missing events in branchmanaer
    
    * Feat: propagate changes from branch to transaction
    
    * Feat: started implementing confirmed propagation
    
    * Fix: fixed unreachable code
    
    * Refactor: refactored some code according to wolfgangs review
    
    * Refactor: cleaned up the code according to DRY
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored according to wollac
    
    * Refactor: refactored the code to make it more readable
    
    * Refactor: added some doc comments + cleaned up some more code
    
    Co-authored-by: default avatarWolfgang Welz <welzwo@gmail.com>
transactionmetadata.go 13.32 KiB
package tangle

import (
	"sync"
	"time"

	"github.com/iotaledger/hive.go/marshalutil"
	"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/transaction"
)

// TransactionMetadata contains the information of a Transaction, that are based on our local perception of things (i.e. if it is
// solid, or when we it became solid).
type TransactionMetadata struct {
	objectstorage.StorableObjectFlags

	id                 transaction.ID
	branchID           branchmanager.BranchID
	solid              bool
	preferred          bool
	finalized          bool
	solidificationTime time.Time
	finalizationTime   time.Time

	branchIDMutex           sync.RWMutex
	solidMutex              sync.RWMutex
	preferredMutex          sync.RWMutex
	finalizedMutex          sync.RWMutex
	solidificationTimeMutex sync.RWMutex
}

// NewTransactionMetadata is the constructor for the TransactionMetadata type.
func NewTransactionMetadata(id transaction.ID) *TransactionMetadata {
	return &TransactionMetadata{
		id: id,
	}
}

// TransactionMetadataFromBytes unmarshals a TransactionMetadata object from a sequence of bytes.
// It either creates a new object or fills the optionally provided object with the parsed information.
func TransactionMetadataFromBytes(bytes []byte, optionalTargetObject ...*TransactionMetadata) (result *TransactionMetadata, consumedBytes int, err error) {
	marshalUtil := marshalutil.New(bytes)
	result, err = ParseTransactionMetadata(marshalUtil, optionalTargetObject...)
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// TransactionMetadataFromStorageKey get's called when we restore TransactionMetadata from the storage.
// In contrast to other database models, it unmarshals some information from the key so we simply store the key before
// it gets handed over to UnmarshalObjectStorageValue (by the ObjectStorage).
func TransactionMetadataFromStorageKey(keyBytes []byte, optionalTargetObject ...*TransactionMetadata) (result *TransactionMetadata, consumedBytes int, err error) {
	// determine the target object that will hold the unmarshaled information
	switch len(optionalTargetObject) {
	case 0:
		result = &TransactionMetadata{}
	case 1:
		result = optionalTargetObject[0]
	default:
		panic("too many arguments in call to TransactionMetadataFromStorageKey")
	}

	// parse information
	marshalUtil := marshalutil.New(keyBytes)
	result.id, err = transaction.ParseID(marshalUtil)
	if err != nil {
		return
	}
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// ParseTransactionMetadata is a wrapper for simplified unmarshaling of TransactionMetadata objects from a byte stream using the marshalUtil package.
func ParseTransactionMetadata(marshalUtil *marshalutil.MarshalUtil, optionalTargetObject ...*TransactionMetadata) (result *TransactionMetadata, err error) {
	parsedObject, parseErr := marshalUtil.Parse(func(data []byte) (interface{}, int, error) {
		return TransactionMetadataFromStorageKey(data, optionalTargetObject...)
	})
	if parseErr != nil {
		err = parseErr

		return
	}

	result = parsedObject.(*TransactionMetadata)
	_, err = marshalUtil.Parse(func(data []byte) (parseResult interface{}, parsedBytes int, parseErr error) {
		parsedBytes, parseErr = result.UnmarshalObjectStorageValue(data)

		return
	})

	return
}

// ID return the id of the Transaction that this TransactionMetadata is associated to.
func (transactionMetadata *TransactionMetadata) ID() transaction.ID {
	return transactionMetadata.id
}

// BranchID returns the identifier of the Branch, that this transaction is booked into.
func (transactionMetadata *TransactionMetadata) BranchID() branchmanager.BranchID {
	transactionMetadata.branchIDMutex.RLock()
	defer transactionMetadata.branchIDMutex.RUnlock()

	return transactionMetadata.branchID
}

// SetBranchID is the setter for the branch id. It returns true if the value of the flag has been updated.
func (transactionMetadata *TransactionMetadata) SetBranchID(branchID branchmanager.BranchID) (modified bool) {
	transactionMetadata.branchIDMutex.RLock()
	if transactionMetadata.branchID == branchID {
		transactionMetadata.branchIDMutex.RUnlock()

		return
	}

	transactionMetadata.branchIDMutex.RUnlock()
	transactionMetadata.branchIDMutex.Lock()
	defer transactionMetadata.branchIDMutex.Unlock()

	if transactionMetadata.branchID == branchID {
		return
	}

	transactionMetadata.branchID = branchID
	modified = true

	return
}

// Conflicting returns true if the Transaction has been forked into its own Branch and there is a vote going on.
func (transactionMetadata *TransactionMetadata) Conflicting() bool {
	return transactionMetadata.BranchID() == branchmanager.NewBranchID(transactionMetadata.ID())
}

// Solid returns true if the Transaction has been marked as solid.
func (transactionMetadata *TransactionMetadata) Solid() (result bool) {
	transactionMetadata.solidMutex.RLock()
	result = transactionMetadata.solid
	transactionMetadata.solidMutex.RUnlock()

	return
}

// SetSolid marks a Transaction as either solid or not solid.
// It returns true if the solid flag was changes and automatically updates the solidificationTime as well.
func (transactionMetadata *TransactionMetadata) SetSolid(solid bool) (modified bool) {
	transactionMetadata.solidMutex.RLock()
	if transactionMetadata.solid != solid {
		transactionMetadata.solidMutex.RUnlock()

		transactionMetadata.solidMutex.Lock()
		if transactionMetadata.solid != solid {
			transactionMetadata.solid = solid
			if solid {
				transactionMetadata.solidificationTimeMutex.Lock()
				transactionMetadata.solidificationTime = time.Now()
				transactionMetadata.solidificationTimeMutex.Unlock()
			}

			transactionMetadata.SetModified()

			modified = true
		}
		transactionMetadata.solidMutex.Unlock()

	} else {
		transactionMetadata.solidMutex.RUnlock()
	}

	return
}

// Preferred returns true if the transaction is considered to be the first valid spender of all of its Inputs.
func (transactionMetadata *TransactionMetadata) Preferred() (result bool) {
	transactionMetadata.preferredMutex.RLock()
	defer transactionMetadata.preferredMutex.RUnlock()

	return transactionMetadata.preferred
}

// setPreferred updates the preferred flag of the transaction. It is defined as a private setter because updating the
// preferred flag causes changes in other transactions and branches as well. This means that we need additional logic
// in the tangle. To update the preferred flag of a transaction, we need to use Tangle.SetTransactionPreferred(bool).
func (transactionMetadata *TransactionMetadata) setPreferred(preferred bool) (modified bool) {
	transactionMetadata.preferredMutex.RLock()
	if transactionMetadata.preferred == preferred {
		transactionMetadata.preferredMutex.RUnlock()

		return
	}

	transactionMetadata.preferredMutex.RUnlock()
	transactionMetadata.preferredMutex.Lock()
	defer transactionMetadata.preferredMutex.Unlock()

	if transactionMetadata.preferred == preferred {
		return
	}

	transactionMetadata.preferred = preferred
	transactionMetadata.SetModified()
	modified = true

	return
}
// SetFinalized allows us to set the finalized flag on the transactions. Finalized transactions will not be forked when
// a conflict arrives later.
func (transactionMetadata *TransactionMetadata) SetFinalized(finalized bool) (modified bool) {
	transactionMetadata.finalizedMutex.RLock()
	if transactionMetadata.finalized == finalized {
		transactionMetadata.finalizedMutex.RUnlock()

		return
	}

	transactionMetadata.finalizedMutex.RUnlock()
	transactionMetadata.finalizedMutex.Lock()
	defer transactionMetadata.finalizedMutex.Unlock()

	if transactionMetadata.finalized == finalized {
		return
	}

	transactionMetadata.finalized = finalized
	transactionMetadata.SetModified()
	if finalized {
		transactionMetadata.finalizationTime = time.Now()
	}
	modified = true

	return
}

// Finalized returns true, if the decision if this transaction is liked or not has been finalized by consensus already.
func (transactionMetadata *TransactionMetadata) Finalized() bool {
	transactionMetadata.finalizedMutex.RLock()
	defer transactionMetadata.finalizedMutex.RUnlock()

	return transactionMetadata.finalized
}

// FinalizationTime returns the time when this transaction was finalized.
func (transactionMetadata *TransactionMetadata) FinalizationTime() time.Time {
	transactionMetadata.finalizedMutex.RLock()
	defer transactionMetadata.finalizedMutex.RUnlock()

	return transactionMetadata.finalizationTime
}

// SoldificationTime returns the time when the Transaction was marked to be solid.
func (transactionMetadata *TransactionMetadata) SoldificationTime() time.Time {
	transactionMetadata.solidificationTimeMutex.RLock()
	defer transactionMetadata.solidificationTimeMutex.RUnlock()

	return transactionMetadata.solidificationTime
}

// Bytes marshals the TransactionMetadata object into a sequence of bytes.
func (transactionMetadata *TransactionMetadata) Bytes() []byte {
	return marshalutil.New(branchmanager.BranchIDLength + 2*marshalutil.TIME_SIZE + 3*marshalutil.BOOL_SIZE).
		WriteBytes(transactionMetadata.BranchID().Bytes()).
		WriteTime(transactionMetadata.SoldificationTime()).
		WriteTime(transactionMetadata.FinalizationTime()).
		WriteBool(transactionMetadata.Solid()).
		WriteBool(transactionMetadata.Preferred()).
		WriteBool(transactionMetadata.Finalized()).
		Bytes()
}

// String creates a human readable version of the metadata (for debug purposes).
func (transactionMetadata *TransactionMetadata) String() string {
	return stringify.Struct("transaction.TransactionMetadata",
		stringify.StructField("id", transactionMetadata.ID()),
		stringify.StructField("branchId", transactionMetadata.BranchID()),
		stringify.StructField("solid", transactionMetadata.Solid()),
		stringify.StructField("solidificationTime", transactionMetadata.SoldificationTime()),
	)
}

// ObjectStorageKey returns the key that is used to identify the TransactionMetadata in the objectstorage.
func (transactionMetadata *TransactionMetadata) ObjectStorageKey() []byte {
	return transactionMetadata.id.Bytes()
}

// Update is disabled and panics if it ever gets called - updates are supposed to happen through the setters.
func (transactionMetadata *TransactionMetadata) Update(other objectstorage.StorableObject) {
	panic("update forbidden")
}

// ObjectStorageValue marshals the TransactionMetadata object into a sequence of bytes and matches the encoding.BinaryMarshaler
// interface.
func (transactionMetadata *TransactionMetadata) ObjectStorageValue() []byte {
	return transactionMetadata.Bytes()
}

// UnmarshalObjectStorageValue restores the values of a TransactionMetadata object from a sequence of bytes and matches the
// encoding.BinaryUnmarshaler interface.
func (transactionMetadata *TransactionMetadata) UnmarshalObjectStorageValue(data []byte) (consumedBytes int, err error) {
	marshalUtil := marshalutil.New(data)
	if transactionMetadata.branchID, err = branchmanager.ParseBranchID(marshalUtil); err != nil {
		return
	}
	if transactionMetadata.solidificationTime, err = marshalUtil.ReadTime(); err != nil {
		return
	}
	if transactionMetadata.finalizationTime, err = marshalUtil.ReadTime(); err != nil {
		return
	}
	if transactionMetadata.solid, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if transactionMetadata.preferred, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if transactionMetadata.finalized, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// CachedTransactionMetadata is a wrapper for the object storage, that takes care of type casting the TransactionMetadata objects.
// Since go does not have generics (yet), the object storage works based on the generic "interface{}" type, which means
// that we have to regularly type cast the returned objects, to match the expected type. To reduce the burden of
// manually managing these type, we create a wrapper that does this for us. This way, we can consistently handle the
// specialized types of TransactionMetadata, without having to manually type cast over and over again.
type CachedTransactionMetadata struct {
	objectstorage.CachedObject
}

// Retain overrides the underlying method to return a new CachedTransactionMetadata instead of a generic CachedObject.
func (cachedTransactionMetadata *CachedTransactionMetadata) Retain() *CachedTransactionMetadata {
	return &CachedTransactionMetadata{cachedTransactionMetadata.CachedObject.Retain()}
}

// Consume  overrides the underlying method to use a CachedTransactionMetadata object instead of a generic CachedObject in the
// consumer).
func (cachedTransactionMetadata *CachedTransactionMetadata) Consume(consumer func(metadata *TransactionMetadata)) bool {
	return cachedTransactionMetadata.CachedObject.Consume(func(object objectstorage.StorableObject) {
		consumer(object.(*TransactionMetadata))
	})
}

// Unwrap provides a way to retrieve a type casted version of the underlying object.
func (cachedTransactionMetadata *CachedTransactionMetadata) Unwrap() *TransactionMetadata {
	untypedTransaction := cachedTransactionMetadata.Get()
	if untypedTransaction == nil {
		return nil
	}

	typeCastedTransaction := untypedTransaction.(*TransactionMetadata)
	if typeCastedTransaction == nil || typeCastedTransaction.IsDeleted() {
		return nil
	}

	return typeCastedTransaction
}

// Interface contract: make compiler warn if the interface is not implemented correctly.
var _ objectstorage.StorableObject = &TransactionMetadata{}