From 9d61542a211dcee6752079e6f2899b369a85dda5 Mon Sep 17 00:00:00 2001
From: "Ching-Hua (Vivian) Lin" <jkrvivian@gmail.com>
Date: Thu, 18 Jun 2020 23:09:07 +0800
Subject: [PATCH] Add value tangle integration tests  (#464)

* feat: Add first value transfer integration test

* fix: fix wrong plugin name

* Add colored tokens test

* Add value tangle test to github workflow

* fix: Fix wrong function name in comments

* refactor: Make testSnapshots disabled in default and minor tweaks

* refactor: refactor balance list update method

* feat: Add inclusion states to weapis

* refactor: Rename Conflict to Rejected

* refactor: Remove unused parameter

* refactor: Simplify colored token test and increase waiting time

* fix: fix updateBalanceList

* fix: enable testsnapshots plugin for each peer

* refactor: Rename package name of each tests

* refactor: Rename functions and add more comments

* refactor: Add maxRetry to limit to avoid infinte loop and reduce else statement

* refactor: Refactor functions and remove redundant codes

* refactor: Rename shadowing variables

* test: Add value test in runTest.sh

* fixes some func comments

* increase startup time to make up for slow CI

Co-authored-by: Luca Moser <moser.luca@gmail.com>
---
 .github/workflows/integration-tests.yml       |  35 +++
 pluginmgr/core/plugins.go                     |   2 +
 plugins/testsnapshots/plugin.go               |  41 +++
 tools/integration-tests/runTests.sh           |   2 +-
 .../tester/framework/docker.go                |  11 +-
 .../tester/framework/drngnetwork.go           |   2 +-
 .../tester/framework/framework.go             |   1 +
 .../tester/framework/network.go               |  11 +-
 .../tester/framework/parameters.go            |   9 +-
 .../tester/framework/peer.go                  |   7 +-
 .../tester/tests/common/common_test.go        |   2 +-
 .../tester/tests/common/main_test.go          |   2 +-
 .../tester/tests/drng/drng_test.go            |   2 +-
 .../tester/tests/drng/main_test.go            |   2 +-
 .../tester/tests/message/main_test.go         |   2 +-
 .../tester/tests/message/message_test.go      |   2 +-
 .../tester/tests/testutil.go                  | 283 +++++++++++++++++-
 .../tester/tests/value/main_test.go           |  23 ++
 .../tester/tests/value/value_test.go          | 122 ++++++++
 19 files changed, 547 insertions(+), 14 deletions(-)
 create mode 100644 plugins/testsnapshots/plugin.go
 create mode 100644 tools/integration-tests/tester/tests/value/main_test.go
 create mode 100644 tools/integration-tests/tester/tests/value/value_test.go

diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index f1160f1c..cd08c1e7 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -143,3 +143,38 @@ jobs:
         with:
           name: ${{ env.TEST_NAME }}
           path: tools/integration-tests/logs
+
+
+  value:
+    name: value
+    env:
+      TEST_NAME: value
+    runs-on: ubuntu-latest
+    steps:
+
+      - name: Check out code
+        uses: actions/checkout@v2
+
+      - name: Build GoShimmer image
+        run: docker build -t iotaledger/goshimmer .
+
+      - name: Pull additional Docker images
+        run: |
+          docker pull angelocapossele/drand:latest
+          docker pull gaiaadm/pumba:latest
+          docker pull gaiadocker/iproute2:latest
+
+      - name: Run integration tests
+        run: docker-compose -f tools/integration-tests/tester/docker-compose.yml up --abort-on-container-exit --exit-code-from tester --build
+
+      - name: Create logs from tester
+        if: always()
+        run: |
+          docker logs tester &> tools/integration-tests/logs/tester.log
+
+      - name: Save logs as artifacts
+        if: always()
+        uses: actions/upload-artifact@v1
+        with:
+          name: ${{ env.TEST_NAME }}
+          path: tools/integration-tests/logs
diff --git a/pluginmgr/core/plugins.go b/pluginmgr/core/plugins.go
index 1eba3f11..eb4ff3b6 100644
--- a/pluginmgr/core/plugins.go
+++ b/pluginmgr/core/plugins.go
@@ -18,6 +18,7 @@ import (
 	"github.com/iotaledger/goshimmer/plugins/portcheck"
 	"github.com/iotaledger/goshimmer/plugins/profiling"
 	"github.com/iotaledger/goshimmer/plugins/sync"
+	"github.com/iotaledger/goshimmer/plugins/testsnapshots"
 
 	"github.com/iotaledger/hive.go/node"
 )
@@ -40,4 +41,5 @@ var PLUGINS = node.Plugins(
 	metrics.Plugin,
 	drng.Plugin,
 	valuetransfers.App,
+	testsnapshots.Plugin,
 )
diff --git a/plugins/testsnapshots/plugin.go b/plugins/testsnapshots/plugin.go
new file mode 100644
index 00000000..294e5337
--- /dev/null
+++ b/plugins/testsnapshots/plugin.go
@@ -0,0 +1,41 @@
+package testsnapshots
+
+import (
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+	"github.com/iotaledger/hive.go/logger"
+	"github.com/iotaledger/hive.go/node"
+)
+
+// FIXME: This plugin can be removed after snapshots is implemented
+const (
+	// PluginName is the plugin name of the TestSnapshots plugin.
+	PluginName = "TestSnapshots"
+)
+
+var (
+	// Plugin is the plugin instance of the TestSnapshots plugin.
+	Plugin = node.NewPlugin(PluginName, node.Disabled, configure, run)
+	log    *logger.Logger
+
+	// addresses for snapshots
+	address0, _ = address.FromBase58("JaMauTaTSVBNc13edCCvBK9fZxZ1KKW5fXegT1B7N9jY")
+)
+
+func configure(_ *node.Plugin) {
+	log = logger.NewLogger(PluginName)
+
+	valuetransfers.Tangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{
+		transaction.GenesisID: {
+			address0: []*balance.Balance{
+				balance.New(balance.ColorIOTA, 10000000),
+			},
+		},
+	})
+
+	log.Infof("load snapshots to tangle")
+}
+
+func run(_ *node.Plugin) {}
diff --git a/tools/integration-tests/runTests.sh b/tools/integration-tests/runTests.sh
index 71a65da5..28cb55b9 100755
--- a/tools/integration-tests/runTests.sh
+++ b/tools/integration-tests/runTests.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-TEST_NAMES='autopeering common drng message'
+TEST_NAMES='autopeering common drng message value'
 
 echo "Build GoShimmer image"
 docker build -t iotaledger/goshimmer ../../.
