From fc4b37450d5fe42bd37ac47158ecb5795e63bba2 Mon Sep 17 00:00:00 2001
From: Hans Moog <hm@mkjc.net>
Date: Fri, 7 Feb 2020 17:23:13 +0100
Subject: [PATCH] New binary tangle using atomic transactions (#239)

* Feat: started porting the new binary stuff

* Refactor: removed unnecessary folder

* Refactor: cleaned up go.mod files

* Fix: removed objectsdb files

* Feat: added transactionrequester package
---
 .gitignore                                    |   1 +
 go.mod                                        |   5 +-
 go.sum                                        |  10 +-
 packages/binary/identity/constants.go         |   7 +
 packages/binary/identity/identity.go          |  48 +++
 packages/binary/identity/identity_test.go     |  41 +++
 packages/binary/identity/type.go              |   8 +
 packages/binary/signature/ed25119/ed25119.go  |  18 +
 packages/binary/signature/ed25119/key_pair.go |   6 +
 .../binary/signature/ed25119/private_key.go   |  15 +
 .../binary/signature/ed25119/public_key.go    |  25 ++
 .../binary/signature/ed25119/signature.go     |  19 ++
 .../binary/storageprefix/storageprefix.go     |  17 +
 packages/binary/tangle/events.go              |  39 +++
 .../binary/tangle/model/approver/approver.go  |  59 ++++
 .../tangle/model/approver/cached_approver.go  |  33 ++
 .../cached_missingtransaction.go              |  21 ++
 .../missingtransaction/missingtransaction.go  |  53 +++
 .../model/transaction/cached_transaction.go   |  31 ++
 .../binary/tangle/model/transaction/id.go     |  34 ++
 .../binary/tangle/model/transaction/init.go   |  10 +
 .../model/transaction/payload/data/data.go    |  52 +++
 .../model/transaction/payload/data/init.go    |   9 +
 .../tangle/model/transaction/payload/id.go    |   5 +
 .../model/transaction/payload/payload.go      |  12 +
 .../tangle/model/transaction/payload/type.go  |   3 +
 .../transaction/payload/type_register.go      |  36 ++
 .../transaction/test/transaction_test.go      |  77 +++++
 .../tangle/model/transaction/transaction.go   | 277 +++++++++++++++
 .../cached_transactionmetadata.go             |  25 ++
 .../transactionmetadata.go                    |  94 +++++
 packages/binary/tangle/tangle.go              | 322 ++++++++++++++++++
 packages/binary/tangle/tangle_test.go         |  83 +++++
 .../binary/transactionrequester/constants.go  |  10 +
 .../binary/transactionrequester/events.go     |   9 +
 .../binary/transactionrequester/options.go    |  37 ++
 .../transactionrequester.go                   |  74 ++++
 37 files changed, 1622 insertions(+), 3 deletions(-)
 create mode 100644 packages/binary/identity/constants.go
 create mode 100644 packages/binary/identity/identity.go
 create mode 100644 packages/binary/identity/identity_test.go
 create mode 100644 packages/binary/identity/type.go
 create mode 100644 packages/binary/signature/ed25119/ed25119.go
 create mode 100644 packages/binary/signature/ed25119/key_pair.go
 create mode 100644 packages/binary/signature/ed25119/private_key.go
 create mode 100644 packages/binary/signature/ed25119/public_key.go
 create mode 100644 packages/binary/signature/ed25119/signature.go
 create mode 100644 packages/binary/storageprefix/storageprefix.go
 create mode 100644 packages/binary/tangle/events.go
 create mode 100644 packages/binary/tangle/model/approver/approver.go
 create mode 100644 packages/binary/tangle/model/approver/cached_approver.go
 create mode 100644 packages/binary/tangle/model/missingtransaction/cached_missingtransaction.go
 create mode 100644 packages/binary/tangle/model/missingtransaction/missingtransaction.go
 create mode 100644 packages/binary/tangle/model/transaction/cached_transaction.go
 create mode 100644 packages/binary/tangle/model/transaction/id.go
 create mode 100644 packages/binary/tangle/model/transaction/init.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/data/data.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/data/init.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/id.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/payload.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/type.go
 create mode 100644 packages/binary/tangle/model/transaction/payload/type_register.go
 create mode 100644 packages/binary/tangle/model/transaction/test/transaction_test.go
 create mode 100644 packages/binary/tangle/model/transaction/transaction.go
 create mode 100644 packages/binary/tangle/model/transactionmetadata/cached_transactionmetadata.go
 create mode 100644 packages/binary/tangle/model/transactionmetadata/transactionmetadata.go
 create mode 100644 packages/binary/tangle/tangle.go
 create mode 100644 packages/binary/tangle/tangle_test.go
 create mode 100644 packages/binary/transactionrequester/constants.go
 create mode 100644 packages/binary/transactionrequester/events.go
 create mode 100644 packages/binary/transactionrequester/options.go
 create mode 100644 packages/binary/transactionrequester/transactionrequester.go

diff --git a/.gitignore b/.gitignore
index 52422ce9..b376e693 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ testNodes/*
 
 # Database directory
 mainnetdb/
+objectsdb/
 
 # OSX related files
 .DS_Store
diff --git a/go.mod b/go.mod
index af0a116b..ba050fa6 100644
--- a/go.mod
+++ b/go.mod
@@ -13,13 +13,16 @@ require (
 	github.com/googollee/go-engine.io v1.4.3-0.20190924125625-798118fc0dd2
 	github.com/googollee/go-socket.io v1.4.3-0.20191204093753-683f8725b6d0
 	github.com/gorilla/websocket v1.4.1
-	github.com/iotaledger/hive.go v0.0.0-20200207144536-27b18f10f09e
+	github.com/iotaledger/hive.go v0.0.0-20200207140228-0bb6cf145e6b
 	github.com/iotaledger/iota.go v1.0.0-beta.14
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/gommon v0.3.0 // indirect
 	github.com/magiconair/properties v1.8.1
 	github.com/mattn/go-colorable v0.1.4 // indirect
 	github.com/mattn/go-isatty v0.0.11 // indirect
+	github.com/mr-tron/base58 v1.1.3
+	github.com/oasislabs/ed25519 v0.0.0-20200206134218-2893bee822a3
+	github.com/panjf2000/ants/v2 v2.2.2
 	github.com/pelletier/go-toml v1.6.0 // indirect
 	github.com/pkg/errors v0.9.1
 	github.com/rogpeppe/go-internal v1.5.2 // indirect
diff --git a/go.sum b/go.sum
index a943eb06..00bc8be6 100644
--- a/go.sum
+++ b/go.sum
@@ -123,8 +123,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/iotaledger/hive.go v0.0.0-20200207144536-27b18f10f09e h1:eM0/mEG3ZPflZRJxOn0cc+/8ZZIpirEAyhzF4rzTKjM=
-github.com/iotaledger/hive.go v0.0.0-20200207144536-27b18f10f09e/go.mod h1:wj3bFHlcX0NiEOWu5+WOg/MI/5N7PKCFnyaziaylB64=
+github.com/iotaledger/hive.go v0.0.0-20200207140228-0bb6cf145e6b h1:SCd1rO4xXhAaQDOdofXl46G7Y/on9KphLtPJjqDgLsY=
+github.com/iotaledger/hive.go v0.0.0-20200207140228-0bb6cf145e6b/go.mod h1:wj3bFHlcX0NiEOWu5+WOg/MI/5N7PKCFnyaziaylB64=
 github.com/iotaledger/iota.go v1.0.0-beta.9/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8=
 github.com/iotaledger/iota.go v1.0.0-beta.14 h1:Oeb28MfBuJEeXcGrLhTCJFtbsnc8y1u7xidsAmiOD5A=
 github.com/iotaledger/iota.go v1.0.0-beta.14/go.mod h1:F6WBmYd98mVjAmmPVYhnxg8NNIWCjjH8VWT9qvv3Rc8=
@@ -166,10 +166,14 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
+github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
 github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/oasislabs/ed25519 v0.0.0-20200206134218-2893bee822a3 h1:xhsvlWpWPdHXHt8i5eaXf2WbAxHLciGqrfup6zAPjVQ=
+github.com/oasislabs/ed25519 v0.0.0-20200206134218-2893bee822a3/go.mod h1:xIpCyrK2ouGA4QBGbiNbkoONrvJ00u9P3QOkXSOAC0c=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
@@ -177,6 +181,7 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/panjf2000/ants/v2 v2.2.2 h1:TWzusBjq/IflXhy+/S6u5wmMLCBdJnB9tPIx9Zmhvok=
 github.com/panjf2000/ants/v2 v2.2.2/go.mod h1:1GFm8bV8nyCQvU5K4WvBCTG1/YBFOD2VzjffD8fV55A=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
@@ -319,6 +324,7 @@ golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaE
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
diff --git a/packages/binary/identity/constants.go b/packages/binary/identity/constants.go
new file mode 100644
index 00000000..b8fb9850
--- /dev/null
+++ b/packages/binary/identity/constants.go
@@ -0,0 +1,7 @@
+package identity
+
+const (
+	PublicKeySize  = 32
+	PrivateKeySize = 64
+	SignatureSize  = 64
+)
diff --git a/packages/binary/identity/identity.go b/packages/binary/identity/identity.go
new file mode 100644
index 00000000..fd52c6f3
--- /dev/null
+++ b/packages/binary/identity/identity.go
@@ -0,0 +1,48 @@
+package identity
+
+import (
+	"crypto/rand"
+
+	"github.com/oasislabs/ed25519"
+)
+
+type Identity struct {
+	Type       Type
+	PublicKey  []byte
+	PrivateKey []byte
+}
+
+func New(publicKey []byte, optionalPrivateKey ...[]byte) *Identity {
+	this := &Identity{
+		PublicKey: make([]byte, len(publicKey)),
+	}
+
+	copy(this.PublicKey, publicKey)
+
+	if len(optionalPrivateKey) == 0 {
+		this.Type = Public
+	} else {
+		this.Type = Private
+		this.PrivateKey = optionalPrivateKey[0]
+	}
+
+	return this
+}
+
+func Generate() *Identity {
+	if public, private, err := ed25519.GenerateKey(rand.Reader); err != nil {
+		panic(err)
+	} else {
+		return New(public, private)
+	}
+}
+
+func (identity *Identity) Sign(data []byte) (sig []byte) {
+	sig = ed25519.Sign(identity.PrivateKey, data)
+
+	return
+}
+
+func (identity *Identity) VerifySignature(data []byte, signature []byte) bool {
+	return ed25519.Verify(identity.PublicKey, data, signature)
+}
diff --git a/packages/binary/identity/identity_test.go b/packages/binary/identity/identity_test.go
new file mode 100644
index 00000000..f5fc0d52
--- /dev/null
+++ b/packages/binary/identity/identity_test.go
@@ -0,0 +1,41 @@
+package identity
+
+import (
+	"sync"
+	"testing"
+
+	"github.com/panjf2000/ants/v2"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func BenchmarkIdentity_VerifySignature(b *testing.B) {
+	identity := Generate()
+	data := []byte("TESTDATA")
+	signature := identity.Sign(data)
+
+	var wg sync.WaitGroup
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		wg.Add(1)
+
+		_ = ants.Submit(func() {
+			identity.VerifySignature(data, signature)
+
+			wg.Done()
+		})
+	}
+
+	wg.Wait()
+}
+
+func Test(t *testing.T) {
+	identity := Generate()
+
+	signature := identity.Sign([]byte("TESTDATA1"))
+
+	assert.Equal(t, true, identity.VerifySignature([]byte("TESTDATA1"), signature))
+	assert.Equal(t, false, identity.VerifySignature([]byte("TESTDATA2"), signature))
+}
diff --git a/packages/binary/identity/type.go b/packages/binary/identity/type.go
new file mode 100644
index 00000000..cc207b02
--- /dev/null
+++ b/packages/binary/identity/type.go
@@ -0,0 +1,8 @@
+package identity
+
+type Type int
+
+const (
+	Private = Type(0)
+	Public  = Type(1)
+)
diff --git a/packages/binary/signature/ed25119/ed25119.go b/packages/binary/signature/ed25119/ed25119.go
new file mode 100644
index 00000000..d4b65997
--- /dev/null
+++ b/packages/binary/signature/ed25119/ed25119.go
@@ -0,0 +1,18 @@
+package ed25119
+
+import (
+	"crypto/rand"
+
+	"github.com/oasislabs/ed25519"
+)
+
+func GenerateKeyPair() (keyPair KeyPair) {
+	if public, private, err := ed25519.GenerateKey(rand.Reader); err != nil {
+		panic(err)
+	} else {
+		copy(keyPair.PrivateKey[:], private)
+		copy(keyPair.PublicKey[:], public)
+
+		return
+	}
+}
diff --git a/packages/binary/signature/ed25119/key_pair.go b/packages/binary/signature/ed25119/key_pair.go
new file mode 100644
index 00000000..ad0c31bb
--- /dev/null
+++ b/packages/binary/signature/ed25119/key_pair.go
@@ -0,0 +1,6 @@
+package ed25119
+
+type KeyPair struct {
+	PrivateKey PrivateKey
+	PublicKey  PublicKey
+}
diff --git a/packages/binary/signature/ed25119/private_key.go b/packages/binary/signature/ed25119/private_key.go
new file mode 100644
index 00000000..89dc1a15
--- /dev/null
+++ b/packages/binary/signature/ed25119/private_key.go
@@ -0,0 +1,15 @@
+package ed25119
+
+import (
+	"github.com/oasislabs/ed25519"
+)
+
+type PrivateKey [PrivateKeySize]byte
+
+func (privateKey PrivateKey) Sign(data []byte) (result Signature) {
+	copy(result[:], ed25519.Sign(privateKey[:], data))
+
+	return
+}
+
+const PrivateKeySize = 64
diff --git a/packages/binary/signature/ed25119/public_key.go b/packages/binary/signature/ed25119/public_key.go
new file mode 100644
index 00000000..c9d58e25
--- /dev/null
+++ b/packages/binary/signature/ed25119/public_key.go
@@ -0,0 +1,25 @@
+package ed25119
+
+import (
+	"errors"
+
+	"github.com/oasislabs/ed25519"
+)
+
+type PublicKey [PublicKeySize]byte
+
+func (publicKey PublicKey) VerifySignature(data []byte, signature Signature) bool {
+	return ed25519.Verify(publicKey[:], data, signature[:])
+}
+
+func (publicKey *PublicKey) UnmarshalBinary(bytes []byte) (err error) {
+	if len(bytes) < PublicKeySize {
+		return errors.New("not enough bytes")
+	}
+
+	copy(publicKey[:], bytes[:])
+
+	return
+}
+
+const PublicKeySize = 32
diff --git a/packages/binary/signature/ed25119/signature.go b/packages/binary/signature/ed25119/signature.go
new file mode 100644
index 00000000..bd33e113
--- /dev/null
+++ b/packages/binary/signature/ed25119/signature.go
@@ -0,0 +1,19 @@
+package ed25119
+
+import (
+	"errors"
+)
+
+type Signature [SignatureSize]byte
+
+func (signature *Signature) UnmarshalBinary(bytes []byte) (err error) {
+	if len(bytes) < SignatureSize {
+		return errors.New("not enough bytes")
+	}
+
+	copy(signature[:], bytes[:])
+
+	return
+}
+
+const SignatureSize = 64
diff --git a/packages/binary/storageprefix/storageprefix.go b/packages/binary/storageprefix/storageprefix.go
new file mode 100644
index 00000000..c491a24e
--- /dev/null
+++ b/packages/binary/storageprefix/storageprefix.go
@@ -0,0 +1,17 @@
+package storageprefix
+
+var (
+	TangleTransaction         = []byte{0}
+	TangleTransactionMetadata = []byte{6}
+	TangleApprovers           = []byte{1}
+	TangleMissingTransaction  = []byte{7}
+
+	ValueTangleTransferMetadata = []byte{8}
+	ValueTangleConsumers        = []byte{9}
+	ValueTangleMissingTransfers = []byte{10}
+
+	LedgerStateTransferOutput        = []byte{2}
+	LedgerStateTransferOutputBooking = []byte{3}
+	LedgerStateReality               = []byte{4}
+	LedgerStateConflictSet           = []byte{5}
+)
diff --git a/packages/binary/tangle/events.go b/packages/binary/tangle/events.go
new file mode 100644
index 00000000..d2bf3552
--- /dev/null
+++ b/packages/binary/tangle/events.go
@@ -0,0 +1,39 @@
+package tangle
+
+import (
+	"github.com/iotaledger/hive.go/events"
+
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transactionmetadata"
+)
+
+type Events struct {
+	TransactionAttached        *events.Event
+	TransactionSolid           *events.Event
+	MissingTransactionReceived *events.Event
+	TransactionMissing         *events.Event
+	TransactionUnsolidifiable  *events.Event
+	TransactionRemoved         *events.Event
+}
+
+func newEvents() *Events {
+	return &Events{
+		TransactionAttached:        events.NewEvent(cachedTransactionEvent),
+		TransactionSolid:           events.NewEvent(cachedTransactionEvent),
+		MissingTransactionReceived: events.NewEvent(cachedTransactionEvent),
+		TransactionMissing:         events.NewEvent(transactionIdEvent),
+		TransactionUnsolidifiable:  events.NewEvent(transactionIdEvent),
+		TransactionRemoved:         events.NewEvent(transactionIdEvent),
+	}
+}
+
+func transactionIdEvent(handler interface{}, params ...interface{}) {
+	handler.(func(transaction.Id))(params[0].(transaction.Id))
+}
+
+func cachedTransactionEvent(handler interface{}, params ...interface{}) {
+	handler.(func(*transaction.CachedTransaction, *transactionmetadata.CachedTransactionMetadata))(
+		params[0].(*transaction.CachedTransaction).Retain(),
+		params[1].(*transactionmetadata.CachedTransactionMetadata).Retain().(*transactionmetadata.CachedTransactionMetadata),
+	)
+}
diff --git a/packages/binary/tangle/model/approver/approver.go b/packages/binary/tangle/model/approver/approver.go
new file mode 100644
index 00000000..b08bf342
--- /dev/null
+++ b/packages/binary/tangle/model/approver/approver.go
@@ -0,0 +1,59 @@
+package approver
+
+import (
+	"github.com/iotaledger/hive.go/objectstorage"
+
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+)
+
+type Approver struct {
+	objectstorage.StorableObjectFlags
+
+	storageKey            []byte
+	referencedTransaction transaction.Id
+	approvingTransaction  transaction.Id
+}
+
+func New(referencedTransaction transaction.Id, approvingTransaction transaction.Id) *Approver {
+	approver := &Approver{
+		storageKey:            make([]byte, transaction.IdLength+transaction.IdLength),
+		referencedTransaction: referencedTransaction,
+		approvingTransaction:  approvingTransaction,
+	}
+
+	copy(approver.storageKey[:transaction.IdLength], referencedTransaction[:])
+	copy(approver.storageKey[transaction.IdLength:], approvingTransaction[:])
+
+	return approver
+}
+
+func FromStorage(id []byte) (result objectstorage.StorableObject) {
+	approver := &Approver{
+		storageKey: make([]byte, transaction.IdLength+transaction.IdLength),
+	}
+	copy(approver.referencedTransaction[:], id[:transaction.IdLength])
+	copy(approver.approvingTransaction[:], id[transaction.IdLength:])
+	copy(approver.storageKey, id)
+
+	return approver
+}
+
+func (approver *Approver) GetStorageKey() []byte {
+	return approver.storageKey
+}
+
+func (approver *Approver) GetApprovingTransactionId() transaction.Id {
+	return approver.approvingTransaction
+}
+
+func (approver *Approver) Update(other objectstorage.StorableObject) {
+	panic("approvers should never be overwritten and only stored once to optimize IO")
+}
+
+func (approver *Approver) MarshalBinary() (result []byte, err error) {
+	return
+}
+
+func (approver *Approver) UnmarshalBinary(data []byte) (err error) {
+	return
+}
diff --git a/packages/binary/tangle/model/approver/cached_approver.go b/packages/binary/tangle/model/approver/cached_approver.go
new file mode 100644
index 00000000..c5284432
--- /dev/null
+++ b/packages/binary/tangle/model/approver/cached_approver.go
@@ -0,0 +1,33 @@
+package approver
+
+import (
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+type CachedApprover struct {
+	objectstorage.CachedObject
+}
+
+func (cachedApprover *CachedApprover) Unwrap() *Approver {
+	if untypedObject := cachedApprover.Get(); untypedObject == nil {
+		return nil
+	} else {
+		if typedObject := untypedObject.(*Approver); typedObject == nil || typedObject.IsDeleted() {
+			return nil
+		} else {
+			return typedObject
+		}
+	}
+}
+
+type CachedApprovers []*CachedApprover
+
+func (cachedApprovers CachedApprovers) Consume(consumer func(approver *Approver)) (consumed bool) {
+	for _, cachedApprover := range cachedApprovers {
+		consumed = consumed || cachedApprover.Consume(func(object objectstorage.StorableObject) {
+			consumer(object.(*Approver))
+		})
+	}
+
+	return
+}
diff --git a/packages/binary/tangle/model/missingtransaction/cached_missingtransaction.go b/packages/binary/tangle/model/missingtransaction/cached_missingtransaction.go
new file mode 100644
index 00000000..f50f5321
--- /dev/null
+++ b/packages/binary/tangle/model/missingtransaction/cached_missingtransaction.go
@@ -0,0 +1,21 @@
+package missingtransaction
+
+import (
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+type CachedMissingTransaction struct {
+	objectstorage.CachedObject
+}
+
+func (cachedObject *CachedMissingTransaction) Unwrap() *MissingTransaction {
+	if untypedObject := cachedObject.Get(); untypedObject == nil {
+		return nil
+	} else {
+		if typedObject := untypedObject.(*MissingTransaction); typedObject == nil || typedObject.IsDeleted() {
+			return nil
+		} else {
+			return typedObject
+		}
+	}
+}
diff --git a/packages/binary/tangle/model/missingtransaction/missingtransaction.go b/packages/binary/tangle/model/missingtransaction/missingtransaction.go
new file mode 100644
index 00000000..ea1715c8
--- /dev/null
+++ b/packages/binary/tangle/model/missingtransaction/missingtransaction.go
@@ -0,0 +1,53 @@
+package missingtransaction
+
+import (
+	"time"
+
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+type MissingTransaction struct {
+	objectstorage.StorableObjectFlags
+
+	transactionId transaction.Id
+	missingSince  time.Time
+}
+
+func New(transactionId transaction.Id) *MissingTransaction {
+	return &MissingTransaction{
+		transactionId: transactionId,
+		missingSince:  time.Now(),
+	}
+}
+
+func FromStorage(key []byte) objectstorage.StorableObject {
+	result := &MissingTransaction{}
+	copy(result.transactionId[:], key)
+
+	return result
+}
+
+func (missingTransaction *MissingTransaction) GetTransactionId() transaction.Id {
+	return missingTransaction.transactionId
+}
+
+func (missingTransaction *MissingTransaction) GetMissingSince() time.Time {
+	return missingTransaction.missingSince
+}
+
+func (missingTransaction *MissingTransaction) GetStorageKey() []byte {
+	return missingTransaction.transactionId[:]
+}
+
+func (missingTransaction *MissingTransaction) Update(other objectstorage.StorableObject) {
+	panic("missing transactions should never be overwritten and only stored once to optimize IO")
+}
+
+func (missingTransaction *MissingTransaction) MarshalBinary() (result []byte, err error) {
+	return missingTransaction.missingSince.MarshalBinary()
+}
+
+func (missingTransaction *MissingTransaction) UnmarshalBinary(data []byte) (err error) {
+	return missingTransaction.missingSince.UnmarshalBinary(data)
+}
diff --git a/packages/binary/tangle/model/transaction/cached_transaction.go b/packages/binary/tangle/model/transaction/cached_transaction.go
new file mode 100644
index 00000000..5f177ea7
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/cached_transaction.go
@@ -0,0 +1,31 @@
+package transaction
+
+import (
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+type CachedTransaction struct {
+	objectstorage.CachedObject
+}
+
+func (cachedTransaction *CachedTransaction) Retain() *CachedTransaction {
+	return &CachedTransaction{cachedTransaction.CachedObject.Retain()}
+}
+
+func (cachedTransaction *CachedTransaction) Consume(consumer func(object *Transaction)) bool {
+	return cachedTransaction.CachedObject.Consume(func(object objectstorage.StorableObject) {
+		consumer(object.(*Transaction))
+	})
+}
+
+func (cachedTransaction *CachedTransaction) Unwrap() *Transaction {
+	if untypedTransaction := cachedTransaction.Get(); untypedTransaction == nil {
+		return nil
+	} else {
+		if typeCastedTransaction := untypedTransaction.(*Transaction); typeCastedTransaction == nil || typeCastedTransaction.IsDeleted() {
+			return nil
+		} else {
+			return typeCastedTransaction
+		}
+	}
+}
diff --git a/packages/binary/tangle/model/transaction/id.go b/packages/binary/tangle/model/transaction/id.go
new file mode 100644
index 00000000..dd5ca154
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/id.go
@@ -0,0 +1,34 @@
+package transaction
+
+import (
+	"github.com/mr-tron/base58"
+)
+
+type Id [IdLength]byte
+
+func NewId(id []byte) (result Id) {
+	copy(result[:], id)
+
+	return
+}
+
+func (id *Id) MarshalBinary() (result []byte, err error) {
+	result = make([]byte, IdLength)
+	copy(result, id[:])
+
+	return
+}
+
+func (id *Id) UnmarshalBinary(data []byte) (err error) {
+	copy(id[:], data)
+
+	return
+}
+
+func (id Id) String() string {
+	return base58.Encode(id[:])
+}
+
+var EmptyId = Id{}
+
+const IdLength = 64
diff --git a/packages/binary/tangle/model/transaction/init.go b/packages/binary/tangle/model/transaction/init.go
new file mode 100644
index 00000000..5805588c
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/init.go
@@ -0,0 +1,10 @@
+package transaction
+
+import (
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload/data"
+)
+
+func init() {
+	payload.SetGenericUnmarshalerFactory(data.GenericPayloadUnmarshalerFactory)
+}
diff --git a/packages/binary/tangle/model/transaction/payload/data/data.go b/packages/binary/tangle/model/transaction/payload/data/data.go
new file mode 100644
index 00000000..8b41b99c
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/data/data.go
@@ -0,0 +1,52 @@
+package data
+
+import (
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload"
+)
+
+type Data struct {
+	payloadType payload.Type
+	data        []byte
+}
+
+var Type = payload.Type(0)
+
+func New(data []byte) *Data {
+	return &Data{
+		payloadType: Type,
+		data:        data,
+	}
+}
+
+func (dataPayload *Data) GetType() payload.Type {
+	return dataPayload.payloadType
+}
+
+func (dataPayload *Data) GetData() []byte {
+	return dataPayload.data
+}
+
+func (dataPayload *Data) UnmarshalBinary(data []byte) error {
+	dataPayload.data = make([]byte, len(data))
+	copy(dataPayload.data, data)
+
+	return nil
+}
+
+func (dataPayload *Data) MarshalBinary() (data []byte, err error) {
+	data = make([]byte, len(dataPayload.data))
+	copy(data, dataPayload.data)
+
+	return
+}
+
+func GenericPayloadUnmarshalerFactory(payloadType payload.Type) payload.Unmarshaler {
+	return func(data []byte) (payload payload.Payload, err error) {
+		payload = &Data{
+			payloadType: payloadType,
+		}
+		err = payload.UnmarshalBinary(data)
+
+		return
+	}
+}
diff --git a/packages/binary/tangle/model/transaction/payload/data/init.go b/packages/binary/tangle/model/transaction/payload/data/init.go
new file mode 100644
index 00000000..547bca2c
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/data/init.go
@@ -0,0 +1,9 @@
+package data
+
+import (
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload"
+)
+
+func init() {
+	payload.RegisterType(Type, GenericPayloadUnmarshalerFactory(Type))
+}
diff --git a/packages/binary/tangle/model/transaction/payload/id.go b/packages/binary/tangle/model/transaction/payload/id.go
new file mode 100644
index 00000000..a20ce75f
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/id.go
@@ -0,0 +1,5 @@
+package payload
+
+type Id [IdLength]byte
+
+const IdLength = 64
diff --git a/packages/binary/tangle/model/transaction/payload/payload.go b/packages/binary/tangle/model/transaction/payload/payload.go
new file mode 100644
index 00000000..23ff09be
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/payload.go
@@ -0,0 +1,12 @@
+package payload
+
+import (
+	"encoding"
+)
+
+type Payload interface {
+	encoding.BinaryMarshaler
+	encoding.BinaryUnmarshaler
+
+	GetType() Type
+}
diff --git a/packages/binary/tangle/model/transaction/payload/type.go b/packages/binary/tangle/model/transaction/payload/type.go
new file mode 100644
index 00000000..a1594aa4
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/type.go
@@ -0,0 +1,3 @@
+package payload
+
+type Type = uint32
diff --git a/packages/binary/tangle/model/transaction/payload/type_register.go b/packages/binary/tangle/model/transaction/payload/type_register.go
new file mode 100644
index 00000000..aac9a919
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/payload/type_register.go
@@ -0,0 +1,36 @@
+package payload
+
+import (
+	"sync"
+)
+
+type Unmarshaler func(data []byte) (Payload, error)
+
+var (
+	typeRegister              = make(map[Type]Unmarshaler)
+	typeRegisterMutex         sync.RWMutex
+	genericUnmarshalerFactory func(payloadType Type) Unmarshaler
+)
+
+func RegisterType(payloadType Type, unmarshaler Unmarshaler) {
+	typeRegisterMutex.Lock()
+	typeRegister[payloadType] = unmarshaler
+	typeRegisterMutex.Unlock()
+}
+
+func GetUnmarshaler(payloadType Type) Unmarshaler {
+	typeRegisterMutex.RLock()
+	if unmarshaler, exists := typeRegister[payloadType]; exists {
+		typeRegisterMutex.RUnlock()
+
+		return unmarshaler
+	} else {
+		typeRegisterMutex.RUnlock()
+
+		return genericUnmarshalerFactory(payloadType)
+	}
+}
+
+func SetGenericUnmarshalerFactory(unmarshalerFactory func(payloadType Type) Unmarshaler) {
+	genericUnmarshalerFactory = unmarshalerFactory
+}
diff --git a/packages/binary/tangle/model/transaction/test/transaction_test.go b/packages/binary/tangle/model/transaction/test/transaction_test.go
new file mode 100644
index 00000000..1ca51d61
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/test/transaction_test.go
@@ -0,0 +1,77 @@
+package test
+
+import (
+	"runtime"
+	"sync"
+	"testing"
+
+	"github.com/iotaledger/hive.go/async"
+
+	"github.com/panjf2000/ants/v2"
+
+	"github.com/iotaledger/goshimmer/packages/binary/identity"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload/data"
+)
+
+func BenchmarkVerifyDataTransactions(b *testing.B) {
+	var pool async.WorkerPool
+	pool.Tune(runtime.NumCPU() * 2)
+
+	transactions := make([][]byte, b.N)
+	for i := 0; i < b.N; i++ {
+		tx := transaction.New(transaction.EmptyId, transaction.EmptyId, identity.Generate(), data.New([]byte("some data")))
+
+		if marshaledTransaction, err := tx.MarshalBinary(); err != nil {
+			b.Error(err)
+		} else {
+			transactions[i] = marshaledTransaction
+		}
+	}
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		currentIndex := i
+		pool.Submit(func() {
+			if tx, err := transaction.FromBytes(transactions[currentIndex]); err != nil {
+				b.Error(err)
+			} else {
+				tx.VerifySignature()
+			}
+		})
+	}
+
+	pool.Shutdown()
+}
+
+func BenchmarkVerifySignature(b *testing.B) {
+	pool, _ := ants.NewPool(80, ants.WithNonblocking(false))
+
+	transactions := make([]*transaction.Transaction, b.N)
+	for i := 0; i < b.N; i++ {
+		transactions[i] = transaction.New(transaction.EmptyId, transaction.EmptyId, identity.Generate(), data.New([]byte("test")))
+		transactions[i].GetBytes()
+	}
+
+	var wg sync.WaitGroup
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		wg.Add(1)
+
+		currentIndex := i
+		if err := pool.Submit(func() {
+			transactions[currentIndex].VerifySignature()
+
+			wg.Done()
+		}); err != nil {
+			b.Error(err)
+
+			return
+		}
+	}
+
+	wg.Wait()
+}
diff --git a/packages/binary/tangle/model/transaction/transaction.go b/packages/binary/tangle/model/transaction/transaction.go
new file mode 100644
index 00000000..de18ffc4
--- /dev/null
+++ b/packages/binary/tangle/model/transaction/transaction.go
@@ -0,0 +1,277 @@
+package transaction
+
+import (
+	"encoding/binary"
+	"sync"
+
+	"github.com/iotaledger/hive.go/stringify"
+
+	"github.com/iotaledger/goshimmer/packages/binary/identity"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload"
+
+	"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.Payload
+	bytes               []byte
+	bytesMutex          sync.RWMutex
+	signature           [identity.SignatureSize]byte
+	signatureMutex      sync.RWMutex
+
+	// derived properties
+	id             *Id
+	idMutex        sync.RWMutex
+	payloadId      *payload.Id
+	payloadIdMutex sync.RWMutex
+}
+
+// Allows us to "issue" a transaction.
+func New(trunkTransactionId Id, branchTransactionId Id, issuer *identity.Identity, payload 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 objectstorage.StorableObject) {
+	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) GetTrunkTransactionId() Id {
+	return transaction.trunkTransactionId
+}
+
+func (transaction *Transaction) GetBranchTransactionId() Id {
+	return transaction.branchTransactionId
+}
+
+func (transaction *Transaction) GetPayload() payload.Payload {
+	return transaction.payload
+}
+
+func (transaction *Transaction) GetPayloadId() (result payload.Id) {
+	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, IdLength+IdLength+payload.IdLength)
+	offset := 0
+
+	copy(hashBase[offset:], transaction.trunkTransactionId[:])
+	offset += IdLength
+
+	copy(hashBase[offset:], transaction.branchTransactionId[:])
+	offset += IdLength
+
+	copy(hashBase[offset:], payloadId[:])
+	// offset += payloadIdLength
+
+	return blake2b.Sum512(hashBase)
+}
+
+func (transaction *Transaction) calculatePayloadId() payload.Id {
+	bytes := transaction.GetBytes()
+
+	return blake2b.Sum512(bytes[2*IdLength:])
+}
+
+// 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, IdLength+IdLength+identity.PublicKeySize+4+serializedPayloadLength+identity.SignatureSize)
+			offset := 0
+
+			copy(result[offset:], transaction.trunkTransactionId[:])
+			offset += IdLength
+
+			copy(result[offset:], transaction.branchTransactionId[:])
+			offset += IdLength
+
+			if transaction.issuer != nil {
+				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
+			}
+
+			if transaction.issuer != nil {
+				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 += IdLength
+
+	copy(transaction.branchTransactionId[:], data[offset:])
+	offset += IdLength
+
+	transaction.issuer = identity.New(data[offset : offset+identity.PublicKeySize])
+	offset += identity.PublicKeySize
+
+	payloadType := binary.LittleEndian.Uint32(data[offset:])
+	offset += 4
+
+	if transaction.payload, err = payload.GetUnmarshaler(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[:])),
+	)
+}
diff --git a/packages/binary/tangle/model/transactionmetadata/cached_transactionmetadata.go b/packages/binary/tangle/model/transactionmetadata/cached_transactionmetadata.go
new file mode 100644
index 00000000..8b327806
--- /dev/null
+++ b/packages/binary/tangle/model/transactionmetadata/cached_transactionmetadata.go
@@ -0,0 +1,25 @@
+package transactionmetadata
+
+import (
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+type CachedTransactionMetadata struct {
+	objectstorage.CachedObject
+}
+
+func (cachedObject *CachedTransactionMetadata) Retain() objectstorage.CachedObject {
+	return &CachedTransactionMetadata{cachedObject}
+}
+
+func (cachedObject *CachedTransactionMetadata) Unwrap() *TransactionMetadata {
+	if untypedObject := cachedObject.Get(); untypedObject == nil {
+		return nil
+	} else {
+		if typedObject := untypedObject.(*TransactionMetadata); typedObject == nil || typedObject.IsDeleted() {
+			return nil
+		} else {
+			return typedObject
+		}
+	}
+}
diff --git a/packages/binary/tangle/model/transactionmetadata/transactionmetadata.go b/packages/binary/tangle/model/transactionmetadata/transactionmetadata.go
new file mode 100644
index 00000000..f29a6e34
--- /dev/null
+++ b/packages/binary/tangle/model/transactionmetadata/transactionmetadata.go
@@ -0,0 +1,94 @@
+package transactionmetadata
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotaledger/hive.go/objectstorage"
+
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+)
+
+type TransactionMetadata struct {
+	objectstorage.StorableObjectFlags
+
+	transactionId      transaction.Id
+	receivedTime       time.Time
+	solid              bool
+	solidificationTime time.Time
+
+	solidMutex              sync.RWMutex
+	solidificationTimeMutex sync.RWMutex
+}
+
+func New(transactionId transaction.Id) *TransactionMetadata {
+	return &TransactionMetadata{
+		transactionId: transactionId,
+		receivedTime:  time.Now(),
+	}
+}
+
+func FromStorage(id []byte) objectstorage.StorableObject {
+	result := &TransactionMetadata{}
+	copy(result.transactionId[:], id)
+
+	return result
+}
+
+func (transactionMetadata *TransactionMetadata) IsSolid() (result bool) {
+	transactionMetadata.solidMutex.RLock()
+	result = transactionMetadata.solid
+	transactionMetadata.solidMutex.RUnlock()
+
+	return
+}
+
+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
+}
+
+func (transactionMetadata *TransactionMetadata) GetSoldificationTime() time.Time {
+	transactionMetadata.solidificationTimeMutex.RLock()
+	defer transactionMetadata.solidificationTimeMutex.RUnlock()
+
+	return transactionMetadata.solidificationTime
+}
+
+func (transactionMetadata *TransactionMetadata) GetStorageKey() []byte {
+	return transactionMetadata.transactionId[:]
+}
+
+func (transactionMetadata *TransactionMetadata) Update(other objectstorage.StorableObject) {
+
+}
+
+func (transactionMetadata *TransactionMetadata) MarshalBinary() ([]byte, error) {
+	return nil, nil
+}
+
+func (transactionMetadata *TransactionMetadata) UnmarshalBinary([]byte) error {
+	return nil
+}
diff --git a/packages/binary/tangle/tangle.go b/packages/binary/tangle/tangle.go
new file mode 100644
index 00000000..9033cfdd
--- /dev/null
+++ b/packages/binary/tangle/tangle.go
@@ -0,0 +1,322 @@
+package tangle
+
+import (
+	"container/list"
+	"time"
+
+	"github.com/iotaledger/hive.go/types"
+
+	"github.com/iotaledger/goshimmer/packages/binary/storageprefix"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/approver"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/missingtransaction"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transactionmetadata"
+
+	"github.com/iotaledger/hive.go/async"
+	"github.com/iotaledger/hive.go/objectstorage"
+)
+
+const (
+	MAX_MISSING_TIME_BEFORE_CLEANUP = 30 * time.Second
+	MISSING_CHECK_INTERVAL          = 5 * time.Second
+)
+
+type Tangle struct {
+	storageId []byte
+
+	transactionStorage         *objectstorage.ObjectStorage
+	transactionMetadataStorage *objectstorage.ObjectStorage
+	approverStorage            *objectstorage.ObjectStorage
+	missingTransactionsStorage *objectstorage.ObjectStorage
+
+	Events Events
+
+	storeTransactionsWorkerPool async.WorkerPool
+	solidifierWorkerPool        async.WorkerPool
+	cleanupWorkerPool           async.WorkerPool
+}
+
+// Constructor for the tangle.
+func New(storageId []byte) (result *Tangle) {
+	result = &Tangle{
+		storageId:                  storageId,
+		transactionStorage:         objectstorage.New(append(storageId, storageprefix.TangleTransaction...), transaction.FromStorage),
+		transactionMetadataStorage: objectstorage.New(append(storageId, storageprefix.TangleTransactionMetadata...), transactionmetadata.FromStorage),
+		approverStorage:            objectstorage.New(append(storageId, storageprefix.TangleApprovers...), approver.FromStorage, objectstorage.PartitionKey(transaction.IdLength, transaction.IdLength)),
+		missingTransactionsStorage: objectstorage.New(append(storageId, storageprefix.TangleMissingTransaction...), missingtransaction.FromStorage),
+
+		Events: *newEvents(),
+	}
+
+	result.solidifierWorkerPool.Tune(1024)
+
+	return
+}
+
+// Returns the storage id of this tangle (can be used to create ontologies that follow the storage of the main tangle).
+func (tangle *Tangle) GetStorageId() []byte {
+	return tangle.storageId
+}
+
+// Attaches a new transaction to the tangle.
+func (tangle *Tangle) AttachTransaction(transaction *transaction.Transaction) {
+	tangle.storeTransactionsWorkerPool.Submit(func() { tangle.storeTransactionWorker(transaction) })
+}
+
+// Retrieves a transaction from the tangle.
+func (tangle *Tangle) GetTransaction(transactionId transaction.Id) *transaction.CachedTransaction {
+	return &transaction.CachedTransaction{CachedObject: tangle.transactionStorage.Load(transactionId[:])}
+}
+
+// Retrieves the metadata of a transaction from the tangle.
+func (tangle *Tangle) GetTransactionMetadata(transactionId transaction.Id) *transactionmetadata.CachedTransactionMetadata {
+	return &transactionmetadata.CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Load(transactionId[:])}
+}
+
+// Retrieves the approvers of a transaction from the tangle.
+func (tangle *Tangle) GetApprovers(transactionId transaction.Id) approver.CachedApprovers {
+	approvers := make(approver.CachedApprovers, 0)
+	tangle.approverStorage.ForEach(func(key []byte, cachedObject objectstorage.CachedObject) bool {
+		approvers = append(approvers, &approver.CachedApprover{CachedObject: cachedObject})
+
+		return true
+	}, transactionId[:])
+
+	return approvers
+}
+
+// Deletes a transaction from the tangle (i.e. for local snapshots)
+func (tangle *Tangle) DeleteTransaction(transactionId transaction.Id) {
+	tangle.GetTransaction(transactionId).Consume(func(currentTransaction *transaction.Transaction) {
+		trunkTransactionId := currentTransaction.GetTrunkTransactionId()
+		tangle.deleteApprover(trunkTransactionId, transactionId)
+
+		branchTransactionId := currentTransaction.GetBranchTransactionId()
+		if branchTransactionId != trunkTransactionId {
+			tangle.deleteApprover(branchTransactionId, transactionId)
+		}
+
+		tangle.transactionMetadataStorage.Delete(transactionId[:])
+		tangle.transactionStorage.Delete(transactionId[:])
+
+		tangle.Events.TransactionRemoved.Trigger(transactionId)
+	})
+}
+
+// Marks the tangle as stopped, so it will not accept any new transactions (waits for all backgroundTasks to finish.
+func (tangle *Tangle) Shutdown() *Tangle {
+	tangle.storeTransactionsWorkerPool.ShutdownGracefully()
+	tangle.solidifierWorkerPool.ShutdownGracefully()
+	tangle.cleanupWorkerPool.ShutdownGracefully()
+
+	tangle.transactionStorage.Shutdown()
+	tangle.transactionMetadataStorage.Shutdown()
+	tangle.approverStorage.Shutdown()
+	tangle.missingTransactionsStorage.Shutdown()
+
+	return tangle
+}
+
+// Resets the database and deletes all objects (good for testing or "node resets").
+func (tangle *Tangle) Prune() error {
+	for _, storage := range []*objectstorage.ObjectStorage{
+		tangle.transactionStorage,
+		tangle.transactionMetadataStorage,
+		tangle.approverStorage,
+		tangle.missingTransactionsStorage,
+	} {
+		if err := storage.Prune(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// Worker that stores the transactions and calls the corresponding "Storage events"
+func (tangle *Tangle) storeTransactionWorker(tx *transaction.Transaction) {
+	// store transaction
+	var cachedTransaction *transaction.CachedTransaction
+	if _tmp, transactionIsNew := tangle.transactionStorage.StoreIfAbsent(tx); !transactionIsNew {
+		return
+	} else {
+		cachedTransaction = &transaction.CachedTransaction{CachedObject: _tmp}
+	}
+
+	// store transaction metadata
+	transactionId := tx.GetId()
+	cachedTransactionMetadata := &transactionmetadata.CachedTransactionMetadata{CachedObject: tangle.transactionMetadataStorage.Store(transactionmetadata.New(transactionId))}
+
+	// store trunk approver
+	trunkTransactionID := tx.GetTrunkTransactionId()
+	tangle.approverStorage.Store(approver.New(trunkTransactionID, transactionId)).Release()
+
+	// store branch approver
+	if branchTransactionID := tx.GetBranchTransactionId(); branchTransactionID != trunkTransactionID {
+		tangle.approverStorage.Store(approver.New(branchTransactionID, transactionId)).Release()
+	}
+
+	// trigger events
+	if tangle.missingTransactionsStorage.DeleteIfPresent(transactionId[:]) {
+		tangle.Events.MissingTransactionReceived.Trigger(cachedTransaction, cachedTransactionMetadata)
+	}
+	tangle.Events.TransactionAttached.Trigger(cachedTransaction, cachedTransactionMetadata)
+
+	// check solidity
+	tangle.solidifierWorkerPool.Submit(func() {
+		tangle.solidifyTransactionWorker(cachedTransaction, cachedTransactionMetadata)
+	})
+}
+
+// Worker that solidifies the transactions (recursively from past to present).
+func (tangle *Tangle) solidifyTransactionWorker(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *transactionmetadata.CachedTransactionMetadata) {
+	isTransactionMarkedAsSolid := func(transactionId transaction.Id) bool {
+		if transactionId == transaction.EmptyId {
+			return true
+		}
+
+		transactionMetadataCached := tangle.GetTransactionMetadata(transactionId)
+		if transactionMetadata := transactionMetadataCached.Unwrap(); transactionMetadata == nil {
+			transactionMetadataCached.Release()
+
+			// if transaction is missing and was not reported as missing, yet
+			if cachedMissingTransaction, missingTransactionStored := tangle.missingTransactionsStorage.StoreIfAbsent(missingtransaction.New(transactionId)); missingTransactionStored {
+				cachedMissingTransaction.Consume(func(object objectstorage.StorableObject) {
+					tangle.monitorMissingTransactionWorker(object.(*missingtransaction.MissingTransaction).GetTransactionId())
+				})
+			}
+
+			return false
+		} else if !transactionMetadata.IsSolid() {
+			transactionMetadataCached.Release()
+
+			return false
+		}
+		transactionMetadataCached.Release()
+
+		return true
+	}
+
+	isTransactionSolid := func(transaction *transaction.Transaction, transactionMetadata *transactionmetadata.TransactionMetadata) bool {
+		if transaction == nil || transaction.IsDeleted() {
+			return false
+		}
+
+		if transactionMetadata == nil || transactionMetadata.IsDeleted() {
+			return false
+		}
+
+		if transactionMetadata.IsSolid() {
+			return true
+		}
+
+		return isTransactionMarkedAsSolid(transaction.GetTrunkTransactionId()) && isTransactionMarkedAsSolid(transaction.GetBranchTransactionId())
+	}
+
+	popElementsFromStack := func(stack *list.List) (*transaction.CachedTransaction, *transactionmetadata.CachedTransactionMetadata) {
+		currentSolidificationEntry := stack.Front()
+		currentCachedTransaction := currentSolidificationEntry.Value.([2]interface{})[0]
+		currentCachedTransactionMetadata := currentSolidificationEntry.Value.([2]interface{})[1]
+		stack.Remove(currentSolidificationEntry)
+
+		return currentCachedTransaction.(*transaction.CachedTransaction), currentCachedTransactionMetadata.(*transactionmetadata.CachedTransactionMetadata)
+	}
+
+	// initialize the stack
+	solidificationStack := list.New()
+	solidificationStack.PushBack([2]interface{}{cachedTransaction, cachedTransactionMetadata})
+
+	// process transactions that are supposed to be checked for solidity recursively
+	for solidificationStack.Len() > 0 {
+		currentCachedTransaction, currentCachedTransactionMetadata := popElementsFromStack(solidificationStack)
+
+		currentTransaction := currentCachedTransaction.Unwrap()
+		currentTransactionMetadata := currentCachedTransactionMetadata.Unwrap()
+		if currentTransaction == nil || currentTransactionMetadata == nil {
+			currentCachedTransaction.Release()
+			currentCachedTransactionMetadata.Release()
+
+			continue
+		}
+
+		// if current transaction is solid and was not marked as solid before: mark as solid and propagate
+		if isTransactionSolid(currentTransaction, currentTransactionMetadata) && currentTransactionMetadata.SetSolid(true) {
+			tangle.Events.TransactionSolid.Trigger(currentCachedTransaction, currentCachedTransactionMetadata)
+
+			tangle.GetApprovers(currentTransaction.GetId()).Consume(func(approver *approver.Approver) {
+				approverTransactionId := approver.GetApprovingTransactionId()
+
+				solidificationStack.PushBack([2]interface{}{
+					tangle.GetTransaction(approverTransactionId),
+					tangle.GetTransactionMetadata(approverTransactionId),
+				})
+			})
+		}
+
+		// release cached results
+		currentCachedTransaction.Release()
+		currentCachedTransactionMetadata.Release()
+	}
+}
+
+// Worker that Monitors the missing transactions (by scheduling regular checks).
+func (tangle *Tangle) monitorMissingTransactionWorker(transactionId transaction.Id) {
+	var scheduleNextMissingCheck func(transactionId transaction.Id)
+	scheduleNextMissingCheck = func(transactionId transaction.Id) {
+		time.AfterFunc(MISSING_CHECK_INTERVAL, func() {
+			tangle.missingTransactionsStorage.Load(transactionId[:]).Consume(func(object objectstorage.StorableObject) {
+				missingTransaction := object.(*missingtransaction.MissingTransaction)
+
+				if time.Since(missingTransaction.GetMissingSince()) >= MAX_MISSING_TIME_BEFORE_CLEANUP {
+					tangle.cleanupWorkerPool.Submit(func() {
+						tangle.Events.TransactionUnsolidifiable.Trigger(transactionId)
+
+						tangle.deleteSubtangle(missingTransaction.GetTransactionId())
+					})
+				} else {
+					// TRIGGER STILL MISSING EVENT?
+
+					scheduleNextMissingCheck(transactionId)
+				}
+			})
+		})
+	}
+
+	tangle.Events.TransactionMissing.Trigger(transactionId)
+
+	scheduleNextMissingCheck(transactionId)
+}
+
+func (tangle *Tangle) deleteApprover(approvedTransaction transaction.Id, approvingTransaction transaction.Id) {
+	idToDelete := make([]byte, transaction.IdLength+transaction.IdLength)
+	copy(idToDelete[:transaction.IdLength], approvedTransaction[:])
+	copy(idToDelete[transaction.IdLength:], approvingTransaction[:])
+	tangle.approverStorage.Delete(idToDelete)
+}
+
+// Deletes a transaction and all of its approvers (recursively).
+func (tangle *Tangle) deleteSubtangle(transactionId transaction.Id) {
+	cleanupStack := list.New()
+	cleanupStack.PushBack(transactionId)
+
+	processedTransactions := make(map[transaction.Id]types.Empty)
+	processedTransactions[transactionId] = types.Void
+
+	for cleanupStack.Len() >= 1 {
+		currentStackEntry := cleanupStack.Front()
+		currentTransactionId := currentStackEntry.Value.(transaction.Id)
+		cleanupStack.Remove(currentStackEntry)
+
+		tangle.DeleteTransaction(currentTransactionId)
+
+		tangle.GetApprovers(currentTransactionId).Consume(func(approver *approver.Approver) {
+			approverId := approver.GetApprovingTransactionId()
+
+			if _, transactionProcessed := processedTransactions[approverId]; !transactionProcessed {
+				cleanupStack.PushBack(approverId)
+
+				processedTransactions[approverId] = types.Void
+			}
+		})
+	}
+}
diff --git a/packages/binary/tangle/tangle_test.go b/packages/binary/tangle/tangle_test.go
new file mode 100644
index 00000000..33420e87
--- /dev/null
+++ b/packages/binary/tangle/tangle_test.go
@@ -0,0 +1,83 @@
+package tangle
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/iotaledger/hive.go/events"
+
+	"github.com/iotaledger/goshimmer/packages/binary/identity"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction/payload/data"
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transactionmetadata"
+)
+
+func BenchmarkTangle_AttachTransaction(b *testing.B) {
+	tangle := New([]byte("TEST_BINARY_TANGLE"))
+	if err := tangle.Prune(); err != nil {
+		b.Error(err)
+
+		return
+	}
+
+	testIdentity := identity.Generate()
+
+	transactionBytes := make([]*transaction.Transaction, b.N)
+	for i := 0; i < b.N; i++ {
+		transactionBytes[i] = transaction.New(transaction.EmptyId, transaction.EmptyId, testIdentity, data.New([]byte("some data")))
+		transactionBytes[i].GetBytes()
+	}
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		tangle.AttachTransaction(transactionBytes[i])
+	}
+
+	tangle.Shutdown()
+}
+
+func TestTangle_AttachTransaction(t *testing.T) {
+	tangle := New([]byte("TEST_BINARY_TANGLE"))
+	if err := tangle.Prune(); err != nil {
+		t.Error(err)
+
+		return
+	}
+
+	tangle.Events.TransactionAttached.Attach(events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *transactionmetadata.CachedTransactionMetadata) {
+		cachedTransaction.Consume(func(transaction *transaction.Transaction) {
+			fmt.Println("ATTACHED:", transaction.GetId())
+		})
+	}))
+
+	tangle.Events.TransactionSolid.Attach(events.NewClosure(func(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *transactionmetadata.CachedTransactionMetadata) {
+		cachedTransaction.Consume(func(transaction *transaction.Transaction) {
+			fmt.Println("SOLID:", transaction.GetId())
+		})
+	}))
+
+	tangle.Events.TransactionUnsolidifiable.Attach(events.NewClosure(func(transactionId transaction.Id) {
+		fmt.Println("UNSOLIDIFIABLE:", transactionId)
+	}))
+
+	tangle.Events.TransactionMissing.Attach(events.NewClosure(func(transactionId transaction.Id) {
+		fmt.Println("MISSING:", transactionId)
+	}))
+
+	tangle.Events.TransactionRemoved.Attach(events.NewClosure(func(transactionId transaction.Id) {
+		fmt.Println("REMOVED:", transactionId)
+	}))
+
+	newTransaction1 := transaction.New(transaction.EmptyId, transaction.EmptyId, identity.Generate(), data.New([]byte("some data")))
+	newTransaction2 := transaction.New(newTransaction1.GetId(), newTransaction1.GetId(), identity.Generate(), data.New([]byte("some other data")))
+
+	tangle.AttachTransaction(newTransaction2)
+
+	time.Sleep(7 * time.Second)
+
+	tangle.AttachTransaction(newTransaction1)
+
+	tangle.Shutdown()
+}
diff --git a/packages/binary/transactionrequester/constants.go b/packages/binary/transactionrequester/constants.go
new file mode 100644
index 00000000..b9f59769
--- /dev/null
+++ b/packages/binary/transactionrequester/constants.go
@@ -0,0 +1,10 @@
+package transactionrequester
+
+import (
+	"time"
+)
+
+const (
+	DEFAULT_REQUEST_WORKER_COUNT = 1024
+	DEFAULT_RETRY_INTERVAL       = 10 * time.Second
+)
diff --git a/packages/binary/transactionrequester/events.go b/packages/binary/transactionrequester/events.go
new file mode 100644
index 00000000..e845d790
--- /dev/null
+++ b/packages/binary/transactionrequester/events.go
@@ -0,0 +1,9 @@
+package transactionrequester
+
+import (
+	"github.com/iotaledger/hive.go/events"
+)
+
+type Events struct {
+	SendRequest *events.Event
+}
diff --git a/packages/binary/transactionrequester/options.go b/packages/binary/transactionrequester/options.go
new file mode 100644
index 00000000..05db5db4
--- /dev/null
+++ b/packages/binary/transactionrequester/options.go
@@ -0,0 +1,37 @@
+package transactionrequester
+
+import (
+	"time"
+)
+
+type Options struct {
+	retryInterval time.Duration
+	workerCount   int
+}
+
+func newOptions(optionalOptions []Option) *Options {
+	result := &Options{
+		retryInterval: 10 * time.Second,
+		workerCount:   DEFAULT_REQUEST_WORKER_COUNT,
+	}
+
+	for _, optionalOption := range optionalOptions {
+		optionalOption(result)
+	}
+
+	return result
+}
+
+type Option func(*Options)
+
+func RetryInterval(interval time.Duration) Option {
+	return func(args *Options) {
+		args.retryInterval = interval
+	}
+}
+
+func WorkerCount(workerCount int) Option {
+	return func(args *Options) {
+		args.workerCount = workerCount
+	}
+}
diff --git a/packages/binary/transactionrequester/transactionrequester.go b/packages/binary/transactionrequester/transactionrequester.go
new file mode 100644
index 00000000..6f036e81
--- /dev/null
+++ b/packages/binary/transactionrequester/transactionrequester.go
@@ -0,0 +1,74 @@
+package transactionrequester
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotaledger/hive.go/async"
+	"github.com/iotaledger/hive.go/events"
+
+	"github.com/iotaledger/goshimmer/packages/binary/tangle/model/transaction"
+)
+
+type TransactionRequester struct {
+	scheduledRequests map[transaction.Id]*time.Timer
+	requestWorker     async.NonBlockingWorkerPool
+	options           *Options
+	Events            Events
+
+	scheduledRequestsMutex sync.RWMutex
+}
+
+func New(optionalOptions ...Option) *TransactionRequester {
+	requester := &TransactionRequester{
+		scheduledRequests: make(map[transaction.Id]*time.Timer),
+		options:           newOptions(optionalOptions),
+		Events: Events{
+			SendRequest: events.NewEvent(func(handler interface{}, params ...interface{}) {
+				handler.(func(transaction.Id))(params[0].(transaction.Id))
+			}),
+		},
+	}
+
+	requester.requestWorker.Tune(requester.options.workerCount)
+
+	return requester
+}
+
+func (requester *TransactionRequester) ScheduleRequest(transactionId transaction.Id) {
+	var retryRequest func(bool)
+	retryRequest = func(initialRequest bool) {
+		requester.requestWorker.Submit(func() {
+			requester.scheduledRequestsMutex.RLock()
+			if _, requestExists := requester.scheduledRequests[transactionId]; !initialRequest && !requestExists {
+				requester.scheduledRequestsMutex.RUnlock()
+
+				return
+			}
+			requester.scheduledRequestsMutex.RUnlock()
+
+			requester.Events.SendRequest.Trigger(transactionId)
+
+			requester.scheduledRequestsMutex.Lock()
+			requester.scheduledRequests[transactionId] = time.AfterFunc(requester.options.retryInterval, func() { retryRequest(false) })
+			requester.scheduledRequestsMutex.Unlock()
+		})
+	}
+
+	retryRequest(true)
+}
+
+func (requester *TransactionRequester) StopRequest(transactionId transaction.Id) {
+	requester.scheduledRequestsMutex.RLock()
+	if timer, timerExists := requester.scheduledRequests[transactionId]; timerExists {
+		requester.scheduledRequestsMutex.RUnlock()
+
+		timer.Stop()
+
+		requester.scheduledRequestsMutex.Lock()
+		delete(requester.scheduledRequests, transactionId)
+		requester.scheduledRequestsMutex.Unlock()
+	} else {
+		requester.scheduledRequestsMutex.RUnlock()
+	}
+}
-- 
GitLab