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") ) // DataMessageSent defines a struct to identify from which issuer a data message was sent. type DataMessageSent struct { number int id string data []byte issuerPublicKey string } type Shutdowner interface { Shutdown() error } // SendDataMessagesOnRandomPeer sends data messages on a random peer and saves the sent message to a map. func SendDataMessagesOnRandomPeer(t *testing.T, peers []*framework.Peer, numMessages int, idsMap ...map[string]DataMessageSent) map[string]DataMessageSent { var ids map[string]DataMessageSent if len(idsMap) > 0 { ids = idsMap[0] } else { ids = make(map[string]DataMessageSent, numMessages) } for i := 0; i < numMessages; i++ { data := []byte(fmt.Sprintf("Test%d", i)) peer := peers[rand.Intn(len(peers))] id, sent := SendDataMessage(t, peer, data, i) ids[id] = sent } return ids } // SendDataMessage sends a data message on a given peer and returns the id and a DataMessageSent struct. func SendDataMessage(t *testing.T, peer *framework.Peer, data []byte, number int) (string, DataMessageSent) { id, err := peer.Data(data) require.NoErrorf(t, err, "Could not send message on %s", peer.String()) sent := DataMessageSent{ number: number, id: id, // save payload to be able to compare API response data: payload.NewData(data).Bytes(), issuerPublicKey: peer.Identity.PublicKey().String(), } return id, sent } // CheckForMessageIds performs checks to make sure that all peers received all given messages defined in ids. func CheckForMessageIds(t *testing.T, peers []*framework.Peer, ids map[string]DataMessageSent, checkSynchronized bool) { var idsSlice []string for id := range ids { idsSlice = append(idsSlice, id) } 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) } resp, err := peer.FindMessageByID(idsSlice) require.NoError(t, err) // check that all messages are present in response respIDs := make([]string, len(resp.Messages)) for i, msg := range resp.Messages { respIDs[i] = msg.ID } assert.ElementsMatchf(t, idsSlice, respIDs, "messages do not match sent in %s", peer.String()) // check for general information for _, msg := range resp.Messages { msgSent := ids[msg.ID] assert.Equalf(t, msgSent.issuerPublicKey, msg.IssuerPublicKey, "messageID=%s, issuer=%s not correct issuer in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) assert.Equalf(t, msgSent.data, msg.Payload, "messageID=%s, issuer=%s data not equal in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) assert.Truef(t, msg.Metadata.Solid, "messageID=%s, issuer=%s not solid in %s.", msgSent.id, msgSent.issuerPublicKey, peer.String()) } } } // 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. Conflict *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 } // 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") } if expectedInclusionState.Conflict != nil { assert.Equal(t, *expectedInclusionState.Conflict, resp.InclusionState.Conflict, "conflict state doesn't match") } if expectedInclusionState.Solid != nil { assert.Equal(t, *expectedInclusionState.Solid, resp.InclusionState.Solid, "solid state doesn't match") } if expectedInclusionState.Rejected != nil { assert.Equal(t, *expectedInclusionState.Rejected, resp.InclusionState.Rejected, "rejected state doesn't match") } if expectedInclusionState.Liked != nil { assert.Equal(t, *expectedInclusionState.Liked, resp.InclusionState.Liked, "liked state doesn't match") } if expectedTransaction != nil { if expectedTransaction.Inputs != nil { assert.Equal(t, *expectedTransaction.Inputs, resp.Transaction.Inputs, "inputs do not match") } if expectedTransaction.Outputs != nil { assert.Equal(t, *expectedTransaction.Outputs, resp.Transaction.Outputs, "outputs do not match") } if expectedTransaction.Signature != nil { assert.Equal(t, *expectedTransaction.Signature, resp.Transaction.Signature, "signatures do not match") } } } } } // 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(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 { _, 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 } // ShutdownNetwork shuts down the network and reports errors. func ShutdownNetwork(t *testing.T, n Shutdowner) { err := n.Shutdown() require.NoError(t, err) }