diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f1160f1cccc88bbb554416c0d7950ac0678c03fb..cd08c1e7c9f756728e30525aea4b0a67c0d0f6b0 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/Dockerfile b/Dockerfile index 0df4a2947139217273586781e007f62c9a9cb0a8..ea398c08f8f67d1d4564ecf03bb3d59133627a9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Build ############################ # golang:1.14.0-buster -FROM golang@sha256:fc7e7c9c4b0f6d2d5e8611ee73b9d1d3132750108878517bbf988aa772359ae4 AS build +FROM golang:1.14.4@sha256:afc8698cebd731cc3f98923ceb697093d378b9de7bd2e9365cb675f863660eea AS build # Ensure ca-certficates are up to date RUN update-ca-certificates diff --git a/dapps/valuetransfers/dapp.go b/dapps/valuetransfers/dapp.go index 7964e47159906f81943a9410a9af87b603daffc8..e929205d15da32a27021d5a2656ef750e6cf0617 100644 --- a/dapps/valuetransfers/dapp.go +++ b/dapps/valuetransfers/dapp.go @@ -1,6 +1,9 @@ package valuetransfers import ( + "github.com/iotaledger/goshimmer/plugins/config" + flag "github.com/spf13/pflag" + "os" "sync" "time" @@ -25,10 +28,21 @@ const ( // PluginName contains the human readable name of the plugin. PluginName = "ValueTransfers" - // AverageNetworkDelay contains the average time it takes for a network to propagate through gossip. - AverageNetworkDelay = 5 * time.Second + // DefaultAverageNetworkDelay contains the default average time it takes for a network to propagate through gossip. + DefaultAverageNetworkDelay = 5 * time.Second + + // CfgValueLayerSnapshotFile is the path to the snapshot file. + CfgValueLayerSnapshotFile = "valueLayer.snapshot.file" + + // CfgValueLayerFCOBAverageNetworkDelay is the avg. network delay to use for FCoB rules + CfgValueLayerFCOBAverageNetworkDelay = "valueLayer.fcob.averageNetworkDelay" ) +func init() { + flag.String(CfgValueLayerSnapshotFile, "", "the path to the snapshot file") + flag.Int(CfgValueLayerFCOBAverageNetworkDelay, 5, "the avg. network delay to use for FCoB rules") +} + var ( // App is the "plugin" instance of the value-transfers application. App = node.NewPlugin(PluginName, node.Enabled, configure, run) @@ -58,6 +72,22 @@ func configure(_ *node.Plugin) { // configure Tangle Tangle = tangle.New(database.Store()) + + // read snapshot file + snapshotFilePath := config.Node.GetString(CfgValueLayerSnapshotFile) + if len(snapshotFilePath) != 0 { + snapshot := tangle.Snapshot{} + f, err := os.Open(snapshotFilePath) + if err != nil { + log.Panic("can not open snapshot file:", err) + } + if _, err := snapshot.ReadFrom(f); err != nil { + log.Panic("could not read snapshot file:", err) + } + Tangle.LoadSnapshot(snapshot) + log.Info("read snapshot from %s", snapshotFilePath) + } + Tangle.Events.Error.Attach(events.NewClosure(func(err error) { log.Error(err) })) @@ -76,7 +106,9 @@ func configure(_ *node.Plugin) { })) // configure FCOB consensus rules - FCOB = consensus.NewFCOB(Tangle, AverageNetworkDelay) + cfgAvgNetworkDelay := config.Node.GetInt(CfgValueLayerFCOBAverageNetworkDelay) + log.Infof("avg. network delay configured to %d seconds", cfgAvgNetworkDelay) + FCOB = consensus.NewFCOB(Tangle, time.Duration(cfgAvgNetworkDelay)*time.Second) FCOB.Events.Vote.Attach(events.NewClosure(func(id string, initOpn vote.Opinion) { if err := voter.Vote(id, initOpn); err != nil { log.Error(err) diff --git a/dapps/valuetransfers/packages/balance/balance.go b/dapps/valuetransfers/packages/balance/balance.go index 142b95f595219b08f45b2171dc2932d4076caf57..6559145b2b172898e42831a9b31e9bda99520804 100644 --- a/dapps/valuetransfers/packages/balance/balance.go +++ b/dapps/valuetransfers/packages/balance/balance.go @@ -8,15 +8,17 @@ import ( // Balance represents a balance in the IOTA ledger. It consists out of a numeric value and a color. type Balance struct { - value int64 - color Color + // The numeric value of the balance. + Value int64 `json:"value"` + // The color of the balance. + Color Color `json:"color"` } // New creates a new Balance with the given details. func New(color Color, balance int64) (result *Balance) { result = &Balance{ - color: color, - value: balance, + Color: color, + Value: balance, } return @@ -28,7 +30,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) { marshalUtil := marshalutil.New(bytes) - result.value, err = marshalUtil.ReadInt64() + result.Value, err = marshalUtil.ReadInt64() if err != nil { return } @@ -40,7 +42,7 @@ func FromBytes(bytes []byte) (result *Balance, consumedBytes int, err error) { return nil, marshalUtil.ReadOffset(), colorErr } - result.color = coinColor.(Color) + result.Color = coinColor.(Color) consumedBytes = marshalUtil.ReadOffset() return @@ -56,29 +58,19 @@ func Parse(marshalUtil *marshalutil.MarshalUtil) (*Balance, error) { return address.(*Balance), nil } -// Value returns the numeric value of the balance. -func (balance *Balance) Value() int64 { - return balance.value -} - -// Color returns the Color of the balance. -func (balance *Balance) Color() Color { - return balance.color -} - // Bytes marshals the Balance into a sequence of bytes. func (balance *Balance) Bytes() []byte { marshalUtil := marshalutil.New(Length) - marshalUtil.WriteInt64(balance.value) - marshalUtil.WriteBytes(balance.color.Bytes()) + marshalUtil.WriteInt64(balance.Value) + marshalUtil.WriteBytes(balance.Color.Bytes()) return marshalUtil.Bytes() } // String creates a human readable string of the Balance. func (balance *Balance) String() string { - return strconv.FormatInt(balance.value, 10) + " " + balance.color.String() + return strconv.FormatInt(balance.Value, 10) + " " + balance.Color.String() } // Length encodes the length of a marshaled Balance (the length of the color + 8 bytes for the balance). diff --git a/dapps/valuetransfers/packages/balance/balance_test.go b/dapps/valuetransfers/packages/balance/balance_test.go index 5dd98567cf634ec3ad3bd8ede4a42ed6d0ab4932..35401c66a5eb5bed4ace5281f4ff1de1e289feb7 100644 --- a/dapps/valuetransfers/packages/balance/balance_test.go +++ b/dapps/valuetransfers/packages/balance/balance_test.go @@ -8,8 +8,8 @@ import ( func TestMarshalUnmarshal(t *testing.T) { balance := New(ColorIOTA, 1337) - assert.Equal(t, int64(1337), balance.Value()) - assert.Equal(t, ColorIOTA, balance.Color()) + assert.Equal(t, int64(1337), balance.Value) + assert.Equal(t, ColorIOTA, balance.Color) marshaledBalance := balance.Bytes() assert.Equal(t, Length, len(marshaledBalance)) @@ -19,6 +19,6 @@ func TestMarshalUnmarshal(t *testing.T) { panic(err) } assert.Equal(t, Length, consumedBytes) - assert.Equal(t, balance.value, restoredBalance.Value()) - assert.Equal(t, balance.color, restoredBalance.Color()) + assert.Equal(t, balance.Value, restoredBalance.Value) + assert.Equal(t, balance.Color, restoredBalance.Color) } diff --git a/dapps/valuetransfers/packages/consensus/fcob.go b/dapps/valuetransfers/packages/consensus/fcob.go index d5b86548b57dc8c2eeb850a6ab5d7aa6cd564e44..1d2a98aa55f4a4299b015386b24a7bdf1f33e7fb 100644 --- a/dapps/valuetransfers/packages/consensus/fcob.go +++ b/dapps/valuetransfers/packages/consensus/fcob.go @@ -3,6 +3,7 @@ package consensus import ( "time" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/hive.go/events" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" @@ -138,9 +139,10 @@ func (fcob *FCOB) setFinalized(cachedTransactionMetadata *tangle.CachedTransacti // onFork triggers a voting process whenever a Transaction gets forked into a new Branch. The initial opinion is derived // from the preferred flag that was set using the FCOB rule. -func (fcob *FCOB) onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata) { +func (fcob *FCOB) onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *tangle.CachedTransactionMetadata, cachedTargetBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) { defer cachedTransaction.Release() defer cachedTransactionMetadata.Release() + defer cachedTargetBranch.Release() transactionMetadata := cachedTransactionMetadata.Unwrap() if transactionMetadata == nil { diff --git a/dapps/valuetransfers/packages/tangle/ledgerstate.go b/dapps/valuetransfers/packages/tangle/ledgerstate.go index 5c0d8c31a7b7bd0c93dcb01aaa29c35719806c22..9b676acfedfebbf7a417bfc8d63c0bf24bdc8991 100644 --- a/dapps/valuetransfers/packages/tangle/ledgerstate.go +++ b/dapps/valuetransfers/packages/tangle/ledgerstate.go @@ -25,7 +25,7 @@ func (ledgerState *LedgerState) Balances(address address.Address) (coloredBalanc ledgerState.tangle.OutputsOnAddress(address).Consume(func(output *Output) { if output.ConsumerCount() == 0 { for _, coloredBalance := range output.Balances() { - coloredBalances[coloredBalance.Color()] += coloredBalance.Value() + coloredBalances[coloredBalance.Color] += coloredBalance.Value } } }) diff --git a/dapps/valuetransfers/packages/tangle/snapshot.go b/dapps/valuetransfers/packages/tangle/snapshot.go new file mode 100644 index 0000000000000000000000000000000000000000..b13df4d81409167068c882c8c893767c030cfcaa --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/snapshot.go @@ -0,0 +1,125 @@ +package tangle + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// Snapshot defines a snapshot of the ledger state. +type Snapshot map[transaction.ID]map[address.Address][]*balance.Balance + +// WriteTo writes the snapshot data to the given writer in the following format: +// transaction_count(int64) +// -> transaction_count * transaction_id(32byte) +// ->address_count(int64) +// ->address_count * address(33byte) +// ->balance_count(int64) +// ->balance_count * value(int64)+color(32byte) +func (s Snapshot) WriteTo(writer io.Writer) (int64, error) { + var bytesWritten int64 + transactionCount := len(s) + if err := binary.Write(writer, binary.LittleEndian, int64(transactionCount)); err != nil { + return 0, fmt.Errorf("unable to write transactions count: %w", err) + } + bytesWritten += 8 + for txID, addresses := range s { + if err := binary.Write(writer, binary.LittleEndian, txID); err != nil { + return bytesWritten, fmt.Errorf("unable to write transaction ID: %w", err) + } + bytesWritten += transaction.IDLength + if err := binary.Write(writer, binary.LittleEndian, int64(len(addresses))); err != nil { + return bytesWritten, fmt.Errorf("unable to write address count: %w", err) + } + bytesWritten += 8 + for addr, balances := range addresses { + if err := binary.Write(writer, binary.LittleEndian, addr); err != nil { + return bytesWritten, fmt.Errorf("unable to write address: %w", err) + } + bytesWritten += address.Length + if err := binary.Write(writer, binary.LittleEndian, int64(len(balances))); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance count: %w", err) + } + bytesWritten += 8 + for _, bal := range balances { + if err := binary.Write(writer, binary.LittleEndian, bal.Value); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance value: %w", err) + } + bytesWritten += 8 + if err := binary.Write(writer, binary.LittleEndian, bal.Color); err != nil { + return bytesWritten, fmt.Errorf("unable to write balance color: %w", err) + } + bytesWritten += balance.ColorLength + } + } + } + + return bytesWritten, nil +} + +// ReadFrom reads the snapshot bytes from the given reader. +// This function overrides existing content of the snapshot. +func (s Snapshot) ReadFrom(reader io.Reader) (int64, error) { + var bytesRead int64 + var transactionCount int64 + if err := binary.Read(reader, binary.LittleEndian, &transactionCount); err != nil { + return 0, fmt.Errorf("unable to read transaction count: %w", err) + } + bytesRead += 8 + + var i int64 + for ; i < transactionCount; i++ { + txIDBytes := make([]byte, transaction.IDLength) + if err := binary.Read(reader, binary.LittleEndian, txIDBytes); err != nil { + return bytesRead, fmt.Errorf("unable to read transaction ID: %w", err) + } + bytesRead += transaction.IDLength + var addrCount int64 + if err := binary.Read(reader, binary.LittleEndian, &addrCount); err != nil { + return bytesRead, fmt.Errorf("unable to read address count: %w", err) + } + bytesRead += 8 + txAddrMap := make(map[address.Address][]*balance.Balance, addrCount) + var j int64 + for ; j < addrCount; j++ { + addrBytes := make([]byte, address.Length) + if err := binary.Read(reader, binary.LittleEndian, addrBytes); err != nil { + return bytesRead, fmt.Errorf("unable to read address: %w", err) + } + bytesRead += address.Length + var balanceCount int64 + if err := binary.Read(reader, binary.LittleEndian, &balanceCount); err != nil { + return bytesRead, fmt.Errorf("unable to read balance count: %w", err) + } + bytesRead += 8 + + balances := make([]*balance.Balance, balanceCount) + var k int64 + for ; k < balanceCount; k++ { + var value int64 + if err := binary.Read(reader, binary.LittleEndian, &value); err != nil { + return bytesRead, fmt.Errorf("unable to read balance value: %w", err) + } + bytesRead += 8 + color := balance.Color{} + if err := binary.Read(reader, binary.LittleEndian, &color); err != nil { + return bytesRead, fmt.Errorf("unable to read balance color: %w", err) + } + bytesRead += balance.ColorLength + balances[k] = &balance.Balance{Value: value, Color: color} + } + addr := address.Address{} + copy(addr[:], addrBytes) + txAddrMap[addr] = balances + } + txID := transaction.ID{} + copy(txID[:], txIDBytes) + s[txID] = txAddrMap + } + + return bytesRead, nil +} diff --git a/dapps/valuetransfers/packages/tangle/snapshot_test.go b/dapps/valuetransfers/packages/tangle/snapshot_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7ef8b153864d90fdfebd5034ec22ab676f87f6bb --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/snapshot_test.go @@ -0,0 +1,88 @@ +package tangle + +import ( + "bytes" + "testing" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + "github.com/iotaledger/hive.go/kvstore/mapdb" + "github.com/stretchr/testify/assert" +) + +func TestLoadSnapshot(t *testing.T) { + tangle := New(mapdb.NewMapDB()) + + snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{ + transaction.GenesisID: { + address.Random(): []*balance.Balance{ + balance.New(balance.ColorIOTA, 337), + }, + + address.Random(): []*balance.Balance{ + balance.New(balance.ColorIOTA, 1000), + balance.New(balance.ColorIOTA, 1000), + }, + }, + } + tangle.LoadSnapshot(snapshot) + + // check whether outputs can be retrieved from tangle + for addr, balances := range snapshot[transaction.GenesisID] { + cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID)) + cachedOutput.Consume(func(output *Output) { + assert.Equal(t, addr, output.Address()) + assert.ElementsMatch(t, balances, output.Balances()) + assert.True(t, output.Solid()) + assert.Equal(t, branchmanager.MasterBranchID, output.BranchID()) + }) + } +} + +func TestSnapshotMarshalUnmarshal(t *testing.T) { + const genesisBalance = 1000000000 + seed := wallet.NewSeed() + genesisAddr := seed.Address(GENESIS) + + snapshot := Snapshot{ + transaction.GenesisID: { + genesisAddr: { + balance.New(balance.ColorIOTA, genesisBalance), + }, + }, + } + + // includes txs count + const int64ByteSize = 8 + expectedLength := int64ByteSize + for _, addresses := range snapshot { + // tx id + expectedLength += transaction.IDLength + // addr count + expectedLength += int64ByteSize + for _, balances := range addresses { + // addr + expectedLength += address.Length + // balance count + expectedLength += int64ByteSize + // balances + expectedLength += len(balances) * (int64ByteSize + balance.ColorLength) + } + } + + var buf bytes.Buffer + written, err := snapshot.WriteTo(&buf) + assert.NoError(t, err, "writing the snapshot to the buffer should succeed") + assert.EqualValues(t, expectedLength, written, "written byte count should match the expected count") + + snapshotFromBytes := Snapshot{} + read, err := snapshotFromBytes.ReadFrom(&buf) + assert.NoError(t, err, "expected no error from reading valid snapshot bytes") + assert.EqualValues(t, expectedLength, read, "read byte count should match the expected count") + + // check that the source and unmarshaled snapshot are equivalent + assert.Equal(t, snapshot, snapshotFromBytes) +} diff --git a/dapps/valuetransfers/packages/tangle/tangle.go b/dapps/valuetransfers/packages/tangle/tangle.go index fd1d4e377d036985f9c2c396c15eae176ce3120a..cdd0c16c6206af4b113d4874b5634ae4a966be7e 100644 --- a/dapps/valuetransfers/packages/tangle/tangle.go +++ b/dapps/valuetransfers/packages/tangle/tangle.go @@ -1309,8 +1309,8 @@ func (tangle *Tangle) bookTransaction(cachedTransaction *transaction.CachedTrans // create correctly colored balances (replacing color of newly minted coins with color of transaction id) coloredBalances := make([]*balance.Balance, len(balances)) for i, currentBalance := range balances { - if currentBalance.Color() == balance.ColorNew { - coloredBalances[i] = balance.New(mintedColor, currentBalance.Value()) + if currentBalance.Color == balance.ColorNew { + coloredBalances[i] = balance.New(mintedColor, currentBalance.Value) } else { coloredBalances[i] = currentBalance } @@ -1527,9 +1527,9 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction) // calculate the input balances for _, inputBalance := range input.Balances() { var newBalance int64 - if currentBalance, balanceExists := consumedBalances[inputBalance.Color()]; balanceExists { + if currentBalance, balanceExists := consumedBalances[inputBalance.Color]; balanceExists { // check overflows in the numbers - if inputBalance.Value() > math.MaxInt64-currentBalance { + if inputBalance.Value > math.MaxInt64-currentBalance { // TODO: make it an explicit error var err = fmt.Errorf("buffer overflow in balances of inputs") @@ -1538,11 +1538,11 @@ func (tangle *Tangle) retrieveConsumedInputDetails(tx *transaction.Transaction) return } - newBalance = currentBalance + inputBalance.Value() + newBalance = currentBalance + inputBalance.Value } else { - newBalance = inputBalance.Value() + newBalance = inputBalance.Value } - consumedBalances[inputBalance.Color()] = newBalance + consumedBalances[inputBalance.Color] = newBalance } } inputsSolid = true @@ -1563,51 +1563,51 @@ func (tangle *Tangle) checkTransactionOutputs(inputBalances map[balance.Color]in aborted := !outputs.ForEach(func(address address.Address, balances []*balance.Balance) bool { for _, outputBalance := range balances { // abort if the output creates a negative or empty output - if outputBalance.Value() <= 0 { + if outputBalance.Value <= 0 { return false } // sidestep logic if we have a newly colored output (we check the supply later) - if outputBalance.Color() == balance.ColorNew { + if outputBalance.Color == balance.ColorNew { // catch overflows - if newlyColoredCoins > math.MaxInt64-outputBalance.Value() { + if newlyColoredCoins > math.MaxInt64-outputBalance.Value { return false } - newlyColoredCoins += outputBalance.Value() + newlyColoredCoins += outputBalance.Value continue } // sidestep logic if we have ColorIOTA - if outputBalance.Color() == balance.ColorIOTA { + if outputBalance.Color == balance.ColorIOTA { // catch overflows - if uncoloredCoins > math.MaxInt64-outputBalance.Value() { + if uncoloredCoins > math.MaxInt64-outputBalance.Value { return false } - uncoloredCoins += outputBalance.Value() + uncoloredCoins += outputBalance.Value continue } // check if the used color does not exist in our supply - availableBalance, spentColorExists := inputBalances[outputBalance.Color()] + availableBalance, spentColorExists := inputBalances[outputBalance.Color] if !spentColorExists { return false } // abort if we spend more coins of the given color than we have - if availableBalance < outputBalance.Value() { + if availableBalance < outputBalance.Value { return false } // subtract the spent coins from the supply of this color - inputBalances[outputBalance.Color()] -= outputBalance.Value() + inputBalances[outputBalance.Color] -= outputBalance.Value // cleanup empty map entries (we have exhausted our funds) - if inputBalances[outputBalance.Color()] == 0 { - delete(inputBalances, outputBalance.Color()) + if inputBalances[outputBalance.Color] == 0 { + delete(inputBalances, outputBalance.Color) } } @@ -1907,4 +1907,4 @@ func (stackEntry *valuePayloadPropagationStackEntry) Unwrap() (payload *payload. transactionMetadata = stackEntry.CachedTransactionMetadata.Unwrap() return -} +} \ No newline at end of file diff --git a/dapps/valuetransfers/packages/tangle/tangle_test.go b/dapps/valuetransfers/packages/tangle/tangle_test.go index dab6589cc4d360d0fa88d0651f74a08000749a05..598a25c473cb68698f061a2cc90bb2e2018c2f56 100644 --- a/dapps/valuetransfers/packages/tangle/tangle_test.go +++ b/dapps/valuetransfers/packages/tangle/tangle_test.go @@ -865,35 +865,6 @@ func TestGetCachedOutputsFromTransactionInputs(t *testing.T) { } } -func TestLoadSnapshot(t *testing.T) { - tangle := New(mapdb.NewMapDB()) - - snapshot := map[transaction.ID]map[address.Address][]*balance.Balance{ - transaction.GenesisID: { - address.Random(): []*balance.Balance{ - balance.New(balance.ColorIOTA, 337), - }, - - address.Random(): []*balance.Balance{ - balance.New(balance.ColorIOTA, 1000), - balance.New(balance.ColorIOTA, 1000), - }, - }, - } - tangle.LoadSnapshot(snapshot) - - // check whether outputs can be retrieved from tangle - for addr, balances := range snapshot[transaction.GenesisID] { - cachedOutput := tangle.TransactionOutput(transaction.NewOutputID(addr, transaction.GenesisID)) - cachedOutput.Consume(func(output *Output) { - assert.Equal(t, addr, output.Address()) - assert.ElementsMatch(t, balances, output.Balances()) - assert.True(t, output.Solid()) - assert.Equal(t, branchmanager.MasterBranchID, output.BranchID()) - }) - } -} - func TestRetrieveConsumedInputDetails(t *testing.T) { // test simple happy case { @@ -1525,7 +1496,7 @@ func sumOutputsByColor(outputs map[address.Address][]*balance.Balance) map[balan for _, balances := range outputs { for _, bal := range balances { - totals[bal.Color()] += bal.Value() + totals[bal.Color] += bal.Value } } @@ -1547,4 +1518,4 @@ func createDummyTransaction() *transaction.Transaction { }, }), ) -} +} \ No newline at end of file diff --git a/go.mod b/go.mod index 5c30e344e36a413a767041c4717b3deeb2c7b0cb..07ea533ab1327f151744785a703ab5bac400eb90 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/gobuffalo/packr/v2 v2.7.1 github.com/golang/protobuf v1.3.5 - github.com/google/go-cmp v0.4.0 + github.com/google/go-cmp v0.4.1 github.com/gorilla/websocket v1.4.1 github.com/iotaledger/hive.go v0.0.0-20200610104211-d603429af242 github.com/iotaledger/iota.go v1.0.0-beta.14 @@ -24,7 +24,7 @@ require ( github.com/prometheus/client_golang v1.5.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.2 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 github.com/valyala/fasttemplate v1.1.0 // indirect go.dedis.ch/kyber/v3 v3.0.12 go.mongodb.org/mongo-driver v1.0.0 diff --git a/go.sum b/go.sum index df56ca18c2bbb77b90e0e96ed7de352fc3d212ea..1b9f1a3f7b9ef9aa472c784ac14d6645fb5bdab3 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -316,6 +318,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -519,6 +523,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= diff --git a/pluginmgr/core/plugins.go b/pluginmgr/core/plugins.go index 1eba3f117348d559b68e2f677c8789f23f58a082..eb4ff3b619c6ea1cd48a61d4325d54ddf5dd7241 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/autopeering/parameters.go b/plugins/autopeering/parameters.go index 01022ef352e2ae927b2841f399dd20c8a366607f..0cd2088fb04e0ace1c5dd58df51241a8a252c512 100644 --- a/plugins/autopeering/parameters.go +++ b/plugins/autopeering/parameters.go @@ -7,8 +7,12 @@ import ( const ( // CfgEntryNodes defines the config flag of the entry nodes. CfgEntryNodes = "autopeering.entryNodes" + + // CfgOutboundUpdateIntervalMs time after which out neighbors are updated. + CfgOutboundUpdateIntervalMs = "autopeering.outboundUpdateIntervalMs" ) func init() { flag.StringSlice(CfgEntryNodes, []string{"V8LYtWWcPYYDTTXLeIEFjJEuWlsjDiI0+Pq/Cx9ai6g=@116.202.49.178:14626"}, "list of trusted entry nodes for auto peering") + flag.Int(CfgOutboundUpdateIntervalMs, 10, "time after which out neighbors are updated") } diff --git a/plugins/config/plugin.go b/plugins/config/plugin.go index ab43e6054cd4bfaff9aa9ceb373f80e75644b234..da882233e400356dfeffae261fb0e500f37453a4 100644 --- a/plugins/config/plugin.go +++ b/plugins/config/plugin.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "strings" "github.com/iotaledger/hive.go/events" "github.com/iotaledger/hive.go/node" @@ -56,6 +57,12 @@ func init() { // It automatically reads in a single config file starting with "config" (can be changed via the --config CLI flag) // and ending with: .json, .toml, .yaml or .yml (in this sequence). func fetch(printConfig bool, ignoreSettingsAtPrint ...[]string) error { + // replace dots with underscores in env + dotReplacer := strings.NewReplacer(".", "_") + Node.SetEnvKeyReplacer(dotReplacer) + // read in ENV variables + Node.AutomaticEnv() + flag.Parse() err := parameter.LoadConfigFile(Node, *configDirPath, *configName, true, *skipConfigAvailable) if err != nil { diff --git a/plugins/dashboard/payload_handler.go b/plugins/dashboard/payload_handler.go index db8ab642269e0f7db2f9161de2a62dbf4d2bb23b..6ffa3593a9927bdeb97c61f3de2d74d05c3d76b4 100644 --- a/plugins/dashboard/payload_handler.go +++ b/plugins/dashboard/payload_handler.go @@ -135,8 +135,8 @@ func processValuePayload(p payload.Payload) (vp ValuePayload) { var b []Balance for _, balance := range balances { b = append(b, Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } t := OutputContent{ diff --git a/plugins/testsnapshots/plugin.go b/plugins/testsnapshots/plugin.go new file mode 100644 index 0000000000000000000000000000000000000000..2e47c4c02938840a68a6d401ab6b745a1f97adb7 --- /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/tangle" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/hive.go/logger" + "github.com/iotaledger/hive.go/node" +) + +const ( + // PluginName is the plugin name of the bootstrap plugin. + PluginName = "TestSnapshots" +) + +var ( + // Plugin is the plugin instance of the bootstrap 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(tangle.Snapshot{ + transaction.GenesisID: { + address0: []*balance.Balance{ + balance.New(balance.ColorIOTA, 10000000), + }, + }, + }) + + log.Infof("load snapshots to tangle") +} + +func run(_ *node.Plugin) {} diff --git a/plugins/webapi/value/gettransactionbyid/handler.go b/plugins/webapi/value/gettransactionbyid/handler.go index df4791aba9deab3ff23bd74334a2f173d5d3f7da..b4bee704b621f56a4e5afdb2b34cc93dc40cc4dd 100644 --- a/plugins/webapi/value/gettransactionbyid/handler.go +++ b/plugins/webapi/value/gettransactionbyid/handler.go @@ -7,40 +7,40 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" "github.com/labstack/echo" - "github.com/labstack/gommon/log" ) // Handler gets the transaction by id. func Handler(c echo.Context) error { txnID, err := transaction.IDFromBase58(c.QueryParam("txnID")) if err != nil { - log.Info(err) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } // get txn by txn id - txnObj := valuetransfers.Tangle.Transaction(txnID) - defer txnObj.Release() - if !txnObj.Exists() { + cachedTxnMetaObj := valuetransfers.Tangle.TransactionMetadata(txnID) + defer cachedTxnMetaObj.Release() + if !cachedTxnMetaObj.Exists() { return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) } - txn := utils.ParseTransaction(txnObj.Unwrap()) - - // get txn metadata - txnMetadataObj := valuetransfers.Tangle.TransactionMetadata(txnID) - defer txnMetadataObj.Release() - if !txnMetadataObj.Exists() { - return c.JSON(http.StatusNotFound, Response{Error: "Transaction Metadata not found"}) + cachedTxnObj := valuetransfers.Tangle.Transaction(txnID) + defer cachedTxnObj.Release() + if !cachedTxnObj.Exists() { + return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) } - txnMetadata := txnMetadataObj.Unwrap() + txn := utils.ParseTransaction(cachedTxnObj.Unwrap()) + txnMeta := cachedTxnMetaObj.Unwrap() + txnMeta.Preferred() return c.JSON(http.StatusOK, Response{ Transaction: txn, InclusionState: utils.InclusionState{ - Solid: txnMetadata.Solid(), - Confirmed: txnMetadata.Confirmed(), - Rejected: txnMetadata.Rejected(), - Liked: txnMetadata.Liked(), + Confirmed: txnMeta.Confirmed(), + Conflicting: txnMeta.Conflicting(), + Liked: txnMeta.Liked(), + Solid: txnMeta.Solid(), + Rejected: txnMeta.Rejected(), + Finalized: txnMeta.Finalized(), + Preferred: txnMeta.Preferred(), }, }) } diff --git a/plugins/webapi/value/sendtransaction/handler.go b/plugins/webapi/value/sendtransaction/handler.go index b1dfa63d99ed7f3b85b4ac1432f4e9e4ab1ecc61..8a65571533c113d60d7a74a9e7108acaa4e34d65 100644 --- a/plugins/webapi/value/sendtransaction/handler.go +++ b/plugins/webapi/value/sendtransaction/handler.go @@ -7,21 +7,18 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" "github.com/iotaledger/goshimmer/plugins/issuer" "github.com/labstack/echo" - "github.com/labstack/gommon/log" ) // Handler sends a transaction. func Handler(c echo.Context) error { var request Request if err := c.Bind(&request); err != nil { - log.Info(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } // prepare transaction tx, _, err := transaction.FromBytes(request.TransactionBytes) if err != nil { - log.Info(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } @@ -29,7 +26,6 @@ func Handler(c echo.Context) error { payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) _, err = issuer.IssuePayload(payload) if err != nil { - log.Info(err.Error()) return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) } diff --git a/plugins/webapi/value/unspentoutputs/handler.go b/plugins/webapi/value/unspentoutputs/handler.go index 738c278ff426654e75f9571c8df2a5e18fe0860c..05be4eb55ba1d901e0f3544cdec998fcef66c9a8 100644 --- a/plugins/webapi/value/unspentoutputs/handler.go +++ b/plugins/webapi/value/unspentoutputs/handler.go @@ -28,29 +28,38 @@ func Handler(c echo.Context) error { outputids := make([]OutputID, 0) // get outputids by address - for id, outputObj := range valuetransfers.Tangle.OutputsOnAddress(address) { - defer outputObj.Release() - output := outputObj.Unwrap() + for id, cachedOutput := range valuetransfers.Tangle.OutputsOnAddress(address) { + // TODO: don't do this in a for + defer cachedOutput.Release() + output := cachedOutput.Unwrap() + cachedTxMeta := valuetransfers.Tangle.TransactionMetadata(output.TransactionID()) + // TODO: don't do this in a for + defer cachedTxMeta.Release() if output.ConsumerCount() == 0 { // iterate balances var b []utils.Balance for _, balance := range output.Balances() { b = append(b, utils.Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } + inclusionState := utils.InclusionState{} + if cachedTxMeta.Exists() { + txMeta := cachedTxMeta.Unwrap() + inclusionState.Confirmed = txMeta.Confirmed() + inclusionState.Liked = txMeta.Liked() + inclusionState.Rejected = txMeta.Rejected() + inclusionState.Finalized = txMeta.Finalized() + inclusionState.Conflicting = txMeta.Conflicting() + inclusionState.Confirmed = txMeta.Confirmed() + } outputids = append(outputids, OutputID{ - ID: id.String(), - Balances: b, - InclusionState: utils.InclusionState{ - Solid: output.Solid(), - Confirmed: output.Confirmed(), - Rejected: output.Rejected(), - Liked: output.Liked(), - }, + ID: id.String(), + Balances: b, + InclusionState: inclusionState, }) } } diff --git a/plugins/webapi/value/utils/transaction_handler.go b/plugins/webapi/value/utils/transaction_handler.go index 359446a258072f14091c027ea639d10bb84ef675..9664c52df05f7f9ec2a7c975b9f6fa0ab6cd8fe8 100644 --- a/plugins/webapi/value/utils/transaction_handler.go +++ b/plugins/webapi/value/utils/transaction_handler.go @@ -21,8 +21,8 @@ func ParseTransaction(t *transaction.Transaction) (txn Transaction) { var b []Balance for _, balance := range balances { b = append(b, Balance{ - Value: balance.Value(), - Color: balance.Color().String(), + Value: balance.Value, + Color: balance.Color.String(), }) } t := Output{ @@ -64,8 +64,11 @@ type Balance struct { // InclusionState represents the different states of an OutputID type InclusionState struct { - Solid bool `json:"solid,omitempty"` - Confirmed bool `json:"confirmed,omitempty"` - Rejected bool `json:"rejected,omitempty"` - Liked bool `json:"liked,omitempty"` + Solid bool `json:"solid,omitempty"` + Confirmed bool `json:"confirmed,omitempty"` + Rejected bool `json:"rejected,omitempty"` + Liked bool `json:"liked,omitempty"` + Conflicting bool `json:"conflicting,omitempty"` + Finalized bool `json:"finalized,omitempty"` + Preferred bool `json:"preferred,omitempty"` } diff --git a/tools/docker-network/docker-compose.yml b/tools/docker-network/docker-compose.yml index e51aa9af1d6faac82e6d7fc939dd985212c22f57..25b9fd0387f2adc489a9a096bc7f19a56905f04a 100644 --- a/tools/docker-network/docker-compose.yml +++ b/tools/docker-network/docker-compose.yml @@ -40,10 +40,12 @@ services: command: > --config-dir=/tmp --database.directory=/tmp/mainnetdb + --valueLayer.snapshot.file=/tmp/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin --node.enablePlugins=bootstrap volumes: - ./config.docker.json:/tmp/config.json:ro - goshimmer-cache:/go + - ../integration-tests/assets:/tmp/assets ports: - "127.0.0.1:8080:8080/tcp" # web API - "127.0.0.1:8081:8081/tcp" # dashboard @@ -57,10 +59,12 @@ services: --config-dir=/tmp --database.directory=/tmp/mainnetdb --node.enablePlugins=bootstrap + --valueLayer.snapshot.file=/tmp/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin --node.disablePlugins=dashboard,portcheck volumes: - ./config.docker.json:/tmp/config.json:ro - goshimmer-cache:/go + - ../integration-tests/assets:/tmp/assets expose: - "8080/tcp" # web API (within Docker network) depends_on: diff --git a/tools/double-spend/double-spend.go b/tools/double-spend/double-spend.go new file mode 100644 index 0000000000000000000000000000000000000000..ce29625f6bd1e0808ba19b2d0b56c4302785aafe --- /dev/null +++ b/tools/double-spend/double-spend.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/iotaledger/goshimmer/client" + "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/mr-tron/base58" +) + +func main() { + + client := client.NewGoShimmerAPI("http://ressims.iota.cafe:8080", http.Client{Timeout: 30 * time.Second}) + + // genesis wallet + genesisSeedBytes, err := base58.Decode("7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih") + if err != nil { + fmt.Println(err) + } + + 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, 2) + conflictingTxIDs := make([]string, 2) + receiverWallets := make([]*wallet.Wallet, 2) + for i := range conflictingTxs { + + // create a new receiver wallet for the given conflict + receiverWallet := wallet.New() + destAddr := receiverWallet.Seed().Address(0) + receiverWallets[i] = receiverWallet + 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 + txID, err := client.SendTransaction(tx.Bytes()) + if err != nil { + fmt.Println(err) + } + conflictingTxIDs[i] = txID + fmt.Printf("issued conflict transaction %s\n", txID) + } +} diff --git a/tools/integration-tests/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin b/tools/integration-tests/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin new file mode 100644 index 0000000000000000000000000000000000000000..2e55197593a21cb650727fb4a4f9c58e92acbc4d Binary files /dev/null and b/tools/integration-tests/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin differ diff --git a/tools/integration-tests/assets/entrypoint.sh b/tools/integration-tests/assets/entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..a5ec0407b81c57c162ead53b0494c224f3c76ae7 --- /dev/null +++ b/tools/integration-tests/assets/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +echo "copying assets into shared volume..." +rm -rf /assets/* +cp -rp /tmp/assets/* /assets +chmod 777 /assets/* +echo "assets:" +ls /assets +echo "running tests..." +go test ./tests/"${TEST_NAME}" -v -timeout 30m diff --git a/tools/integration-tests/runTests.sh b/tools/integration-tests/runTests.sh index 71a65da52f4cf9fb0441d8caeef39e2d578b8c0a..42926b752932817fcc014824a45a490ca1f36d36 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 consensus' echo "Build GoShimmer image" docker build -t iotaledger/goshimmer ../../. @@ -10,8 +10,6 @@ docker pull angelocapossele/drand:latest docker pull gaiaadm/pumba:latest docker pull gaiadocker/iproute2:latest -echo "Run integration tests" - for name in $TEST_NAMES do TEST_NAME=$name docker-compose -f tester/docker-compose.yml up --abort-on-container-exit --exit-code-from tester --build diff --git a/tools/integration-tests/tester/docker-compose.yml b/tools/integration-tests/tester/docker-compose.yml index 3b291d69fabb2447f74b68d6e3421b85600efd25..592f47e7af9eade0bf1a1fcd61c161723e4c3870 100644 --- a/tools/integration-tests/tester/docker-compose.yml +++ b/tools/integration-tests/tester/docker-compose.yml @@ -5,13 +5,19 @@ services: container_name: tester image: golang:1.14 working_dir: /tmp/goshimmer/tools/integration-tests/tester - entrypoint: go test ./tests/${TEST_NAME} -v -mod=readonly -timeout 30m + command: /tmp/assets/entrypoint.sh + environment: + - TEST_NAME=${TEST_NAME} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - - ../../..:/tmp/goshimmer:ro + - ../../..:/tmp/goshimmer:rw - ../logs:/tmp/logs + - ../assets:/tmp/assets - goshimmer-testing-cache:/go + - goshimmer-testing-assets:/assets volumes: goshimmer-testing-cache: name: goshimmer-testing-cache + goshimmer-testing-assets: + name: goshimmer-testing-assets \ No newline at end of file diff --git a/tools/integration-tests/tester/framework/docker.go b/tools/integration-tests/tester/framework/docker.go index a5c2cb2493fc6732b013f8d608caf8d8003e2926..7b901ce4ff033a1bc438663a9bdb8f9f99275db7 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" @@ -81,13 +82,21 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error { Cmd: strslice.StrSlice{ "--skip-config=true", "--logger.level=debug", + fmt.Sprintf("--valueLayer.fcob.averageNetworkDelay=%d", ParaFCoBAverageNetworkDelay), + fmt.Sprintf("--autopeering.outboundUpdateIntervalMs=%d", ParaOutboundUpdateIntervalMs), fmt.Sprintf("--node.disablePlugins=%s", config.DisabledPlugins), fmt.Sprintf("--node.enablePlugins=%s", func() string { + var plugins []string if config.Bootstrap { - return "Bootstrap" + plugins = append(plugins, "Bootstrap") } - return "" + if config.Faucet { + plugins = append(plugins, "faucet") + plugins = append(plugins, "testSnapshots") + } + return strings.Join(plugins[:], ",") }()), + fmt.Sprintf("--valueLayer.snapshot.file=%s", config.SnapshotFilePath), fmt.Sprintf("--bootstrap.initialIssuance.timePeriodSec=%d", config.BootstrapInitialIssuanceTimePeriodSec), "--webapi.bindAddress=0.0.0.0:8080", fmt.Sprintf("--autopeering.seed=base58:%s", config.Seed), @@ -99,7 +108,9 @@ func (d *DockerContainer) CreateGoShimmerPeer(config GoShimmerConfig) error { }, } - return d.CreateContainer(config.Name, containerConfig) + return d.CreateContainer(config.Name, containerConfig, &container.HostConfig{ + Binds: []string{"goshimmer-testing-assets:/assets:rw"}, + }) } // CreateDrandMember creates a new container with the drand configuration. @@ -148,7 +159,7 @@ func (d *DockerContainer) CreatePumba(name string, containerName string, targetI cmd = append(cmd, slice...) containerConfig := &container.Config{ - Image: "gaiaadm/pumba:latest", + Image: "gaiaadm/pumba:0.7.2", Cmd: cmd, } diff --git a/tools/integration-tests/tester/framework/drngnetwork.go b/tools/integration-tests/tester/framework/drngnetwork.go index 9df42cdb73c4b856cc828e13095c72fc1b20a318..2f3031ceab001dc7d8e415bb41dce956673cf7b5 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 449c2dc6d07993d3bc722f342c21731e0c6252f0..fa1a80a90f48784f4931dff730fb1bbfbd15476f 100644 --- a/tools/integration-tests/tester/framework/framework.go +++ b/tools/integration-tests/tester/framework/framework.go @@ -80,8 +80,14 @@ func (f *Framework) CreateNetwork(name string, peers int, minimumNeighbors int, // create peers/GoShimmer nodes for i := 0; i < peers; i++ { config := GoShimmerConfig{ - Bootstrap: i == 0, + Bootstrap: func(i int) bool { + if ParaBootstrapOnEveryNode { + return true + } + return i == 0 + }(i), BootstrapInitialIssuanceTimePeriodSec: bootstrapInitialIssuanceTimePeriodSec, + Faucet: i == 0, } if _, err = network.CreatePeer(config); err != nil { return nil, err @@ -127,9 +133,12 @@ func (f *Framework) CreateNetworkWithPartitions(name string, peers, partitions, // create peers/GoShimmer nodes for i := 0; i < peers; i++ { - config := GoShimmerConfig{ - Bootstrap: i == 0, - } + config := GoShimmerConfig{Bootstrap: func(i int) bool { + if ParaBootstrapOnEveryNode { + return true + } + return i == 0 + }(i)} if _, err = network.CreatePeer(config); err != nil { return nil, err } @@ -235,7 +244,12 @@ func (f *Framework) CreateDRNGNetwork(name string, members, peers, minimumNeighb // create peers/GoShimmer nodes for i := 0; i < peers; i++ { - config.Bootstrap = i == 0 + config.Bootstrap = func(i int) bool { + if ParaBootstrapOnEveryNode { + return true + } + return i == 0 + }(i) config.Seed = privKeys[i].Seed().String() if _, err = drng.CreatePeer(config, pubKeys[i]); err != nil { return nil, err diff --git a/tools/integration-tests/tester/framework/network.go b/tools/integration-tests/tester/framework/network.go index c115fcb5a364e57a59183f05ae6b2cedc170c817..b2001e554c81fc8bcfd922a6acf5b23466b32bdc 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" ) @@ -99,6 +100,15 @@ func (n *Network) CreatePeer(c GoShimmerConfig) (*Peer, error) { config.EntryNodeHost = n.namePrefix(containerNameEntryNode) config.EntryNodePublicKey = n.entryNodePublicKey() config.DisabledPlugins = disabledPluginsPeer + config.SnapshotFilePath = snapshotFilePath + + // 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) @@ -115,7 +125,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 6e2ae8c4719bc7ffe388474f9d3094e6f6cba0bd..9b88366083f9aed1e0d03198b4029efff86ec613 100644 --- a/tools/integration-tests/tester/framework/parameters.go +++ b/tools/integration-tests/tester/framework/parameters.go @@ -1,5 +1,9 @@ package framework +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" +) + const ( autopeeringMaxTries = 50 @@ -13,16 +17,31 @@ 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 + snapshotFilePath = "/assets/7R1itJx5hVuo9w9hjg5cwKFmek4HMSoBDgJZN8hKGxih.bin" + dockerLogsPrefixLen = 8 dkgMaxTries = 50 exitStatusSuccessful = 0 ) +// Parameters to override before calling any peer creation function. +var ( + // ParaFCoBAverageNetworkDelay defines the configured avg. network delay (in seconds) for the FCOB rules. + ParaFCoBAverageNetworkDelay = 5 + // ParaOutboundUpdateIntervalMs the autopeering outbound update interval in milliseconds. + ParaOutboundUpdateIntervalMs = 100 + // ParaBootstrapOnEveryNode whether to enable the bootstrap plugin on every node. + ParaBootstrapOnEveryNode = false +) + +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 @@ -30,6 +49,7 @@ type GoShimmerConfig struct { EntryNodeHost string EntryNodePublicKey string DisabledPlugins string + SnapshotFilePath string Bootstrap bool BootstrapInitialIssuanceTimePeriodSec int @@ -38,6 +58,10 @@ type GoShimmerConfig struct { DRNGDistKey string DRNGInstance int DRNGThreshold int + + Faucet bool + + Wallet *wallet.Wallet } // 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 18c059e8d91d6d07b2131fbf620d0b35f45d39ea..8da31c20ef4634f2dc408034b5046a9b7b589712 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/go.mod b/tools/integration-tests/tester/go.mod index 81fe8f1db5258747f5dd843238207468d7dc4c99..fb23805656004ff35923f67605f823ac705c6c07 100644 --- a/tools/integration-tests/tester/go.mod +++ b/tools/integration-tests/tester/go.mod @@ -11,8 +11,8 @@ require ( github.com/drand/drand v0.8.1 github.com/iotaledger/goshimmer v0.1.3 github.com/iotaledger/hive.go v0.0.0-20200610104211-d603429af242 - github.com/opencontainers/go-digest v1.0.0-rc1 // indirect - github.com/stretchr/testify v1.5.1 + github.com/mr-tron/base58 v1.1.3 + github.com/stretchr/testify v1.6.1 ) replace github.com/iotaledger/goshimmer => ../../.. diff --git a/tools/integration-tests/tester/go.sum b/tools/integration-tests/tester/go.sum index dcd5ef8f8d50ea0a9d8c12f8ff8040b4e3171a6c..8d1d03b6fd96ca2ba969cd7d6285a4dbb9ad53d1 100644 --- a/tools/integration-tests/tester/go.sum +++ b/tools/integration-tests/tester/go.sum @@ -204,6 +204,8 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 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-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= @@ -287,6 +289,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -478,6 +482,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= diff --git a/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e0ebf25abb19b226d1fee692ad7dd4d30e8c2dff --- /dev/null +++ b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go @@ -0,0 +1,199 @@ +package consensus + +import ( + "github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework" + "log" + "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/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 + framework.ParaFCoBAverageNetworkDelay = 90 + framework.ParaBootstrapOnEveryNode = true + + // 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 + 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)) + } +} diff --git a/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go b/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e4840807684108caec0ec15a5e0318c53d1afdf7 --- /dev/null +++ b/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go @@ -0,0 +1,118 @@ +package consensus + +import ( + "math/rand" + "testing" + "time" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "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/require" +) + +// TestConsensusNoConflicts issues valid non-conflicting value objects and then checks +// whether the ledger of every peer reflects the same correct state. +func TestConsensusNoConflicts(t *testing.T) { + n, err := f.CreateNetwork("consensus_TestConsensusNoConflicts", 4, 2) + require.NoError(t, err) + defer tests.ShutdownNetwork(t, n) + + time.Sleep(5 * time.Second) + + // 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) + + firstReceiver := wallet.New() + const depositCount = 10 + const deposit = genesisBalance / depositCount + firstReceiverAddresses := make([]string, depositCount) + firstReceiverDepositAddrs := make([]address.Address, depositCount) + firstReceiverDepositOutputs := map[address.Address][]*balance.Balance{} + firstReceiverExpectedBalances := map[string]map[balance.Color]int64{} + for i := 0; i < depositCount; i++ { + addr := firstReceiver.Seed().Address(uint64(i)) + firstReceiverDepositAddrs[i] = addr + firstReceiverAddresses[i] = addr.String() + firstReceiverDepositOutputs[addr] = []*balance.Balance{{Value: deposit, Color: balance.ColorIOTA}} + firstReceiverExpectedBalances[addr.String()] = map[balance.Color]int64{balance.ColorIOTA: deposit} + } + + // issue transaction spending from the genesis output + tx := transaction.New(transaction.NewInputs(genesisOutputID), transaction.NewOutputs(firstReceiverDepositOutputs)) + tx = tx.Sign(signaturescheme.ED25519(*genesisWallet.Seed().KeyPair(0))) + utilsTx := utils.ParseTransaction(tx) + + txID, err := n.Peers()[0].SendTransaction(tx.Bytes()) + require.NoError(t, err) + + // wait for the transaction to be propagated through the network + // and it becoming preferred, finalized and confirmed + time.Sleep(valuetransfers.DefaultAverageNetworkDelay*2 + valuetransfers.DefaultAverageNetworkDelay/2) + + // since we just issued a transaction spending the genesis output, there + // shouldn't be any UTXOs on the genesis address anymore + tests.CheckAddressOutputsFullyConsumed(t, n.Peers(), []string{genesisAddr.String()}) + + // since we waited 2.5 avg. network delays and there were no conflicting transactions, + // the transaction we just issued must be preferred, liked, finalized and confirmed + tests.CheckTransactions(t, n.Peers(), map[string]*tests.ExpectedTransaction{ + txID: {Inputs: &utilsTx.Inputs, Outputs: &utilsTx.Outputs, Signature: &utilsTx.Signature}, + }, true, tests.ExpectedInclusionState{ + Confirmed: tests.True(), Finalized: tests.True(), + Conflicting: tests.False(), Solid: tests.True(), + Rejected: tests.False(), Liked: tests.True(), + }) + + // check balances on peers + tests.CheckBalances(t, n.Peers(), firstReceiverExpectedBalances) + + // issue transactions spending all the outputs which were just created from a random peer + secondReceiverWallet := wallet.New() + secondReceiverAddresses := make([]string, depositCount) + secondReceiverExpectedBalances := map[string]map[balance.Color]int64{} + secondReceiverExpectedTransactions := map[string]*tests.ExpectedTransaction{} + for i := 0; i < depositCount; i++ { + addr := secondReceiverWallet.Seed().Address(uint64(i)) + tx := transaction.New( + transaction.NewInputs(transaction.NewOutputID(firstReceiver.Seed().Address(uint64(i)), tx.ID())), + transaction.NewOutputs(map[address.Address][]*balance.Balance{ + addr: {{Value: deposit, Color: balance.ColorIOTA}}, + }), + ) + secondReceiverAddresses[i] = addr.String() + tx = tx.Sign(signaturescheme.ED25519(*secondReceiverWallet.Seed().KeyPair(uint64(i)))) + txID, err := n.Peers()[rand.Intn(len(n.Peers()))].SendTransaction(tx.Bytes()) + require.NoError(t, err) + + utilsTx := utils.ParseTransaction(tx) + secondReceiverExpectedBalances[addr.String()] = map[balance.Color]int64{balance.ColorIOTA: deposit} + secondReceiverExpectedTransactions[txID] = &tests.ExpectedTransaction{ + Inputs: &utilsTx.Inputs, Outputs: &utilsTx.Outputs, Signature: &utilsTx.Signature, + } + } + + // wait again some network delays for the transactions to materialize + time.Sleep(valuetransfers.DefaultAverageNetworkDelay*2 + valuetransfers.DefaultAverageNetworkDelay/2) + tests.CheckAddressOutputsFullyConsumed(t, n.Peers(), firstReceiverAddresses) + tests.CheckTransactions(t, n.Peers(), secondReceiverExpectedTransactions, true, + tests.ExpectedInclusionState{ + Confirmed: tests.True(), Finalized: tests.True(), + Conflicting: tests.False(), Solid: tests.True(), + Rejected: tests.False(), Liked: tests.True(), + }, + ) + tests.CheckBalances(t, n.Peers(), secondReceiverExpectedBalances) +} diff --git a/tools/integration-tests/tester/tests/consensus/main_test.go b/tools/integration-tests/tester/tests/consensus/main_test.go new file mode 100644 index 0000000000000000000000000000000000000000..422928f9627868376b0610691f0e3ac2f6108636 --- /dev/null +++ b/tools/integration-tests/tester/tests/consensus/main_test.go @@ -0,0 +1,23 @@ +package consensus + +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/testutil.go b/tools/integration-tests/tester/tests/testutil.go index 9e11ee44d83c13f5f9108ce43998198d0e800bb8..31332cffee2dc8975dce16680afb3e3cbb3a7f4e 100644 --- a/tools/integration-tests/tester/tests/testutil.go +++ b/tools/integration-tests/tester/tests/testutil.go @@ -1,16 +1,31 @@ package tests import ( + "errors" "fmt" "math/rand" + "sync" + "sync/atomic" "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/plugins/webapi/value/utils" "github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework" + "github.com/iotaledger/hive.go/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var ( + ErrTransactionNotAvailableInTime = errors.New("transaction was not available in time") + ErrTransactionStateNotSameInTime = errors.New("transaction state did not materialize in time") +) + // DataMessageSent defines a struct to identify from which issuer a data message was sent. type DataMessageSent struct { number int @@ -95,6 +110,460 @@ func CheckForMessageIds(t *testing.T, peers []*framework.Peer, ids map[string]Da } } +// SendValueMessagesOnFaucet sends funds to peers from the faucet and returns the transaction ID. +func SendValueMessagesOnFaucet(t *testing.T, peers []*framework.Peer) (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) + addrBalance[addr][balance.ColorIOTA] = 0 + } + + 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 := SendIotaValueMessages(t, faucetPeer, peers[i], addrBalance) + require.False(t, fail) + txIds = append(txIds, txId) + + // let the transaction propagate + time.Sleep(1 * time.Second) + } + return +} + +// SendValueMessagesOnRandomPeer sends IOTA tokens on random peer and saves the sent message token to a map. +func SendValueMessagesOnRandomPeer(t *testing.T, peers []*framework.Peer, addrBalance map[string]map[balance.Color]int64, numMessages int) (txIds []string) { + for i := 0; i < numMessages; i++ { + from := rand.Intn(len(peers)) + to := rand.Intn(len(peers)) + fail, txId := SendIotaValueMessages(t, peers[from], peers[to], addrBalance) + if fail { + i-- + continue + } + + // attach tx id + txIds = append(txIds, txId) + + // let the transaction propagate + time.Sleep(1 * time.Second) + } + + return +} + +// SendIotaValueMessages sends IOTA token from and to a given peer and returns the transaction ID. +// The same addresses are used in each round +func SendIotaValueMessages(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]int64) (fail bool, txId string) { + var sentValue int64 = 100 + 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 remain 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 +} + +// SendColoredValueMessagesOnRandomPeer sends colored token on a random peer and saves the sent token to a map. +func SendColoredValueMessagesOnRandomPeer(t *testing.T, peers []*framework.Peer, addrBalance map[string]map[balance.Color]int64, numMessages int) (txIds []string) { + for i := 0; i < numMessages; i++ { + from := rand.Intn(len(peers)) + to := rand.Intn(len(peers)) + fail, txId := SendColoredValueMessage(t, peers[from], peers[to], addrBalance) + if fail { + i-- + continue + } + + // attach tx id + txIds = append(txIds, txId) + + // let the transaction propagate + time.Sleep(1 * time.Second) + } + + return +} + +// SendColoredValueMessage sends a colored tokens from and to a given peer and returns the transaction ID. +// The same addresses are used in each round +func SendColoredValueMessage(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]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, "" + } + + 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{} + bs := []*balance.Balance{} + var outputs *transaction.Outputs + var availableIOTA int64 + availableBalances := resp.UnspentOutputs[0].OutputIDs[0].Balances + newColor := false + + // set balances + if len(availableBalances) > 1 { + // the balances contain more than one color, send it all + for _, b := range availableBalances { + value := b.Value + color := getColorFromString(b.Color) + bs = append(bs, balance.New(color, value)) + + // update balance list + addrBalance[inputAddr.String()][color] -= value + if _, ok := addrBalance[outputAddr.String()][color]; ok { + addrBalance[outputAddr.String()][color] += value + } else { + addrBalance[outputAddr.String()][color] = value + } + } + } else { + // create new colored token if inputs only contain IOTA + // half of availableIota tokens remain IOTA, else get recolored + newColor = true + availableIOTA = availableBalances[0].Value + + bs = append(bs, balance.New(balance.ColorIOTA, availableIOTA/2)) + bs = append(bs, balance.New(balance.ColorNew, availableIOTA/2)) + + // update balance list + addrBalance[inputAddr.String()][balance.ColorIOTA] -= availableIOTA + addrBalance[outputAddr.String()][balance.ColorIOTA] += availableIOTA / 2 + } + outmap[outputAddr] = bs + + 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()) + + // FIXME: the new color should be txn ID + if newColor { + if _, ok := addrBalance[outputAddr.String()][balance.ColorNew]; ok { + addrBalance[outputAddr.String()][balance.ColorNew] += availableIOTA / 2 + } else { + addrBalance[outputAddr.String()][balance.ColorNew] = availableIOTA / 2 + } + //addrBalance[outputAddr.String()][getColorFromString(txId)] = availableIOTA / 2 + } + return false, txId +} + +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 color coin + for _, unspents := range resp.UnspentOutputs[0].OutputIDs { + for _, b := range unspents.Balances { + color := getColorFromString(b.Color) + if _, ok := sum[color]; ok { + sum[color] += b.Value + } else { + sum[color] = b.Value + } + } + } + + // check balances + for color, value := range sum { + assert.Equal(t, b[color], value) + } + } + } +} + +// CheckAddressOutputsFullyConsumed performs checks to make sure that on all given peers, +// the given addresses have no UTXOs. +func CheckAddressOutputsFullyConsumed(t *testing.T, peers []*framework.Peer, addrs []string) { + for _, peer := range peers { + resp, err := peer.GetUnspentOutputs(addrs) + assert.NoError(t, err) + assert.Len(t, resp.Error, 0) + for i, utxos := range resp.UnspentOutputs { + assert.Len(t, utxos.OutputIDs, 0, "address %s should not have any UTXOs", addrs[i]) + } + } +} + +// ExpectedInclusionState is an expected inclusion state. +// All fields are optional. +type ExpectedInclusionState struct { + // The optional confirmed state to check against. + Confirmed *bool + // The optional finalized state to check against. + Finalized *bool + // The optional conflict state to check against. + Conflicting *bool + // The optional solid state to check against. + Solid *bool + // The optional rejected state to check against. + Rejected *bool + // The optional liked state to check against. + Liked *bool + // The optional preferred state to check against. + Preferred *bool +} + +// True returns a pointer to a true bool. +func True() *bool { + x := true + return &x +} + +// False returns a pointer to a false bool. +func False() *bool { + x := false + return &x +} + +// ExpectedTransaction defines the expected data of a transaction. +// All fields are optional. +type ExpectedTransaction struct { + // The optional input IDs to check against. + Inputs *[]string + // The optional outputs to check against. + Outputs *[]utils.Output + // The optional signature to check against. + Signature *[]byte +} + +// CheckTransactions performs checks to make sure that all peers have received all transactions. +// Optionally takes an expected inclusion state for all supplied transaction IDs and expected transaction +// data per transaction ID. +func CheckTransactions(t *testing.T, peers []*framework.Peer, transactionIDs map[string]*ExpectedTransaction, checkSynchronized bool, expectedInclusionState ExpectedInclusionState) { + 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, expectedTransaction := range transactionIDs { + resp, err := peer.GetTransactionByID(txId) + require.NoError(t, err) + + // check inclusion state + if expectedInclusionState.Confirmed != nil { + assert.Equal(t, *expectedInclusionState.Confirmed, resp.InclusionState.Confirmed, "confirmed state doesn't match - %s", txId) + } + if expectedInclusionState.Conflicting != nil { + assert.Equal(t, *expectedInclusionState.Conflicting, resp.InclusionState.Conflicting, "conflict state doesn't match - %s", txId) + } + if expectedInclusionState.Solid != nil { + assert.Equal(t, *expectedInclusionState.Solid, resp.InclusionState.Solid, "solid state doesn't match - %s", txId) + } + if expectedInclusionState.Rejected != nil { + assert.Equal(t, *expectedInclusionState.Rejected, resp.InclusionState.Rejected, "rejected state doesn't match - %s", txId) + } + if expectedInclusionState.Liked != nil { + assert.Equal(t, *expectedInclusionState.Liked, resp.InclusionState.Liked, "liked state doesn't match - %s", txId) + } + if expectedInclusionState.Preferred != nil { + assert.Equal(t, *expectedInclusionState.Preferred, resp.InclusionState.Preferred, "preferred state doesn't match - %s", txId) + } + + if expectedTransaction != nil { + if expectedTransaction.Inputs != nil { + assert.Equal(t, *expectedTransaction.Inputs, resp.Transaction.Inputs, "inputs do not match - %s", txId) + } + if expectedTransaction.Outputs != nil { + assert.Equal(t, *expectedTransaction.Outputs, resp.Transaction.Outputs, "outputs do not match - %s", txId) + } + if expectedTransaction.Signature != nil { + assert.Equal(t, *expectedTransaction.Signature, resp.Transaction.Signature, "signatures do not match - %s", txId) + } + } + } + } +} + +// AwaitTransactionAvailability awaits until the given transaction IDs become available on all given peers or +// the max duration is reached. Returns a map of missing transactions per peer. An error is returned if at least +// one peer does not have all specified transactions available. +func AwaitTransactionAvailability(peers []*framework.Peer, transactionIDs []string, maxAwait time.Duration) (missing map[string]map[string]types.Empty, err error) { + s := time.Now() + var missingMu sync.Mutex + missing = map[string]map[string]types.Empty{} + for ; time.Since(s) < maxAwait; time.Sleep(500 * time.Millisecond) { + var wg sync.WaitGroup + wg.Add(len(peers)) + counter := int32(len(peers) * len(transactionIDs)) + for _, p := range peers { + go func(p *framework.Peer) { + defer wg.Done() + for _, txID := range transactionIDs { + _, err := p.GetTransactionByID(txID) + if err == nil { + missingMu.Lock() + m, has := missing[p.ID().String()] + if has { + delete(m, txID) + if len(m) == 0 { + delete(missing, p.ID().String()) + } + } + missingMu.Unlock() + atomic.AddInt32(&counter, -1) + continue + } + missingMu.Lock() + m, has := missing[p.ID().String()] + if !has { + m = map[string]types.Empty{} + } + m[txID] = types.Empty{} + missing[p.ID().String()] = m + missingMu.Unlock() + } + }(p) + } + wg.Wait() + if counter == 0 { + // everything available + return missing, nil + } + } + return missing, ErrTransactionNotAvailableInTime +} + +// AwaitTransactionInclusionState awaits on all given peers until the specified transactions +// have the expected state or max duration is reached. This function does not gracefully +// handle the transactions not existing on the given peers, therefore it must be ensured +// the the transactions exist beforehand. +func AwaitTransactionInclusionState(peers []*framework.Peer, transactionIDs map[string]ExpectedInclusionState, maxAwait time.Duration) error { + s := time.Now() + for ; time.Since(s) < maxAwait; time.Sleep(1 * time.Second) { + var wg sync.WaitGroup + wg.Add(len(peers)) + counter := int32(len(peers) * len(transactionIDs)) + for _, p := range peers { + go func(p *framework.Peer) { + defer wg.Done() + for txID := range transactionIDs { + tx, err := p.GetTransactionByID(txID) + if err != nil { + continue + } + expInclState := transactionIDs[txID] + if expInclState.Confirmed != nil && *expInclState.Confirmed != tx.InclusionState.Confirmed { + continue + } + if expInclState.Conflicting != nil && *expInclState.Conflicting != tx.InclusionState.Conflicting { + continue + } + if expInclState.Finalized != nil && *expInclState.Finalized != tx.InclusionState.Finalized { + continue + } + if expInclState.Liked != nil && *expInclState.Liked != tx.InclusionState.Liked { + continue + } + if expInclState.Preferred != nil && *expInclState.Preferred != tx.InclusionState.Preferred { + continue + } + if expInclState.Rejected != nil && *expInclState.Rejected != tx.InclusionState.Rejected { + continue + } + if expInclState.Solid != nil && *expInclState.Solid != tx.InclusionState.Solid { + continue + } + atomic.AddInt32(&counter, -1) + } + }(p) + } + wg.Wait() + if counter == 0 { + // everything available + return nil + } + } + return ErrTransactionStateNotSameInTime +} + // 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 0000000000000000000000000000000000000000..bcc09c2e53590df061cbff5a2159071d567440f1 --- /dev/null +++ b/tools/integration-tests/tester/tests/value/main_test.go @@ -0,0 +1,23 @@ +package autopeering + +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 0000000000000000000000000000000000000000..f0cb8109be17c0bea4ffaa1959042cd902070f43 --- /dev/null +++ b/tools/integration-tests/tester/tests/value/value_test.go @@ -0,0 +1,145 @@ +package autopeering + +import ( + "testing" + "time" + + "github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests" + "github.com/stretchr/testify/require" +) + +// TestValueIotaPersistence issues messages on random peers, restarts them and checks for persistence after restart. +func TestValueIotaPersistence(t *testing.T) { + n, err := f.CreateNetwork("valueIota_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 + txIdsSlice, addrBalance := tests.SendValueMessagesOnFaucet(t, n.Peers()) + txIds := make(map[string]*tests.ExpectedTransaction) + for _, txID := range txIdsSlice { + txIds[txID] = nil + } + + // wait for messages to be gossiped + time.Sleep(10 * time.Second) + + // check whether the first issued transaction is available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.True(), + }) + + // check ledger state + tests.CheckBalances(t, n.Peers(), addrBalance) + + // send value message randomly + randomTxIds := tests.SendValueMessagesOnRandomPeer(t, n.Peers(), addrBalance, 10) + for _, randomTxId := range randomTxIds { + txIds[randomTxId] = nil + } + + // wait for messages to be gossiped + time.Sleep(10 * time.Second) + + // check whether all issued transactions are persistently available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.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(10 * time.Second) + + // check whether all issued transactions are persistently available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.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 + txIdsSlice, addrBalance := tests.SendValueMessagesOnFaucet(t, n.Peers()) + txIds := make(map[string]*tests.ExpectedTransaction) + for _, txID := range txIdsSlice { + txIds[txID] = nil + } + + // wait for messages to be gossiped + time.Sleep(10 * time.Second) + + // check whether the transactions are available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.True(), + }) + + // check ledger state + tests.CheckBalances(t, n.Peers(), addrBalance) + + // send funds around + randomTxIds := tests.SendColoredValueMessagesOnRandomPeer(t, n.Peers(), addrBalance, 10) + for _, randomTxId := range randomTxIds { + txIds[randomTxId] = nil + } + + // wait for value messages to be gossiped + time.Sleep(10 * time.Second) + + // check whether all issued transactions are persistently available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.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(10 * time.Second) + + // check whether all issued transactions are persistently available on all nodes, and confirmed + tests.CheckTransactions(t, n.Peers(), txIds, true, tests.ExpectedInclusionState{ + Confirmed: tests.True(), + }) + + // 5. check ledger state + tests.CheckBalances(t, n.Peers(), addrBalance) +}