Skip to content
Snippets Groups Projects
consensus_conflicts_test.go 7.98 KiB
package consensus

import (
	"log"
	"testing"
	"time"

	"github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework"

	"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/dapps/valuetransfers/packages/wallet"
	"github.com/iotaledger/goshimmer/plugins/webapi/value/utils"
	"github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests"
	"github.com/mr-tron/base58/base58"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestConsensusFiftyFiftyOpinionSplit spawns two network partitions with their own peers,
// then issues valid value objects spending the genesis in both, deletes the partitions (and lets them merge)
// and then checks that the conflicts are resolved via FPC.
func TestConsensusFiftyFiftyOpinionSplit(t *testing.T) {

	// override avg. network delay to accustom integration test slowness
	backupFCoBAvgNetworkDelay := framework.ParaFCoBAverageNetworkDelay
	backupBootstrapOnEveryNode := framework.ParaBootstrapOnEveryNode
	backupParaWaitToKill := framework.ParaWaitToKill
	framework.ParaFCoBAverageNetworkDelay = 90
	framework.ParaBootstrapOnEveryNode = true
	framework.ParaWaitToKill = 2 * framework.ParaFCoBAverageNetworkDelay

	// reset framework paras
	defer func() {
		framework.ParaFCoBAverageNetworkDelay = backupFCoBAvgNetworkDelay
		framework.ParaBootstrapOnEveryNode = backupBootstrapOnEveryNode
		framework.ParaWaitToKill = backupParaWaitToKill
	}()

	// create two partitions with their own peers
	n, err := f.CreateNetworkWithPartitions("abc", 6, 2, 2)
	require.NoError(t, err)
	defer tests.ShutdownNetwork(t, n)

	// split the network
	for i, partition := range n.Partitions() {
		log.Printf("partition %d peers:", i)
		for _, p := range partition.Peers() {
			log.Println(p.ID().String())
		}
	}

	// genesis wallet
	genesisSeedBytes, err := base58.Decode("7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih")
	require.NoError(t, err, "couldn't decode genesis seed from base58 seed")

	const genesisBalance = 1000000000
	genesisWallet := wallet.New(genesisSeedBytes)
	genesisAddr := genesisWallet.Seed().Address(0)
	genesisOutputID := transaction.NewOutputID(genesisAddr, transaction.GenesisID)

	// issue transactions which spend the same genesis output in all partitions
	conflictingTxs := make([]*transaction.Transaction, len(n.Partitions()))
	conflictingTxIDs := make([]string, len(n.Partitions()))
	receiverWallets := make([]*wallet.Wallet, len(n.Partitions()))
	for i, partition := range n.Partitions() {

		// create a new receiver wallet for the given partition
		partitionReceiverWallet := wallet.New()
		destAddr := partitionReceiverWallet.Seed().Address(0)
		receiverWallets[i] = partitionReceiverWallet
		tx := transaction.New(
			transaction.NewInputs(genesisOutputID),
			transaction.NewOutputs(map[address.Address][]*balance.Balance{
				destAddr: {
					{Value: genesisBalance, Color: balance.ColorIOTA},
				},
			}))
		tx = tx.Sign(signaturescheme.ED25519(*genesisWallet.Seed().KeyPair(0)))
		conflictingTxs[i] = tx

		// issue the transaction on the first peer of the partition
		issuerPeer := partition.Peers()[0]
		txID, err := issuerPeer.SendTransaction(tx.Bytes())
		conflictingTxIDs[i] = txID
		log.Printf("issued conflict transaction %s on partition %d on peer %s", txID, i, issuerPeer.ID().String())
		assert.NoError(t, err)

		// check that the transaction is actually available on all the peers of the partition
		missing, err := tests.AwaitTransactionAvailability(partition.Peers(), []string{txID}, 15*time.Second)
		if err != nil {
			assert.NoError(t, err, "transactions should have been available in partition")
			for p, missingOnPeer := range missing {
				log.Printf("missing on peer %s:", p)
				for missingTx := range missingOnPeer {
					log.Println("tx id: ", missingTx)
				}
			}
			return
		}

		require.NoError(t, err)
	}

	// sleep the avg. network delay so both partitions prefer their own first seen transaction
	log.Printf("waiting %d seconds avg. network delay to make the transactions "+
		"preferred in their corresponding partition", framework.ParaFCoBAverageNetworkDelay)
	time.Sleep(time.Duration(framework.ParaFCoBAverageNetworkDelay) * time.Second)

	// check that each partition is preferring its corresponding transaction
	log.Println("checking that each partition likes its corresponding transaction before the conflict:")
	for i, partition := range n.Partitions() {
		tests.CheckTransactions(t, partition.Peers(), map[string]*tests.ExpectedTransaction{
			conflictingTxIDs[i]: nil,
		}, true, tests.ExpectedInclusionState{
			Confirmed:   tests.False(),
			Finalized:   tests.False(),
			Conflicting: tests.False(),
			Solid:       tests.True(),
			Rejected:    tests.False(),
			Liked:       tests.True(),
			Preferred:   tests.True(),
		})
	}

	// merge back the partitions
	log.Println("merging partitions...")
	assert.NoError(t, n.DeletePartitions(), "merging the network partitions should work")
	log.Println("waiting for resolved partitions to autopeer to each other")
	err = n.WaitForAutopeering(4)
	require.NoError(t, err)

	// ensure message flow so that both partitions will get the conflicting tx
	for _, p := range n.Peers() {
		tests.SendDataMessage(t, p, []byte("DATA"), 10)
	}

	log.Println("waiting for transactions to be available on all peers...")
	missing, err := tests.AwaitTransactionAvailability(n.Peers(), conflictingTxIDs, 30*time.Second)
	if err != nil {
		assert.NoError(t, err, "transactions should have been available")
		for p, missingOnPeer := range missing {
			log.Printf("missing on peer %s:", p)
			for missingTx := range missingOnPeer {
				log.Println("tx id: ", missingTx)
			}
		}
		return
	}

	expectations := map[string]*tests.ExpectedTransaction{}
	for _, conflictingTx := range conflictingTxs {
		utilsTx := utils.ParseTransaction(conflictingTx)
		expectations[conflictingTx.ID().String()] = &tests.ExpectedTransaction{
			Inputs:    &utilsTx.Inputs,
			Outputs:   &utilsTx.Outputs,
			Signature: &utilsTx.Signature,
		}
	}

	// check that the transactions are marked as conflicting
	tests.CheckTransactions(t, n.Peers(), expectations, true, tests.ExpectedInclusionState{
		Finalized:   tests.False(),
		Conflicting: tests.True(),
		Solid:       tests.True(),
	})

	// wait until the voting has finalized
	log.Println("waiting for voting/transaction finalization to be done on all peers...")
	awaitFinalization := map[string]tests.ExpectedInclusionState{}
	for _, conflictingTx := range conflictingTxs {
		awaitFinalization[conflictingTx.ID().String()] = tests.ExpectedInclusionState{
			Finalized: tests.True(),
		}
	}
	err = tests.AwaitTransactionInclusionState(n.Peers(), awaitFinalization, 2*time.Minute)
	assert.NoError(t, err)

	// now all transactions must be finalized and at most one must be confirmed
	var confirmedOverConflictSet int
	for _, conflictingTx := range conflictingTxIDs {
		var rejected, confirmed int
		for _, p := range n.Peers() {
			tx, err := p.GetTransactionByID(conflictingTx)
			assert.NoError(t, err)
			if tx.InclusionState.Confirmed {
				confirmed++
				continue
			}
			if tx.InclusionState.Rejected {
				rejected++
			}
		}

		if rejected != 0 {
			assert.Len(t, n.Peers(), rejected, "the rejected count for %s should be equal to the amount of peers", conflictingTx)
		}
		if confirmed != 0 {
			assert.Len(t, n.Peers(), confirmed, "the confirmed count for %s should be equal to the amount of peers", conflictingTx)
			confirmedOverConflictSet++
		}

		assert.False(t, rejected == 0 && confirmed == 0, "a transaction must either be rejected or confirmed")
	}

	// there must only be one confirmed transaction out of the conflict set
	if confirmedOverConflictSet != 0 {
		assert.Equal(t, 1, confirmedOverConflictSet, "only one transaction can be confirmed out of the conflict set. %d of %d are confirmed", confirmedOverConflictSet, len(conflictingTxIDs))
	}
}