From b52c874d12482fce093311d9bfcebf7fa56fde6b Mon Sep 17 00:00:00 2001 From: Ethan Buchman <ethan@coinculture.info> Date: Thu, 28 Apr 2016 13:12:32 -0400 Subject: [PATCH] add rpc tests. not finished yet --- rpc/test/client_rpc_test.go | 129 ++++++++++++++ rpc/test/client_ws_test.go | 212 +++++++++++++++++++++++ rpc/test/config.go | 13 ++ rpc/test/genesis.go | 49 ++++++ rpc/test/helpers.go | 331 ++++++++++++++++++++++++++++++++++++ rpc/test/tests.go | 286 +++++++++++++++++++++++++++++++ rpc/test/ws_helpers.go | 278 ++++++++++++++++++++++++++++++ 7 files changed, 1298 insertions(+) create mode 100644 rpc/test/client_rpc_test.go create mode 100644 rpc/test/client_ws_test.go create mode 100644 rpc/test/config.go create mode 100644 rpc/test/genesis.go create mode 100644 rpc/test/helpers.go create mode 100644 rpc/test/tests.go create mode 100644 rpc/test/ws_helpers.go diff --git a/rpc/test/client_rpc_test.go b/rpc/test/client_rpc_test.go new file mode 100644 index 00000000..eb8a0945 --- /dev/null +++ b/rpc/test/client_rpc_test.go @@ -0,0 +1,129 @@ +package rpctest + +import ( + "testing" + + // ctypes "github.com/eris-ltd/eris-db/rpc/core/types" + _ "github.com/tendermint/tendermint/config/tendermint_test" +) + +// When run with `-test.short` we only run: +// TestHTTPStatus, TestHTTPBroadcast, TestJSONStatus, TestJSONBroadcast, TestWSConnect, TestWSSend + +//-------------------------------------------------------------------------------- +func TestHTTPStatus(t *testing.T) { + testStatus(t, "HTTP") +} + +func TestHTTPGenPriv(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGenPriv(t, "HTTP") +} + +func TestHTTPGetAccount(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGetAccount(t, "HTTP") +} + +func TestHTTPSignedTx(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testSignedTx(t, "HTTP") +} + +func TestHTTPBroadcastTx(t *testing.T) { + testBroadcastTx(t, "HTTP") +} + +func TestHTTPGetStorage(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGetStorage(t, "HTTP") +} + +func TestHTTPCallCode(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testCallCode(t, "HTTP") +} + +func TestHTTPCallContract(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testCall(t, "HTTP") +} + +func TestHTTPNameReg(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testNameReg(t, "HTTP") +} + +//-------------------------------------------------------------------------------- +// Test the JSONRPC client + +func TestJSONStatus(t *testing.T) { + testStatus(t, "JSONRPC") +} + +func TestJSONGenPriv(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGenPriv(t, "JSONRPC") +} + +func TestJSONGetAccount(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGetAccount(t, "JSONRPC") +} + +func TestJSONSignedTx(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testSignedTx(t, "JSONRPC") +} + +func TestJSONBroadcastTx(t *testing.T) { + testBroadcastTx(t, "JSONRPC") +} + +func TestJSONGetStorage(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testGetStorage(t, "JSONRPC") +} + +func TestJSONCallCode(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testCallCode(t, "JSONRPC") +} + +func TestJSONCallContract(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testCall(t, "JSONRPC") +} + +func TestJSONNameReg(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + testNameReg(t, "JSONRPC") +} diff --git a/rpc/test/client_ws_test.go b/rpc/test/client_ws_test.go new file mode 100644 index 00000000..94e85508 --- /dev/null +++ b/rpc/test/client_ws_test.go @@ -0,0 +1,212 @@ +package rpctest + +import ( + "fmt" + "testing" + + "github.com/eris-ltd/eris-db/txs" + _ "github.com/tendermint/tendermint/config/tendermint_test" +) + +var wsTyp = "JSONRPC" + +//-------------------------------------------------------------------------------- +// Test the websocket service + +// make a simple connection to the server +func TestWSConnect(t *testing.T) { + wsc := newWSClient(t) + wsc.Stop() +} + +// receive a new block message +func TestWSNewBlock(t *testing.T) { + wsc := newWSClient(t) + eid := types.EventStringNewBlock() + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + waitForEvent(t, wsc, eid, true, func() {}, func(eid string, b interface{}) error { + fmt.Println("Check:", string(b.([]byte))) + return nil + }) +} + +// receive a few new block messages in a row, with increasing height +func TestWSBlockchainGrowth(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + wsc := newWSClient(t) + eid := types.EventStringNewBlock() + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + // listen for NewBlock, ensure height increases by 1 + unmarshalValidateBlockchain(t, wsc, eid) +} + +// send a transaction and validate the events from listening for both sender and receiver +func TestWSSend(t *testing.T) { + toAddr := user[1].Address + amt := int64(100) + + wsc := newWSClient(t) + eidInput := types.EventStringAccInput(user[0].Address) + eidOutput := types.EventStringAccOutput(toAddr) + subscribe(t, wsc, eidInput) + subscribe(t, wsc, eidOutput) + defer func() { + unsubscribe(t, wsc, eidInput) + unsubscribe(t, wsc, eidOutput) + wsc.Stop() + }() + waitForEvent(t, wsc, eidInput, true, func() { + tx := makeDefaultSendTxSigned(t, wsTyp, toAddr, amt) + broadcastTx(t, wsTyp, tx) + }, unmarshalValidateSend(amt, toAddr)) + waitForEvent(t, wsc, eidOutput, true, func() {}, unmarshalValidateSend(amt, toAddr)) +} + +// ensure events are only fired once for a given transaction +func TestWSDoubleFire(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + wsc := newWSClient(t) + eid := types.EventStringAccInput(user[0].Address) + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + amt := int64(100) + toAddr := user[1].Address + // broadcast the transaction, wait to hear about it + waitForEvent(t, wsc, eid, true, func() { + tx := makeDefaultSendTxSigned(t, wsTyp, toAddr, amt) + broadcastTx(t, wsTyp, tx) + }, func(eid string, b interface{}) error { + return nil + }) + // but make sure we don't hear about it twice + waitForEvent(t, wsc, eid, false, func() { + }, func(eid string, b interface{}) error { + return nil + }) +} + +// create a contract, wait for the event, and send it a msg, validate the return +func TestWSCallWait(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + wsc := newWSClient(t) + eid1 := types.EventStringAccInput(user[0].Address) + subscribe(t, wsc, eid1) + defer func() { + unsubscribe(t, wsc, eid1) + wsc.Stop() + }() + amt, gasLim, fee := int64(10000), int64(1000), int64(1000) + code, returnCode, returnVal := simpleContract() + var contractAddr []byte + // wait for the contract to be created + waitForEvent(t, wsc, eid1, true, func() { + tx := makeDefaultCallTx(t, wsTyp, nil, code, amt, gasLim, fee) + receipt := broadcastTx(t, wsTyp, tx) + contractAddr = receipt.ContractAddr + }, unmarshalValidateTx(amt, returnCode)) + + // susbscribe to the new contract + amt = int64(10001) + eid2 := types.EventStringAccOutput(contractAddr) + subscribe(t, wsc, eid2) + defer func() { + unsubscribe(t, wsc, eid2) + }() + // get the return value from a call + data := []byte{0x1} + waitForEvent(t, wsc, eid2, true, func() { + tx := makeDefaultCallTx(t, wsTyp, contractAddr, data, amt, gasLim, fee) + receipt := broadcastTx(t, wsTyp, tx) + contractAddr = receipt.ContractAddr + }, unmarshalValidateTx(amt, returnVal)) +} + +// create a contract and send it a msg without waiting. wait for contract event +// and validate return +func TestWSCallNoWait(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + wsc := newWSClient(t) + amt, gasLim, fee := int64(10000), int64(1000), int64(1000) + code, _, returnVal := simpleContract() + + tx := makeDefaultCallTx(t, wsTyp, nil, code, amt, gasLim, fee) + receipt := broadcastTx(t, wsTyp, tx) + contractAddr := receipt.ContractAddr + + // susbscribe to the new contract + amt = int64(10001) + eid := types.EventStringAccOutput(contractAddr) + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + // get the return value from a call + data := []byte{0x1} + waitForEvent(t, wsc, eid, true, func() { + tx := makeDefaultCallTx(t, wsTyp, contractAddr, data, amt, gasLim, fee) + broadcastTx(t, wsTyp, tx) + }, unmarshalValidateTx(amt, returnVal)) +} + +// create two contracts, one of which calls the other +func TestWSCallCall(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + wsc := newWSClient(t) + amt, gasLim, fee := int64(10000), int64(1000), int64(1000) + code, _, returnVal := simpleContract() + txid := new([]byte) + + // deploy the two contracts + tx := makeDefaultCallTx(t, wsTyp, nil, code, amt, gasLim, fee) + receipt := broadcastTx(t, wsTyp, tx) + contractAddr1 := receipt.ContractAddr + + code, _, _ = simpleCallContract(contractAddr1) + tx = makeDefaultCallTx(t, wsTyp, nil, code, amt, gasLim, fee) + receipt = broadcastTx(t, wsTyp, tx) + contractAddr2 := receipt.ContractAddr + + // susbscribe to the new contracts + amt = int64(10001) + eid1 := types.EventStringAccCall(contractAddr1) + subscribe(t, wsc, eid1) + defer func() { + unsubscribe(t, wsc, eid1) + wsc.Stop() + }() + // call contract2, which should call contract1, and wait for ev1 + + // let the contract get created first + waitForEvent(t, wsc, eid1, true, func() { + }, func(eid string, b interface{}) error { + return nil + }) + // call it + waitForEvent(t, wsc, eid1, true, func() { + tx := makeDefaultCallTx(t, wsTyp, contractAddr2, nil, amt, gasLim, fee) + broadcastTx(t, wsTyp, tx) + *txid = types.TxID(chainID, tx) + }, unmarshalValidateCall(user[0].Address, returnVal, txid)) +} diff --git a/rpc/test/config.go b/rpc/test/config.go new file mode 100644 index 00000000..2fe20981 --- /dev/null +++ b/rpc/test/config.go @@ -0,0 +1,13 @@ +package rpctest + +import ( + cfg "github.com/tendermint/go-config" +) + +var config cfg.Config = nil + +func init() { + cfg.OnConfig(func(newConfig cfg.Config) { + config = newConfig + }) +} diff --git a/rpc/test/genesis.go b/rpc/test/genesis.go new file mode 100644 index 00000000..d1ee5621 --- /dev/null +++ b/rpc/test/genesis.go @@ -0,0 +1,49 @@ +package rpctest + +// priv keys generated deterministically eg rpc/tests/helpers.go +var defaultGenesis = `{ + "chain_id" : "tendermint_test", + "accounts": [ + { + "address": "E9B5D87313356465FAE33C406CE2C2979DE60BCB", + "amount": 200000000 + }, + { + "address": "DFE4AFFA4CEE17CD01CB9E061D77C3ECED29BD88", + "amount": 200000000 + }, + { + "address": "F60D30722E7B497FA532FB3207C3FB29C31B1992", + "amount": 200000000 + }, + { + "address": "336CB40A5EB92E496E19B74FDFF2BA017C877FD6", + "amount": 200000000 + }, + { + "address": "D218F0F439BF0384F6F5EF8D0F8B398D941BD1DC", + "amount": 200000000 + } + ], + "validators": [ + { + "pub_key": [1, "583779C3BFA3F6C7E23C7D830A9C3D023A216B55079AD38BFED1207B94A19548"], + "amount": 1000000, + "unbond_to": [ + { + "address": "E9B5D87313356465FAE33C406CE2C2979DE60BCB", + "amount": 100000 + } + ] + } + ] +}` + +var defaultPrivValidator = `{ + "address": "1D7A91CB32F758A02EBB9BE1FB6F8DEE56F90D42", + "pub_key": [1,"06FBAC4E285285D1D91FCBC7E91C780ADA11516F67462340B3980CE2B94940E8"], + "priv_key": [1,"C453604BD6480D5538B4C6FD2E3E314B5BCE518D75ADE4DA3DA85AB8ADFD819606FBAC4E285285D1D91FCBC7E91C780ADA11516F67462340B3980CE2B94940E8"], + "last_height":0, + "last_round":0, + "last_step":0 +}` diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go new file mode 100644 index 00000000..91b9154d --- /dev/null +++ b/rpc/test/helpers.go @@ -0,0 +1,331 @@ +package rpctest + +import ( + "bytes" + "strconv" + "testing" + + acm "github.com/eris-ltd/eris-db/account" + edb "github.com/eris-ltd/eris-db/erisdb" + edbcli "github.com/eris-ltd/eris-db/rpc/client" + ctypes "github.com/eris-ltd/eris-db/rpc/core/types" + sm "github.com/eris-ltd/eris-db/state" + stypes "github.com/eris-ltd/eris-db/state/types" + edbapp "github.com/eris-ltd/eris-db/tmsp" + txs "github.com/eris-ltd/eris-db/txs" + + . "github.com/tendermint/go-common" + "github.com/tendermint/go-crypto" + dbm "github.com/tendermint/go-db" + "github.com/tendermint/go-events" + "github.com/tendermint/go-p2p" + rpcclient "github.com/tendermint/go-rpc/client" + "github.com/tendermint/go-wire" + + "github.com/tendermint/tendermint/config/tendermint_test" + nm "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/types" +) + +// global variables for use across all tests +var ( + node *nm.Node + mempoolCount = 0 + chainID string + rpcAddr string + requestAddr string + websocketAddr string + websocketEndpoint string + clientURI *rpcclient.ClientURI + clientJSON *rpcclient.ClientJSONRPC + + user = makeUsers(5) // make keys + clients map[string]rpcclient.Client +) + +func init() { + tendermint_test.ResetConfig("rpc_test_client_test") + initGlobalVariables() + + saveNewPriv() +} + +// initialize config and create new node +func initGlobalVariables() { + chainID = config.GetString("tm.chain_id") + rpcAddr = config.GetString("tm.rpc_laddr") + config.Set("erisdb.chain_id", chainID) + requestAddr = rpcAddr + websocketAddr = rpcAddr + websocketEndpoint = "/websocket" + + clientURI = rpcclient.NewClientURI(requestAddr) + clientJSON = rpcclient.NewClientJSONRPC(requestAddr) + + clients = map[string]rpcclient.Client{ + "JSONRPC": clientJSON, + "HTTP": clientURI, + } + + // write the genesis + MustWriteFile(config.GetString("tm.genesis_file"), []byte(defaultGenesis), 0600) + + // TODO: change consensus/state.go timeouts to be shorter + + // start a node + ready := make(chan struct{}) + go newNode(ready) + <-ready +} + +// deterministic account generation, synced with genesis file in config/tendermint_test/config.go +func makeUsers(n int) []*acm.PrivAccount { + accounts := []*acm.PrivAccount{} + for i := 0; i < n; i++ { + secret := ("mysecret" + strconv.Itoa(i)) + user := acm.GenPrivAccountFromSecret(secret) + accounts = append(accounts, user) + } + return accounts +} + +// create a new node and sleep forever +func newNode(ready chan struct{}) { + stateDB := dbm.GetDB("app_state", "memdb", "") + genDoc, state := sm.MakeGenesisStateFromFile(stateDB, config.GetString("tm.genesis_file")) + state.Save() + buf, n, err := new(bytes.Buffer), new(int), new(error) + wire.WriteJSON(genDoc, buf, n, err) + stateDB.Set(stypes.GenDocKey, buf.Bytes()) + if *err != nil { + Exit(Fmt("Unable to write gendoc to db: %v", err)) + } + evsw := events.NewEventSwitch() + evsw.Start() + // create the app + app := edbapp.NewErisDBApp(state, evsw) + + // Create & start node + privValidatorFile := config.GetString("tm.priv_validator_file") + privValidator := types.LoadOrGenPrivValidator(privValidatorFile) + node = nm.NewNode(privValidator, nm.GetProxyApp) + l := p2p.NewDefaultListener("tcp", config.GetString("tm.node_laddr"), true) + node.AddListener(l) + node.Start() + + // Run the RPC server. + edb.StartRPC(node, app) + ready <- struct{}{} + + // Sleep forever + ch := make(chan struct{}) + <-ch +} + +func saveNewPriv() { + // Save new priv_validator file. + priv := &types.PrivValidator{ + Address: user[0].Address, + PubKey: crypto.PubKeyEd25519(user[0].PubKey.(crypto.PubKeyEd25519)), + PrivKey: crypto.PrivKeyEd25519(user[0].PrivKey.(crypto.PrivKeyEd25519)), + } + priv.SetFile(config.GetString("tm.priv_validator_file")) + priv.Save() +} + +//------------------------------------------------------------------------------- +// some default transaction functions + +func makeDefaultSendTx(t *testing.T, typ string, addr []byte, amt int64) *txs.SendTx { + nonce := getNonce(t, typ, user[0].Address) + tx := txs.NewSendTx() + tx.AddInputWithNonce(user[0].PubKey, amt, nonce+1) + tx.AddOutput(addr, amt) + return tx +} + +func makeDefaultSendTxSigned(t *testing.T, typ string, addr []byte, amt int64) *txs.SendTx { + tx := makeDefaultSendTx(t, typ, addr, amt) + tx.SignInput(chainID, 0, user[0]) + return tx +} + +func makeDefaultCallTx(t *testing.T, typ string, addr, code []byte, amt, gasLim, fee int64) *txs.CallTx { + nonce := getNonce(t, typ, user[0].Address) + tx := txs.NewCallTxWithNonce(user[0].PubKey, addr, code, amt, gasLim, fee, nonce+1) + tx.Sign(chainID, user[0]) + return tx +} + +func makeDefaultNameTx(t *testing.T, typ string, name, value string, amt, fee int64) *txs.NameTx { + nonce := getNonce(t, typ, user[0].Address) + tx := txs.NewNameTxWithNonce(user[0].PubKey, name, value, amt, fee, nonce+1) + tx.Sign(chainID, user[0]) + return tx +} + +//------------------------------------------------------------------------------- +// rpc call wrappers (fail on err) + +// get an account's nonce +func getNonce(t *testing.T, typ string, addr []byte) int { + client := clients[typ] + ac, err := edbcli.GetAccount(client, addr) + if err != nil { + t.Fatal(err) + } + if ac == nil { + return 0 + } + return ac.Sequence +} + +// get the account +func getAccount(t *testing.T, typ string, addr []byte) *acm.Account { + client := clients[typ] + ac, err := edbcli.GetAccount(client, addr) + if err != nil { + t.Fatal(err) + } + return ac +} + +// sign transaction +func signTx(t *testing.T, typ string, tx txs.Tx, privAcc *acm.PrivAccount) txs.Tx { + client := clients[typ] + signedTx, err := edbcli.SignTx(client, tx, []*acm.PrivAccount{privAcc}) + if err != nil { + t.Fatal(err) + } + return signedTx +} + +// broadcast transaction +func broadcastTx(t *testing.T, typ string, tx txs.Tx) ctypes.Receipt { + client := clients[typ] + rec, err := edbcli.BroadcastTx(client, tx) + if err != nil { + t.Fatal(err) + } + mempoolCount += 1 + return rec +} + +// dump all storage for an account. currently unused +func dumpStorage(t *testing.T, addr []byte) *ctypes.ResultDumpStorage { + client := clients["HTTP"] + resp, err := edbcli.DumpStorage(client, addr) + if err != nil { + t.Fatal(err) + } + return resp +} + +func getStorage(t *testing.T, typ string, addr, key []byte) []byte { + client := clients[typ] + resp, err := edbcli.GetStorage(client, addr, key) + if err != nil { + t.Fatal(err) + } + return resp +} + +func callCode(t *testing.T, client rpcclient.Client, fromAddress, code, data, expected []byte) { + resp, err := edbcli.CallCode(client, fromAddress, code, data) + if err != nil { + t.Fatal(err) + } + ret := resp.Return + // NOTE: we don't flip memory when it comes out of RETURN (?!) + if bytes.Compare(ret, LeftPadWord256(expected).Bytes()) != 0 { + t.Fatalf("Conflicting return value. Got %x, expected %x", ret, expected) + } +} + +func callContract(t *testing.T, client rpcclient.Client, fromAddress, toAddress, data, expected []byte) { + resp, err := edbcli.Call(client, fromAddress, toAddress, data) + if err != nil { + t.Fatal(err) + } + ret := resp.Return + // NOTE: we don't flip memory when it comes out of RETURN (?!) + if bytes.Compare(ret, LeftPadWord256(expected).Bytes()) != 0 { + t.Fatalf("Conflicting return value. Got %x, expected %x", ret, expected) + } +} + +// get the namereg entry +func getNameRegEntry(t *testing.T, typ string, name string) *txs.NameRegEntry { + client := clients[typ] + entry, err := edbcli.GetName(client, name) + if err != nil { + t.Fatal(err) + } + return entry +} + +//-------------------------------------------------------------------------------- +// utility verification function + +func checkTx(t *testing.T, fromAddr []byte, priv *acm.PrivAccount, tx *txs.SendTx) { + if bytes.Compare(tx.Inputs[0].Address, fromAddr) != 0 { + t.Fatal("Tx input addresses don't match!") + } + + signBytes := acm.SignBytes(chainID, tx) + in := tx.Inputs[0] //(*types.SendTx).Inputs[0] + + if err := in.ValidateBasic(); err != nil { + t.Fatal(err) + } + // Check signatures + // acc := getAccount(t, byteAddr) + // NOTE: using the acc here instead of the in fails; it is nil. + if !in.PubKey.VerifyBytes(signBytes, in.Signature) { + t.Fatal(txs.ErrTxInvalidSignature) + } +} + +// simple contract returns 5 + 6 = 0xb +func simpleContract() ([]byte, []byte, []byte) { + // this is the code we want to run when the contract is called + contractCode := []byte{0x60, 0x5, 0x60, 0x6, 0x1, 0x60, 0x0, 0x52, 0x60, 0x20, 0x60, 0x0, 0xf3} + // the is the code we need to return the contractCode when the contract is initialized + lenCode := len(contractCode) + // push code to the stack + //code := append([]byte{byte(0x60 + lenCode - 1)}, RightPadWord256(contractCode).Bytes()...) + code := append([]byte{0x7f}, RightPadWord256(contractCode).Bytes()...) + // store it in memory + code = append(code, []byte{0x60, 0x0, 0x52}...) + // return whats in memory + //code = append(code, []byte{0x60, byte(32 - lenCode), 0x60, byte(lenCode), 0xf3}...) + code = append(code, []byte{0x60, byte(lenCode), 0x60, 0x0, 0xf3}...) + // return init code, contract code, expected return + return code, contractCode, LeftPadBytes([]byte{0xb}, 32) +} + +// simple call contract calls another contract +func simpleCallContract(addr []byte) ([]byte, []byte, []byte) { + gas1, gas2 := byte(0x1), byte(0x1) + value := byte(0x1) + inOff, inSize := byte(0x0), byte(0x0) // no call data + retOff, retSize := byte(0x0), byte(0x20) + // this is the code we want to run (call a contract and return) + contractCode := []byte{0x60, retSize, 0x60, retOff, 0x60, inSize, 0x60, inOff, 0x60, value, 0x73} + contractCode = append(contractCode, addr...) + contractCode = append(contractCode, []byte{0x61, gas1, gas2, 0xf1, 0x60, 0x20, 0x60, 0x0, 0xf3}...) + + // the is the code we need to return; the contractCode when the contract is initialized + // it should copy the code from the input into memory + lenCode := len(contractCode) + memOff := byte(0x0) + inOff = byte(0xc) // length of code before codeContract + length := byte(lenCode) + + code := []byte{0x60, length, 0x60, inOff, 0x60, memOff, 0x37} + // return whats in memory + code = append(code, []byte{0x60, byte(lenCode), 0x60, 0x0, 0xf3}...) + code = append(code, contractCode...) + // return init code, contract code, expected return + return code, contractCode, LeftPadBytes([]byte{0xb}, 32) +} diff --git a/rpc/test/tests.go b/rpc/test/tests.go new file mode 100644 index 00000000..b6ed2c84 --- /dev/null +++ b/rpc/test/tests.go @@ -0,0 +1,286 @@ +package rpctest + +import ( + "bytes" + "fmt" + "testing" + + edbcli "github.com/eris-ltd/eris-db/rpc/client" + "github.com/eris-ltd/eris-db/txs" + + . "github.com/tendermint/go-common" +) + +var doNothing = func(eid string, b interface{}) error { return nil } + +func testStatus(t *testing.T, typ string) { + client := clients[typ] + resp, err := edbcli.Status(client) + if err != nil { + t.Fatal(err) + } + fmt.Println(resp) + if resp.NodeInfo.Network != chainID { + t.Fatal(fmt.Errorf("ChainID mismatch: got %s expected %s", + resp.NodeInfo.Network, chainID)) + } +} + +func testGenPriv(t *testing.T, typ string) { + client := clients[typ] + privAcc, err := edbcli.GenPrivAccount(client) + if err != nil { + t.Fatal(err) + } + if len(privAcc.Address) == 0 { + t.Fatal("Failed to generate an address") + } +} + +func testGetAccount(t *testing.T, typ string) { + acc := getAccount(t, typ, user[0].Address) + if acc == nil { + t.Fatalf("Account was nil") + } + if bytes.Compare(acc.Address, user[0].Address) != 0 { + t.Fatalf("Failed to get correct account. Got %x, expected %x", acc.Address, user[0].Address) + } +} + +func testSignedTx(t *testing.T, typ string) { + amt := int64(100) + toAddr := user[1].Address + testOneSignTx(t, typ, toAddr, amt) + + toAddr = user[2].Address + testOneSignTx(t, typ, toAddr, amt) + + toAddr = user[3].Address + testOneSignTx(t, typ, toAddr, amt) +} + +func testOneSignTx(t *testing.T, typ string, addr []byte, amt int64) { + tx := makeDefaultSendTx(t, typ, addr, amt) + tx2 := signTx(t, typ, tx, user[0]) + tx2hash := types.TxID(chainID, tx2) + tx.SignInput(chainID, 0, user[0]) + txhash := types.TxID(chainID, tx) + if bytes.Compare(txhash, tx2hash) != 0 { + t.Fatal("Got different signatures for signing via rpc vs tx_utils") + } + + tx_ := signTx(t, typ, tx, user[0]) + tx = tx_.(*types.SendTx) + checkTx(t, user[0].Address, user[0], tx) +} + +func testBroadcastTx(t *testing.T, typ string) { + amt := int64(100) + toAddr := user[1].Address + tx := makeDefaultSendTxSigned(t, typ, toAddr, amt) + receipt := broadcastTx(t, typ, tx) + if receipt.CreatesContract > 0 { + t.Fatal("This tx does not create a contract") + } + if len(receipt.TxHash) == 0 { + t.Fatal("Failed to compute tx hash") + } + pool := node.MempoolReactor().Mempool + txs := pool.Reap(-1) + if len(txs) != mempoolCount { + t.Fatalf("The mem pool has %d txs. Expected %d", len(txs), mempoolCount) + } + tx2 := types.DecodeTx(txs[mempoolCount-1]).(*types.SendTx) + n, err := new(int), new(error) + buf1, buf2 := new(bytes.Buffer), new(bytes.Buffer) + tx.WriteSignBytes(chainID, buf1, n, err) + tx2.WriteSignBytes(chainID, buf2, n, err) + if bytes.Compare(buf1.Bytes(), buf2.Bytes()) != 0 { + t.Fatal("inconsistent hashes for mempool tx and sent tx") + } +} + +func testGetStorage(t *testing.T, typ string) { + wsc := newWSClient(t) + eid := types.EventStringNewBlock() + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + + amt, gasLim, fee := int64(1100), int64(1000), int64(1000) + code := []byte{0x60, 0x5, 0x60, 0x1, 0x55} + tx := makeDefaultCallTx(t, typ, nil, code, amt, gasLim, fee) + receipt := broadcastTx(t, typ, tx) + if receipt.CreatesContract == 0 { + t.Fatal("This tx creates a contract") + } + if len(receipt.TxHash) == 0 { + t.Fatal("Failed to compute tx hash") + } + contractAddr := receipt.ContractAddr + if len(contractAddr) == 0 { + t.Fatal("Creates contract but resulting address is empty") + } + + // allow it to get mined + waitForEvent(t, wsc, eid, true, func() {}, doNothing) + mempoolCount = 0 + + v := getStorage(t, typ, contractAddr, []byte{0x1}) + got := LeftPadWord256(v) + expected := LeftPadWord256([]byte{0x5}) + if got.Compare(expected) != 0 { + t.Fatalf("Wrong storage value. Got %x, expected %x", got.Bytes(), expected.Bytes()) + } +} + +func testCallCode(t *testing.T, typ string) { + client := clients[typ] + + // add two integers and return the result + code := []byte{0x60, 0x5, 0x60, 0x6, 0x1, 0x60, 0x0, 0x52, 0x60, 0x20, 0x60, 0x0, 0xf3} + data := []byte{} + expected := []byte{0xb} + callCode(t, client, user[0].PubKey.Address(), code, data, expected) + + // pass two ints as calldata, add, and return the result + code = []byte{0x60, 0x0, 0x35, 0x60, 0x20, 0x35, 0x1, 0x60, 0x0, 0x52, 0x60, 0x20, 0x60, 0x0, 0xf3} + data = append(LeftPadWord256([]byte{0x5}).Bytes(), LeftPadWord256([]byte{0x6}).Bytes()...) + expected = []byte{0xb} + callCode(t, client, user[0].PubKey.Address(), code, data, expected) +} + +func testCall(t *testing.T, typ string) { + wsc := newWSClient(t) + eid := types.EventStringNewBlock() + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + + client := clients[typ] + + // create the contract + amt, gasLim, fee := int64(6969), int64(1000), int64(1000) + code, _, _ := simpleContract() + tx := makeDefaultCallTx(t, typ, nil, code, amt, gasLim, fee) + receipt := broadcastTx(t, typ, tx) + + if receipt.CreatesContract == 0 { + t.Fatal("This tx creates a contract") + } + if len(receipt.TxHash) == 0 { + t.Fatal("Failed to compute tx hash") + } + contractAddr := receipt.ContractAddr + if len(contractAddr) == 0 { + t.Fatal("Creates contract but resulting address is empty") + } + + // allow it to get mined + waitForEvent(t, wsc, eid, true, func() {}, doNothing) + mempoolCount = 0 + + // run a call through the contract + data := []byte{} + expected := []byte{0xb} + callContract(t, client, user[0].PubKey.Address(), contractAddr, data, expected) +} + +func testNameReg(t *testing.T, typ string) { + client := clients[typ] + wsc := newWSClient(t) + + types.MinNameRegistrationPeriod = 1 + + // register a new name, check if its there + // since entries ought to be unique and these run against different clients, we append the typ + name := "ye_old_domain_name_" + typ + data := "if not now, when" + fee := int64(1000) + numDesiredBlocks := int64(2) + amt := fee + numDesiredBlocks*types.NameByteCostMultiplier*types.NameBlockCostMultiplier*types.NameBaseCost(name, data) + + eid := types.EventStringNameReg(name) + subscribe(t, wsc, eid) + + tx := makeDefaultNameTx(t, typ, name, data, amt, fee) + broadcastTx(t, typ, tx) + // verify the name by both using the event and by checking get_name + waitForEvent(t, wsc, eid, true, func() {}, func(eid string, b interface{}) error { + // TODO: unmarshal the response + _ = b // TODO + tx, err := unmarshalResponseNameReg([]byte{}) + if err != nil { + return err + } + if tx.Name != name { + t.Fatal(fmt.Sprintf("Err on received event tx.Name: Got %s, expected %s", tx.Name, name)) + } + if tx.Data != data { + t.Fatal(fmt.Sprintf("Err on received event tx.Data: Got %s, expected %s", tx.Data, data)) + } + return nil + }) + mempoolCount = 0 + entry := getNameRegEntry(t, typ, name) + if entry.Data != data { + t.Fatal(fmt.Sprintf("Err on entry.Data: Got %s, expected %s", entry.Data, data)) + } + if bytes.Compare(entry.Owner, user[0].Address) != 0 { + t.Fatal(fmt.Sprintf("Err on entry.Owner: Got %s, expected %s", entry.Owner, user[0].Address)) + } + + unsubscribe(t, wsc, eid) + + // for the rest we just use new block event + // since we already tested the namereg event + eid = types.EventStringNewBlock() + subscribe(t, wsc, eid) + defer func() { + unsubscribe(t, wsc, eid) + wsc.Stop() + }() + + // update the data as the owner, make sure still there + numDesiredBlocks = int64(2) + data = "these are amongst the things I wish to bestow upon the youth of generations come: a safe supply of honey, and a better money. For what else shall they need" + amt = fee + numDesiredBlocks*types.NameByteCostMultiplier*types.NameBlockCostMultiplier*types.NameBaseCost(name, data) + tx = makeDefaultNameTx(t, typ, name, data, amt, fee) + broadcastTx(t, typ, tx) + // commit block + waitForEvent(t, wsc, eid, true, func() {}, doNothing) + mempoolCount = 0 + entry = getNameRegEntry(t, typ, name) + if entry.Data != data { + t.Fatal(fmt.Sprintf("Err on entry.Data: Got %s, expected %s", entry.Data, data)) + } + + // try to update as non owner, should fail + nonce := getNonce(t, typ, user[1].Address) + data2 := "this is not my beautiful house" + tx = types.NewNameTxWithNonce(user[1].PubKey, name, data2, amt, fee, nonce+1) + tx.Sign(chainID, user[1]) + _, err := edbcli.BroadcastTx(client, tx) + if err == nil { + t.Fatal("Expected error on NameTx") + } + + // commit block + waitForEvent(t, wsc, eid, true, func() {}, doNothing) + + // now the entry should be expired, so we can update as non owner + _, err = edbcli.BroadcastTx(client, tx) + waitForEvent(t, wsc, eid, true, func() {}, doNothing) + mempoolCount = 0 + entry = getNameRegEntry(t, typ, name) + if entry.Data != data2 { + t.Fatal(fmt.Sprintf("Error on entry.Data: Got %s, expected %s", entry.Data, data2)) + } + if bytes.Compare(entry.Owner, user[1].Address) != 0 { + t.Fatal(fmt.Sprintf("Err on entry.Owner: Got %s, expected %s", entry.Owner, user[1].Address)) + } +} diff --git a/rpc/test/ws_helpers.go b/rpc/test/ws_helpers.go new file mode 100644 index 00000000..5b0b88ed --- /dev/null +++ b/rpc/test/ws_helpers.go @@ -0,0 +1,278 @@ +package rpctest + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + "time" + + ctypes "github.com/eris-ltd/eris-db/rpc/core/types" + txtypes "github.com/eris-ltd/eris-db/txs" + "github.com/tendermint/tendermint/types" + + "github.com/tendermint/go-events" + client "github.com/tendermint/go-rpc/client" + rpctypes "github.com/tendermint/go-rpc/types" + "github.com/tendermint/go-wire" +) + +//-------------------------------------------------------------------------------- +// Utilities for testing the websocket service + +// create a new connection +func newWSClient(t *testing.T) *client.WSClient { + wsc := client.NewWSClient(websocketAddr, websocketEndpoint) + if _, err := wsc.Start(); err != nil { + t.Fatal(err) + } + return wsc +} + +// subscribe to an event +func subscribe(t *testing.T, wsc *client.WSClient, eventid string) { + if err := wsc.Subscribe(eventid); err != nil { + t.Fatal(err) + } +} + +// unsubscribe from an event +func unsubscribe(t *testing.T, wsc *client.WSClient, eventid string) { + if err := wsc.Unsubscribe(eventid); err != nil { + t.Fatal(err) + } +} + +// wait for an event; do things that might trigger events, and check them when they are received +// the check function takes an event id and the byte slice read off the ws +func waitForEvent(t *testing.T, wsc *client.WSClient, eventid string, dieOnTimeout bool, f func(), check func(string, interface{}) error) { + // go routine to wait for webscoket msg + goodCh := make(chan interface{}) + errCh := make(chan error) + + // Read message + go func() { + var err error + LOOP: + for { + select { + case r := <-wsc.ResultsCh: + result := new(ctypes.ErisDBResult) + wire.ReadJSONPtr(result, r, &err) + if err != nil { + errCh <- err + break LOOP + } + event, ok := (*result).(*ctypes.ResultEvent) + if ok && event.Event == eventid { + goodCh <- event.Data + break LOOP + } + case err := <-wsc.ErrorsCh: + errCh <- err + break LOOP + case <-wsc.Quit: + break LOOP + } + } + }() + + // do stuff (transactions) + f() + + // wait for an event or timeout + timeout := time.NewTimer(10 * time.Second) + select { + case <-timeout.C: + if dieOnTimeout { + wsc.Stop() + t.Fatalf("%s event was not received in time", eventid) + } + // else that's great, we didn't hear the event + // and we shouldn't have + case eventData := <-goodCh: + if dieOnTimeout { + // message was received and expected + // run the check + if err := check(eventid, eventData); err != nil { + t.Fatal(err) // Show the stack trace. + } + } else { + wsc.Stop() + t.Fatalf("%s event was not expected", eventid) + } + case err := <-errCh: + t.Fatal(err) + panic(err) // Show the stack trace. + + } +} + +//-------------------------------------------------------------------------------- + +func unmarshalResponseNewBlock(b []byte) (*types.Block, error) { + // unmarshall and assert somethings + var response rpctypes.RPCResponse + var err error + wire.ReadJSON(&response, b, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + // TODO + //block := response.Result.(*ctypes.ResultEvent).Data.(types.EventDataNewBlock).Block + // return block, nil + return nil, nil +} + +func unmarshalResponseNameReg(b []byte) (*txtypes.NameTx, error) { + // unmarshall and assert somethings + var response rpctypes.RPCResponse + var err error + wire.ReadJSON(&response, b, &err) + if err != nil { + return nil, err + } + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + _, val := UnmarshalEvent(*response.Result) + tx := txtypes.DecodeTx(val.(types.EventDataTx).Tx).(*txtypes.NameTx) + return tx, nil +} + +func unmarshalValidateBlockchain(t *testing.T, wsc *client.WSClient, eid string) { + var initBlockN int + for i := 0; i < 2; i++ { + waitForEvent(t, wsc, eid, true, func() {}, func(eid string, b interface{}) error { + block, err := unmarshalResponseNewBlock(b.([]byte)) + if err != nil { + return err + } + if i == 0 { + initBlockN = block.Header.Height + } else { + if block.Header.Height != initBlockN+i { + return fmt.Errorf("Expected block %d, got block %d", i, block.Header.Height) + } + } + + return nil + }) + } +} + +func unmarshalValidateSend(amt int64, toAddr []byte) func(string, interface{}) error { + return func(eid string, b interface{}) error { + // unmarshal and assert correctness + var response rpctypes.RPCResponse + var err error + wire.ReadJSON(&response, b.([]byte), &err) + if err != nil { + return err + } + if response.Error != "" { + return fmt.Errorf(response.Error) + } + event, val := UnmarshalEvent(*response.Result) + if eid != event { + return fmt.Errorf("Eventid is not correct. Got %s, expected %s", event, eid) + } + tx := txtypes.DecodeTx(val.(types.EventDataTx).Tx).(*txtypes.SendTx) + if !bytes.Equal(tx.Inputs[0].Address, user[0].Address) { + return fmt.Errorf("Senders do not match up! Got %x, expected %x", tx.Inputs[0].Address, user[0].Address) + } + if tx.Inputs[0].Amount != amt { + return fmt.Errorf("Amt does not match up! Got %d, expected %d", tx.Inputs[0].Amount, amt) + } + if !bytes.Equal(tx.Outputs[0].Address, toAddr) { + return fmt.Errorf("Receivers do not match up! Got %x, expected %x", tx.Outputs[0].Address, user[0].Address) + } + return nil + } +} + +func unmarshalValidateTx(amt int64, returnCode []byte) func(string, interface{}) error { + return func(eid string, b interface{}) error { + // unmarshall and assert somethings + var response rpctypes.RPCResponse + var err error + wire.ReadJSON(&response, b.([]byte), &err) + if err != nil { + return err + } + if response.Error != "" { + return fmt.Errorf(response.Error) + } + _, val := UnmarshalEvent(*response.Result) + var data = val.(txtypes.EventDataTx) + if data.Exception != "" { + return fmt.Errorf(data.Exception) + } + tx := data.Tx.(*txtypes.CallTx) + if !bytes.Equal(tx.Input.Address, user[0].Address) { + return fmt.Errorf("Senders do not match up! Got %x, expected %x", + tx.Input.Address, user[0].Address) + } + if tx.Input.Amount != amt { + return fmt.Errorf("Amt does not match up! Got %d, expected %d", + tx.Input.Amount, amt) + } + ret := data.Return + if !bytes.Equal(ret, returnCode) { + return fmt.Errorf("Tx did not return correctly. Got %x, expected %x", ret, returnCode) + } + return nil + } +} + +func unmarshalValidateCall(origin, returnCode []byte, txid *[]byte) func(string, interface{}) error { + return func(eid string, b interface{}) error { + // unmarshall and assert somethings + var response rpctypes.RPCResponse + var err error + wire.ReadJSON(&response, b.([]byte), &err) + if err != nil { + return err + } + if response.Error != "" { + return fmt.Errorf(response.Error) + } + _, val := UnmarshalEvent(*response.Result) + var data = val.(txtypes.EventDataCall) + if data.Exception != "" { + return fmt.Errorf(data.Exception) + } + if !bytes.Equal(data.Origin, origin) { + return fmt.Errorf("Origin does not match up! Got %x, expected %x", + data.Origin, origin) + } + ret := data.Return + if !bytes.Equal(ret, returnCode) { + return fmt.Errorf("Call did not return correctly. Got %x, expected %x", ret, returnCode) + } + if !bytes.Equal(data.TxID, *txid) { + return fmt.Errorf("TxIDs do not match up! Got %x, expected %x", + data.TxID, *txid) + } + return nil + } +} + +// Unmarshal a json event +func UnmarshalEvent(b json.RawMessage) (string, events.EventData) { + var err error + result := new(ctypes.ErisDBResult) + wire.ReadJSONPtr(result, b, &err) + if err != nil { + panic(err) + } + event, ok := (*result).(*ctypes.ResultEvent) + if !ok { + return "", nil // TODO: handle non-event messages (ie. return from subscribe/unsubscribe) + // fmt.Errorf("Result is not type *ctypes.ResultEvent. Got %v", reflect.TypeOf(*result)) + } + return event.Event, event.Data +} -- GitLab