diff --git a/tools/integration-tests/tester/framework/docker.go b/tools/integration-tests/tester/framework/docker.go
index a5c2cb24..67a65d58 100644
--- a/tools/integration-tests/tester/framework/docker.go
+++ b/tools/integration-tests/tester/framework/docker.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"strings"
 	"time"
 
 	"github.com/docker/docker/api/types"
@@ -83,10 +84,16 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error {
 			"--logger.level=debug",
 			fmt.Sprintf("--node.disablePlugins=%s", config.DisabledPlugins),
 			fmt.Sprintf("--node.enablePlugins=%s", func() string {
+				var plugins []string
+				//TODO: remove this when snapshots is implemented
+				plugins = append(plugins, "testSnapshots")
 				if config.Bootstrap {
-					return "Bootstrap"
+					plugins = append(plugins, "Bootstrap")
 				}
-				return ""
+				if config.Faucet {
+					plugins = append(plugins, "faucet")
+				}
+				return strings.Join(plugins[:], ",")
 			}()),
 			fmt.Sprintf("--bootstrap.initialIssuance.timePeriodSec=%d", config.BootstrapInitialIssuanceTimePeriodSec),
 			"--webapi.bindAddress=0.0.0.0:8080",
diff --git a/tools/integration-tests/tester/framework/drngnetwork.go b/tools/integration-tests/tester/framework/drngnetwork.go
index 9df42cdb..2f3031ce 100644
--- a/tools/integration-tests/tester/framework/drngnetwork.go
+++ b/tools/integration-tests/tester/framework/drngnetwork.go
@@ -55,7 +55,7 @@ func (n *DRNGNetwork) CreatePeer(c GoShimmerConfig, publicKey ed25519.PublicKey)
 		return nil, err
 	}
 
