Skip to content
Snippets Groups Projects
transaction.go 6.87 KiB
package transaction

import (
	"encoding/binary"
	"sync"

	"github.com/iotaledger/goshimmer/packages/binary/identity"
	"github.com/iotaledger/goshimmer/packages/stringify"

	"github.com/iotaledger/hive.go/objectstorage"

	"github.com/mr-tron/base58"

	"golang.org/x/crypto/blake2b"
)

type Transaction struct {
	// base functionality of StorableObject
	objectstorage.StorableObjectFlags

	// core properties (they are part of the transaction when being sent)
	trunkTransactionId  Id
	branchTransactionId Id
	issuer              *identity.Identity
	payload             Payload
	bytes               []byte
	bytesMutex          sync.RWMutex
	signature           [identity.SignatureSize]byte
	signatureMutex      sync.RWMutex

	// derived properties
	id             *Id
	idMutex        sync.RWMutex
	payloadId      *PayloadId
	payloadIdMutex sync.RWMutex
}

// Allows us to "issue" a transaction.
func New(trunkTransactionId Id, branchTransactionId Id, issuer *identity.Identity, payload Payload) (result *Transaction) {
	return &Transaction{
		trunkTransactionId:  trunkTransactionId,
		branchTransactionId: branchTransactionId,
		issuer:              issuer,
		payload:             payload,
	}
}

// Get's called when we restore a transaction from storage. The bytes and the content will be unmarshaled by an external
// caller (the objectStorage factory).
func FromStorage(id []byte) (result *Transaction) {
	var transactionId Id
	copy(transactionId[:], id)

	result = &Transaction{
		id: &transactionId,
	}

	return
}

func FromBytes(bytes []byte) (result *Transaction, err error) {
	result = &Transaction{}
	err = result.UnmarshalBinary(bytes)

	return
}

func (transaction *Transaction) VerifySignature() (result bool) {
	transactionBytes := transaction.GetBytes()
	transaction.signatureMutex.RLock()
	result = transaction.issuer.VerifySignature(transactionBytes[:len(transactionBytes)-identity.SignatureSize], transaction.signature[:])
	transaction.signatureMutex.RUnlock()

	return
}

func (transaction *Transaction) GetId() (result Id) {
	transaction.idMutex.RLock()
	if transaction.id == nil {
		transaction.idMutex.RUnlock()

		transaction.idMutex.Lock()
		if transaction.id == nil {
			result = transaction.calculateTransactionId()

			transaction.id = &result
		} else {
			result = *transaction.id
		}
		transaction.idMutex.Unlock()
	} else {
		result = *transaction.id

		transaction.idMutex.RUnlock()
	}

	return
}

func (transaction *Transaction) GetPayload() Payload {
	return transaction.payload
}

func (transaction *Transaction) GetPayloadId() (result PayloadId) {
	transaction.payloadIdMutex.RLock()
	if transaction.payloadId == nil {
		transaction.payloadIdMutex.RUnlock()

		transaction.payloadIdMutex.Lock()
		if transaction.payloadId == nil {
			result = transaction.calculatePayloadId()

			transaction.payloadId = &result
		} else {
			result = *transaction.payloadId
		}
		transaction.payloadIdMutex.Unlock()
	} else {
		result = *transaction.payloadId

		transaction.payloadIdMutex.RUnlock()
	}

	return
}

func (transaction *Transaction) GetBytes() []byte {
	if result, err := transaction.MarshalBinary(); err != nil {
		panic(err)
	} else {
		return result
	}
}

func (transaction *Transaction) calculateTransactionId() Id {
	payloadId := transaction.GetPayloadId()

	hashBase := make([]byte, transactionIdLength+transactionIdLength+payloadIdLength)
	offset := 0

	copy(hashBase[offset:], transaction.trunkTransactionId[:])
	offset += transactionIdLength

	copy(hashBase[offset:], transaction.branchTransactionId[:])
	offset += transactionIdLength

	copy(hashBase[offset:], payloadId[:])
	// offset += payloadIdLength

	return blake2b.Sum512(hashBase)
}

func (transaction *Transaction) calculatePayloadId() PayloadId {
	bytes := transaction.GetBytes()

	return blake2b.Sum512(bytes[2*transactionIdLength:])
}

// Since transactions are immutable and do not get changed after being created, we cache the result of the marshaling.
func (transaction *Transaction) MarshalBinary() (result []byte, err error) {
	transaction.bytesMutex.RLock()
	if transaction.bytes == nil {
		transaction.bytesMutex.RUnlock()

		transaction.bytesMutex.Lock()
		if transaction.bytes == nil {
			var serializedPayload []byte
			if transaction.payload != nil {
				if serializedPayload, err = transaction.payload.MarshalBinary(); err != nil {
					return
				}
			}
			serializedPayloadLength := len(serializedPayload)

			result = make([]byte, transactionIdLength+transactionIdLength+identity.PublicKeySize+4+serializedPayloadLength+identity.SignatureSize)
			offset := 0

			copy(result[offset:], transaction.trunkTransactionId[:])
			offset += transactionIdLength

			copy(result[offset:], transaction.branchTransactionId[:])
			offset += transactionIdLength

			copy(result[offset:], transaction.issuer.PublicKey)
			offset += identity.PublicKeySize

			binary.LittleEndian.PutUint32(result[offset:], transaction.payload.GetType())
			offset += 4

			if serializedPayloadLength != 0 {
				copy(result[offset:], serializedPayload)
				offset += serializedPayloadLength
			}

			transaction.signatureMutex.Lock()
			copy(transaction.signature[:], transaction.issuer.Sign(result[:offset]))
			transaction.signatureMutex.Unlock()
			copy(result[offset:], transaction.signature[:])
			// offset += identity.SignatureSize

			transaction.bytes = result
		} else {
			result = transaction.bytes
		}
		transaction.bytesMutex.Unlock()
	} else {
		result = transaction.bytes

		transaction.bytesMutex.RUnlock()
	}

	return
}

func (transaction *Transaction) UnmarshalBinary(data []byte) (err error) {
	offset := 0

	copy(transaction.trunkTransactionId[:], data[offset:])
	offset += transactionIdLength

	copy(transaction.branchTransactionId[:], data[offset:])
	offset += transactionIdLength

	transaction.issuer = identity.New(data[offset : offset+identity.PublicKeySize])
	offset += identity.PublicKeySize

	payloadType := binary.LittleEndian.Uint32(data[offset:])
	offset += 4

	if transaction.payload, err = GetPayloadUnmarshaler(payloadType)(data[offset : len(data)-identity.SignatureSize]); err != nil {
		return
	}
	offset += len(data) - identity.SignatureSize - offset

	copy(transaction.signature[:], data[offset:])
	// offset += identity.SignatureSize

	transaction.bytes = make([]byte, len(data))
	copy(transaction.bytes, data)

	return
}

func (transaction *Transaction) GetStorageKey() []byte {
	transactionId := transaction.GetId()

	return transactionId[:]
}

func (transaction *Transaction) Update(other objectstorage.StorableObject) {
	panic("transactions should never be overwritten and only stored once to optimize IO")
}

func (transaction *Transaction) String() string {
	transactionId := transaction.GetId()

	return stringify.Struct("Transaction",
		stringify.StructField("id", base58.Encode(transactionId[:])),
		stringify.StructField("trunkTransactionId", base58.Encode(transaction.trunkTransactionId[:])),
		stringify.StructField("trunkTransactionId", base58.Encode(transaction.branchTransactionId[:])),
	)
}