-	peer, err := newPeer(name, identity.New(publicKey), container, n.network)
+	peer, err := newPeer(name, identity.New(publicKey), container, nil, n.network)
 	if err != nil {
 		return nil, err
 	}
diff --git a/tools/integration-tests/tester/framework/framework.go b/tools/integration-tests/tester/framework/framework.go
index 449c2dc6..d4456d57 100644
--- a/tools/integration-tests/tester/framework/framework.go
+++ b/tools/integration-tests/tester/framework/framework.go
@@ -82,6 +82,7 @@ func (f *Framework) CreateNetwork(name string, peers int, minimumNeighbors int,
 		config := GoShimmerConfig{
 			Bootstrap:                             i == 0,
 			BootstrapInitialIssuanceTimePeriodSec: bootstrapInitialIssuanceTimePeriodSec,
+			Faucet:                                i == 0,
 		}
 		if _, err = network.CreatePeer(config); err != nil {
 			return nil, err
diff --git a/tools/integration-tests/tester/framework/network.go b/tools/integration-tests/tester/framework/network.go
index c115fcb5..4580588f 100644
--- a/tools/integration-tests/tester/framework/network.go
+++ b/tools/integration-tests/tester/framework/network.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet"
 	"github.com/iotaledger/hive.go/crypto/ed25519"
 	"github.com/iotaledger/hive.go/identity"
 )
@@ -100,6 +101,14 @@ func (n *Network) CreatePeer(c GoShimmerConfig) (*Peer, error) {
 	config.EntryNodePublicKey = n.entryNodePublicKey()
 	config.DisabledPlugins = disabledPluginsPeer
 
+	// create wallet
+	var nodeWallet *wallet.Wallet
+	if c.Faucet == true {
+		nodeWallet = wallet.New(faucetSeed)
+	} else {
+		nodeWallet = wallet.New()
+	}
+
 	// create Docker container
 	container := NewDockerContainer(n.dockerClient)
 	err = container.CreateGoShimmerPeer(config)
@@ -115,7 +124,7 @@ func (n *Network) CreatePeer(c GoShimmerConfig) (*Peer, error) {
 		return nil, err
 	}
 
-	peer, err := newPeer(name, identity.New(publicKey), container, n)
+	peer, err := newPeer(name, identity.New(publicKey), container, nodeWallet, n)
 	if err != nil {
 		return nil, err
 	}
diff --git a/tools/integration-tests/tester/framework/parameters.go b/tools/integration-tests/tester/framework/parameters.go
index 6e2ae8c4..ddbb7046 100644
--- a/tools/integration-tests/tester/framework/parameters.go
+++ b/tools/integration-tests/tester/framework/parameters.go
@@ -13,7 +13,7 @@ const (
 
 	logsDir = "/tmp/logs/"
 
-	disabledPluginsEntryNode = "portcheck,dashboard,analysis-client,profiling,gossip,drng,issuer,sync,metrics,valuetransfers,messagelayer,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint"
+	disabledPluginsEntryNode = "portcheck,dashboard,analysis-client,profiling,gossip,drng,issuer,sync,metrics,valuetransfers,testsnapshots,messagelayer,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint"
 	disabledPluginsPeer      = "portcheck,dashboard,analysis-client,profiling"
 
 	dockerLogsPrefixLen = 8
@@ -23,6 +23,11 @@ const (
 	exitStatusSuccessful = 0
 )
 
+var (
+	faucetSeed = []byte{251, 163, 190, 98, 92, 82, 164, 79, 74, 48, 203, 162, 247, 119, 140, 76, 33, 100, 148, 204, 244, 248, 232, 18,
+		132, 217, 85, 31, 246, 83, 193, 193}
+)
+
 // GoShimmerConfig defines the config of a GoShimmer node.
 type GoShimmerConfig struct {
 	Seed               string
@@ -38,6 +43,8 @@ type GoShimmerConfig struct {
 	DRNGDistKey   string
 	DRNGInstance  int
 	DRNGThreshold int
+
+	Faucet bool
 }
 
 // NetworkConfig defines the config of a GoShimmer Docker network.
diff --git a/tools/integration-tests/tester/framework/peer.go b/tools/integration-tests/tester/framework/peer.go
index 18c059e8..8da31c20 100644
--- a/tools/integration-tests/tester/framework/peer.go
+++ b/tools/integration-tests/tester/framework/peer.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/iotaledger/goshimmer/client"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet"
 	"github.com/iotaledger/goshimmer/plugins/webapi/autopeering"
 	"github.com/iotaledger/hive.go/identity"
 )
@@ -24,13 +25,16 @@ type Peer struct {
 	// the DockerContainer that this peer is running in
 	*DockerContainer
 
+	// Wallet
+	*wallet.Wallet
+
 	chosen   []autopeering.Neighbor
 	accepted []autopeering.Neighbor
 }
 
 // newPeer creates a new instance of Peer with the given information.
 // dockerContainer needs to be started in order to determine the container's (and therefore peer's) IP correctly.
-func newPeer(name string, identity *identity.Identity, dockerContainer *DockerContainer, network *Network) (*Peer, error) {
+func newPeer(name string, identity *identity.Identity, dockerContainer *DockerContainer, wallet *wallet.Wallet, network *Network) (*Peer, error) {
 	// after container is started we can get its IP
 	ip, err := dockerContainer.IP(network.name)
 	if err != nil {
@@ -43,6 +47,7 @@ func newPeer(name string, identity *identity.Identity, dockerContainer *DockerCo
 		Identity:        identity,
 		GoShimmerAPI:    client.NewGoShimmerAPI(getWebAPIBaseURL(name), http.Client{Timeout: 30 * time.Second}),
 		DockerContainer: dockerContainer,
+		Wallet:          wallet,
 	}, nil
 }
 
diff --git a/tools/integration-tests/tester/tests/common/common_test.go b/tools/integration-tests/tester/tests/common/common_test.go
index 7cdceef8..32f4fea3 100644
--- a/tools/integration-tests/tester/tests/common/common_test.go
+++ b/tools/integration-tests/tester/tests/common/common_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package common
 
 import (
 	"testing"
diff --git a/tools/integration-tests/tester/tests/common/main_test.go b/tools/integration-tests/tester/tests/common/main_test.go
index bcc09c2e..cdffe476 100644
--- a/tools/integration-tests/tester/tests/common/main_test.go
+++ b/tools/integration-tests/tester/tests/common/main_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package common
 
 import (
 	"os"
diff --git a/tools/integration-tests/tester/tests/drng/drng_test.go b/tools/integration-tests/tester/tests/drng/drng_test.go
index a9b59a9f..4db59119 100644
--- a/tools/integration-tests/tester/tests/drng/drng_test.go
+++ b/tools/integration-tests/tester/tests/drng/drng_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package drng
 
 import (
 	"encoding/json"
diff --git a/tools/integration-tests/tester/tests/drng/main_test.go b/tools/integration-tests/tester/tests/drng/main_test.go
index bcc09c2e..27877125 100644
--- a/tools/integration-tests/tester/tests/drng/main_test.go
+++ b/tools/integration-tests/tester/tests/drng/main_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package drng
 
 import (
 	"os"
diff --git a/tools/integration-tests/tester/tests/message/main_test.go b/tools/integration-tests/tester/tests/message/main_test.go
index bcc09c2e..4d3e5451 100644
--- a/tools/integration-tests/tester/tests/message/main_test.go
+++ b/tools/integration-tests/tester/tests/message/main_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package message
 
 import (
 	"os"
diff --git a/tools/integration-tests/tester/tests/message/message_test.go b/tools/integration-tests/tester/tests/message/message_test.go
index 37460d75..11315ebb 100644
--- a/tools/integration-tests/tester/tests/message/message_test.go
+++ b/tools/integration-tests/tester/tests/message/message_test.go
@@ -1,4 +1,4 @@
-package autopeering
+package message
 
 import (
 	"testing"
diff --git a/tools/integration-tests/tester/tests/testutil.go b/tools/integration-tests/tester/tests/testutil.go
index 9e11ee44..b47c5c12 100644
--- a/tools/integration-tests/tester/tests/testutil.go
+++ b/tools/integration-tests/tester/tests/testutil.go
@@ -4,13 +4,20 @@ import (
 	"fmt"
 	"math/rand"
 	"testing"
+	"time"
 
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
 	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/payload"
 	"github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+const maxRetry = 50
+
 // DataMessageSent defines a struct to identify from which issuer a data message was sent.
 type DataMessageSent struct {
 	number          int
@@ -47,7 +54,7 @@ func SendDataMessagesOnRandomPeer(t *testing.T, peers []*framework.Peer, numMess
 // SendDataMessage sends a data message on a given peer and returns the id and a DataMessageSent struct.
 func SendDataMessage(t *testing.T, peer *framework.Peer, data []byte, number int) (string, DataMessageSent) {
 	id, err := peer.Data(data)
-	require.NoErrorf(t, err, "Could not send message on %s", peer.String())
+	require.NoErrorf(t, err, "could not send message on %s", peer.String())
 
 	sent := DataMessageSent{
 		number: number,
@@ -95,6 +102,280 @@ func CheckForMessageIds(t *testing.T, peers []*framework.Peer, ids map[string]Da
 	}
 }
 
+// SendTransactionFromFaucet sends funds to peers from the faucet, sends back the remainder to faucet, and returns the transaction ID.
+func SendTransactionFromFaucet(t *testing.T, peers []*framework.Peer, sentValue int64) (txIds []string, addrBalance map[string]map[balance.Color]int64) {
+	// initiate addrBalance map
+	addrBalance = make(map[string]map[balance.Color]int64)
+	for _, p := range peers {
+		addr := p.Seed().Address(0).String()
+		addrBalance[addr] = make(map[balance.Color]int64)
+	}
+
+	faucetPeer := peers[0]
+	faucetAddrStr := faucetPeer.Seed().Address(0).String()
+
+	// get faucet balances
+	unspentOutputs, err := faucetPeer.GetUnspentOutputs([]string{faucetAddrStr})
+	require.NoErrorf(t, err, "could not get unspent outputs on %s", faucetPeer.String())
+	addrBalance[faucetAddrStr][balance.ColorIOTA] = unspentOutputs.UnspentOutputs[0].OutputIDs[0].Balances[0].Value
+
+	// send funds to other peers
+	for i := 1; i < len(peers); i++ {
+		fail, txId := SendIotaTransaction(t, faucetPeer, peers[i], addrBalance, sentValue)
+		require.False(t, fail)
+		txIds = append(txIds, txId)
+
+		// let the transaction propagate
+		time.Sleep(3 * time.Second)
+	}
+
+	return
+}
+
+// SendTransactionOnRandomPeer sends sentValue amount of IOTA tokens from/to a random peer, mutates the given balance map and returns the transaction IDs.
+func SendTransactionOnRandomPeer(t *testing.T, peers []*framework.Peer, addrBalance map[string]map[balance.Color]int64, numMessages int, sentValue int64) (txIds []string) {
+	counter := 0
+	for i := 0; i < numMessages; i++ {
+		from := rand.Intn(len(peers))
+		to := rand.Intn(len(peers))
+		fail, txId := SendIotaTransaction(t, peers[from], peers[to], addrBalance, sentValue)
+		if fail {
+			i--
+			counter++
+			if counter >= maxRetry {
+				return
+			}
+			continue
+		}
+
+		// attach tx id
+		txIds = append(txIds, txId)
+
+		// let the transaction propagate
+		time.Sleep(3 * time.Second)
+	}
+
+	return
+}
+
+// SendIotaTransaction sends sentValue amount of IOTA tokens and remainders from and to a given peer and returns the fail flag and the transaction ID.
+// Every peer sends and receives the transaction on the address of index 0.
+func SendIotaTransaction(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]int64, sentValue int64) (fail bool, txId string) {
+	sigScheme := signaturescheme.ED25519(*from.Seed().KeyPair(0))
+	inputAddr := from.Seed().Address(0)
+	outputAddr := to.Seed().Address(0)
+
+	// prepare inputs
+	resp, err := from.GetUnspentOutputs([]string{inputAddr.String()})
+	require.NoErrorf(t, err, "could not get unspent outputs on %s", from.String())
+
+	// abort if no unspent outputs
+	if len(resp.UnspentOutputs[0].OutputIDs) == 0 {
+		return true, ""
+	}
+	availableValue := resp.UnspentOutputs[0].OutputIDs[0].Balances[0].Value
+
+	// abort if the balance is not enough
+	if availableValue < sentValue {
+		return true, ""
+	}
+
+	out, err := transaction.OutputIDFromBase58(resp.UnspentOutputs[0].OutputIDs[0].ID)
+	require.NoErrorf(t, err, "invalid unspent outputs ID on %s", from.String())
+	inputs := transaction.NewInputs([]transaction.OutputID{out}...)
+
+	// prepare outputs
+	outmap := map[address.Address][]*balance.Balance{}
+	if inputAddr == outputAddr {
+		sentValue = availableValue
+	}
+
+	// set balances
+	outmap[outputAddr] = []*balance.Balance{balance.New(balance.ColorIOTA, sentValue)}
+	outputs := transaction.NewOutputs(outmap)
+
+	// handle remainder address
+	if availableValue > sentValue {
+		outputs.Add(inputAddr, []*balance.Balance{balance.New(balance.ColorIOTA, availableValue-sentValue)})
+	}
+
+	// sign transaction
+	txn := transaction.New(inputs, outputs).Sign(sigScheme)
+
+	// send transaction
+	txId, err = from.SendTransaction(txn.Bytes())
+	require.NoErrorf(t, err, "could not send transaction on %s", from.String())
+
+	addrBalance[inputAddr.String()][balance.ColorIOTA] -= sentValue
+	addrBalance[outputAddr.String()][balance.ColorIOTA] += sentValue
+
+	return false, txId
+}
+
+// SendColoredTransactionOnRandomPeer sends colored tokens on a random peer, saves the sent token amount to a map, and returns transaction IDs.
+func SendColoredTransactionOnRandomPeer(t *testing.T, peers []*framework.Peer, addrBalance map[string]map[balance.Color]int64, numMessages int) (txIds []string) {
+	counter := 0
+	for i := 0; i < numMessages; i++ {
+		from := rand.Intn(len(peers))
+		to := rand.Intn(len(peers))
+		fail, txId := SendColoredTransaction(t, peers[from], peers[to], addrBalance)
+		if fail {
+			i--
+			counter++
+			if counter >= maxRetry {
+				return
+			}
+			continue
+		}
+
+		// attach tx id
+		txIds = append(txIds, txId)
+
+		// let the transaction propagate
+		time.Sleep(3 * time.Second)
+	}
+
+	return
+}
+
+// SendColoredTransaction sends IOTA and colored tokens from and to a given peer and returns the fail flag and the transaction ID.
+// 1. Get the first unspent outputs of `from`
+// 2. Accumulate the token amount of the first unspent output
+// 3. Send 50 IOTA tokens + [accumalate token amount - 50] new minted tokens to `to`
+func SendColoredTransaction(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]int64) (fail bool, txId string) {
+	var sentValue int64 = 50
+	var balanceList []*balance.Balance
+	sigScheme := signaturescheme.ED25519(*from.Seed().KeyPair(0))
+	inputAddr := from.Seed().Address(0)
+	outputAddr := to.Seed().Address(0)
+
+	// prepare inputs
+	resp, err := from.GetUnspentOutputs([]string{inputAddr.String()})
+	require.NoErrorf(t, err, "could not get unspent outputs on %s", from.String())
+
+	// abort if no unspent outputs
+	if len(resp.UnspentOutputs[0].OutputIDs) == 0 {
+		return true, ""
+	}
+
+	// calculate available token in the unspent output
+	var availableValue int64 = 0
+	for _, b := range resp.UnspentOutputs[0].OutputIDs[0].Balances {
+		availableValue += b.Value
+		balanceList = append(balanceList, balance.New(getColorFromString(b.Color), (-1)*b.Value))
+	}
+
+	// abort if not enough tokens
+	if availableValue < sentValue {
+		return true, ""
+	}
+
+	out, err := transaction.OutputIDFromBase58(resp.UnspentOutputs[0].OutputIDs[0].ID)
+	require.NoErrorf(t, err, "invalid unspent outputs ID on %s", from.String())
+	inputs := transaction.NewInputs([]transaction.OutputID{out}...)
+
+	// prepare outputs
+	outmap := map[address.Address][]*balance.Balance{}
+
+	// set balances
+	outmap[outputAddr] = []*balance.Balance{balance.New(balance.ColorIOTA, sentValue)}
+	if availableValue > sentValue {
+		outmap[outputAddr] = append(outmap[outputAddr], balance.New(balance.ColorNew, availableValue-sentValue))
+	}
+	outputs := transaction.NewOutputs(outmap)
+
+	// sign transaction
+	txn := transaction.New(inputs, outputs).Sign(sigScheme)
+
+	// send transaction
+	txId, err = from.SendTransaction(txn.Bytes())
+	require.NoErrorf(t, err, "could not send transaction on %s", from.String())
+
+	// update balance list
+	balanceList = append(balanceList, outmap[outputAddr]...)
+	updateBalanceList(addrBalance, balanceList, inputAddr.String(), outputAddr.String(), txId)
+
+	return false, txId
+}
+
+// updateBalanceList updates the token amount map with given peers and balances.
+// If the value of balance is negative, it is the balance to be deducted from peer from, else it is deposited to peer to.
+// If the color is balance.ColorNew, it should be recolored with txId.
+func updateBalanceList(addrBalance map[string]map[balance.Color]int64, balances []*balance.Balance, from, to, txId string) {
+	for _, b := range balances {
+		color := b.Color()
+		value := b.Value()
+		if value < 0 {
+			// deduct
+			addrBalance[from][color] += value
+			continue
+		}
+		// deposit
+		if color == balance.ColorNew {
+			addrBalance[to][getColorFromString(txId)] = value
+			continue
+		}
+		addrBalance[to][color] += value
+	}
+	return
+}
+
+func getColorFromString(colorStr string) (color balance.Color) {
+	if colorStr == "IOTA" {
+		color = balance.ColorIOTA
+	} else {
+		t, _ := transaction.IDFromBase58(colorStr)
+		color, _, _ = balance.ColorFromBytes(t.Bytes())
+	}
+	return
+}
+
+// CheckBalances performs checks to make sure that all peers have the same ledger state.
+func CheckBalances(t *testing.T, peers []*framework.Peer, addrBalance map[string]map[balance.Color]int64) {
+	for _, peer := range peers {
+		for addr, b := range addrBalance {
+			sum := make(map[balance.Color]int64)
+			resp, err := peer.GetUnspentOutputs([]string{addr})
+			require.NoError(t, err)
+			assert.Equal(t, addr, resp.UnspentOutputs[0].Address)
+
+			// calculate the balances of each colored coin
+			for _, unspents := range resp.UnspentOutputs[0].OutputIDs {
+				for _, respBalance := range unspents.Balances {
+					color := getColorFromString(respBalance.Color)
+					sum[color] += respBalance.Value
+				}
+			}
+
+			// check balances
+			for color, value := range sum {
+				assert.Equal(t, b[color], value)
+			}
+		}
+	}
+}
+
+// CheckTransactions performs checks to make sure that all peers have received all transactions .
+func CheckTransactions(t *testing.T, peers []*framework.Peer, transactionIDs []string, checkSynchronized bool) {
+	for _, peer := range peers {
+		if checkSynchronized {
+			// check that the peer sees itself as synchronized
+			info, err := peer.Info()
+			require.NoError(t, err)
+			require.True(t, info.Synced)
+		}
+
+		for _, txId := range transactionIDs {
+			resp, err := peer.GetTransactionByID(txId)
+			require.NoError(t, err)
+
+			// check inclusion state
+			assert.True(t, resp.InclusionState.Confirmed)
+			assert.False(t, resp.InclusionState.Rejected)
+		}
+	}
+}
+
 // ShutdownNetwork shuts down the network and reports errors.
 func ShutdownNetwork(t *testing.T, n Shutdowner) {
 	err := n.Shutdown()
diff --git a/tools/integration-tests/tester/tests/value/main_test.go b/tools/integration-tests/tester/tests/value/main_test.go
new file mode 100644
index 00000000..f5d00e21
--- /dev/null
+++ b/tools/integration-tests/tester/tests/value/main_test.go
@@ -0,0 +1,23 @@
+package value
+
+import (
+	"os"
+	"testing"
+
+	"github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework"
+)
+
+var f *framework.Framework
+
+// TestMain gets called by the test utility and is executed before any other test in this package.
+// It is therefore used to initialize the integration testing framework.
+func TestMain(m *testing.M) {
+	var err error
+	f, err = framework.Instance()
+	if err != nil {
+		panic(err)
+	}
+
+	// call the tests
+	os.Exit(m.Run())
+}
diff --git a/tools/integration-tests/tester/tests/value/value_test.go b/tools/integration-tests/tester/tests/value/value_test.go
new file mode 100644
index 00000000..a5fd1ea0
--- /dev/null
+++ b/tools/integration-tests/tester/tests/value/value_test.go
@@ -0,0 +1,122 @@
+package value
+
+import (
+	"testing"
+	"time"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers"
+	"github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests"
+	"github.com/stretchr/testify/require"
+)
+
+// TestTransactionPersistence issues messages on random peers, restarts them and checks for persistence after restart.
+func TestTransactionPersistence(t *testing.T) {
+	n, err := f.CreateNetwork("transaction_TestPersistence", 4, 2)
+	require.NoError(t, err)
+	defer tests.ShutdownNetwork(t, n)
+
+	// wait for peers to change their state to synchronized
+	time.Sleep(5 * time.Second)
+
+	// faucet node sends 100 IOTA tokens to all peers in the network
+	txIds, addrBalance := tests.SendTransactionFromFaucet(t, n.Peers(), 100)
+
+	// wait for messages to be gossiped
+	time.Sleep(2 * valuetransfers.AverageNetworkDelay)
+
+	// check whether the first issued transaction is available on all nodes, and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+
+	// send value message randomly
+	randomTxIds := tests.SendTransactionOnRandomPeer(t, n.Peers(), addrBalance, 10, 100)
+	txIds = append(txIds, randomTxIds...)
+
+	// wait for messages to be gossiped
+	time.Sleep(2 * valuetransfers.AverageNetworkDelay)
+
+	// check whether all issued transactions are available on all nodes and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+
+	// 3. stop all nodes
+	for _, peer := range n.Peers() {
+		err = peer.Stop()
+		require.NoError(t, err)
+	}
+
+	// 4. start all nodes
+	for _, peer := range n.Peers() {
+		err = peer.Start()
+		require.NoError(t, err)
+	}
+
+	// wait for peers to start
+	time.Sleep(20 * time.Second)
+
+	// check whether all issued transactions are available on all nodes and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// 5. check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+}
+
+// TestValueColoredPersistence issues colored tokens on random peers, restarts them and checks for persistence after restart.
+func TestValueColoredPersistence(t *testing.T) {
+	n, err := f.CreateNetwork("valueColor_TestPersistence", 4, 2)
+	require.NoError(t, err)
+	defer tests.ShutdownNetwork(t, n)
+
+	// wait for peers to change their state to synchronized
+	time.Sleep(5 * time.Second)
+
+	// master node sends funds to all peers in the network
+	txIds, addrBalance := tests.SendTransactionFromFaucet(t, n.Peers(), 100)
+
+	// wait for messages to be gossiped
+	time.Sleep(2 * valuetransfers.AverageNetworkDelay)
+
+	// check whether the transactions are available on all nodes, and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+
+	// send funds around
+	randomTxIds := tests.SendColoredTransactionOnRandomPeer(t, n.Peers(), addrBalance, 10)
+	txIds = append(txIds, randomTxIds...)
+
+	// wait for value messages to be gossiped
+	time.Sleep(2 * valuetransfers.AverageNetworkDelay)
+
+	// check whether all issued transactions are persistently available on all nodes, and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+
+	// stop all nodes
+	for _, peer := range n.Peers() {
+		err = peer.Stop()
+		require.NoError(t, err)
+	}
+
+	// start all nodes
+	for _, peer := range n.Peers() {
+		err = peer.Start()
+		require.NoError(t, err)
+	}
+
+	// wait for peers to start
+	time.Sleep(20 * time.Second)
+
+	// check whether all issued transactions are persistently available on all nodes, and confirmed
+	tests.CheckTransactions(t, n.Peers(), txIds, true)
+
+	// 5. check ledger state
+	tests.CheckBalances(t, n.Peers(), addrBalance)
+}
-- 
GitLab