// Copyright (c) 2019-2020 The Zcash developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php . package frontend import ( "bufio" "bytes" "context" "encoding/hex" "encoding/json" "errors" "fmt" "io/ioutil" "os" "strings" "testing" "github.com/sirupsen/logrus" "github.com/zcash/lightwalletd/common" "github.com/zcash/lightwalletd/walletrpc" ) var ( testT *testing.T logger = logrus.New() step int blocks [][]byte // four test blocks rawTxData [][]byte ) const ( unitTestPath = "unittestcache" unitTestChain = "unittestnet" ) func testsetup() (walletrpc.CompactTxStreamerServer, *common.BlockCache) { os.RemoveAll(unitTestPath) cache := common.NewBlockCache(unitTestPath, unitTestChain, 380640, true) lwd, err := NewLwdStreamer(cache) if err != nil { os.Stderr.WriteString(fmt.Sprint("NewLwdStreamer failed:", err)) os.Exit(1) } return lwd, cache } func TestMain(m *testing.M) { output, err := os.OpenFile("test-log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { os.Stderr.WriteString(fmt.Sprint("Cannot open test-log:", err)) os.Exit(1) } logger.SetOutput(output) common.Log = logger.WithFields(logrus.Fields{ "app": "test", }) // Several tests need test blocks; read all 4 into memory just once // (for efficiency). testBlocks, err := os.Open("../testdata/blocks") if err != nil { os.Stderr.WriteString(fmt.Sprint("Error:", err)) os.Exit(1) } defer testBlocks.Close() scan := bufio.NewScanner(testBlocks) for scan.Scan() { // each line (block) blockJSON, _ := json.Marshal(scan.Text()) blocks = append(blocks, blockJSON) } testData, err := os.Open("../testdata/zip243_raw_tx") if err != nil { os.Stderr.WriteString(fmt.Sprint("Error:", err)) os.Exit(1) } defer testData.Close() // Parse the raw transactions file rawTxData = [][]byte{} scan = bufio.NewScanner(testData) for scan.Scan() { dataLine := scan.Text() // Skip the comments if strings.HasPrefix(dataLine, "#") { continue } txData, err := hex.DecodeString(dataLine) if err != nil { os.Stderr.WriteString(fmt.Sprint("Error:", err)) os.Exit(1) } rawTxData = append(rawTxData, txData) } // Setup is done; run all tests. exitcode := m.Run() // cleanup os.Remove("test-log") os.RemoveAll(unitTestPath) os.Exit(exitcode) } func TestGetTransaction(t *testing.T) { // GetTransaction() will mostly be tested below via TestGetTaddressTxids lwd, _ := testsetup() rawtx, err := lwd.GetTransaction(context.Background(), &walletrpc.TxFilter{}) if err == nil { testT.Fatal("GetTransaction unexpectedly succeeded") } if err.Error() != "Please call GetTransaction with txid" { testT.Fatal("GetTransaction unexpected error message") } if rawtx != nil { testT.Fatal("GetTransaction non-nil rawtx returned") } rawtx, err = lwd.GetTransaction(context.Background(), &walletrpc.TxFilter{Block: &walletrpc.BlockID{Hash: []byte{}}}) if err == nil { testT.Fatal("GetTransaction unexpectedly succeeded") } if err.Error() != "Can't GetTransaction with a blockhash+num. Please call GetTransaction with txid" { testT.Fatal("GetTransaction unexpected error message") } if rawtx != nil { testT.Fatal("GetTransaction non-nil rawtx returned") } } func getblockStub(method string, params []json.RawMessage) (json.RawMessage, error) { step++ var height string err := json.Unmarshal(params[0], &height) if err != nil { testT.Fatal("could not unmarshal height") } if height != "380640" { testT.Fatal("unexpected getblock height", height) } // Test retry logic (for the moment, it's very simple, just one retry). switch step { case 1: return blocks[0], nil case 2: return nil, errors.New("getblock test error") } testT.Fatal("unexpected call to getblockStub") return nil, nil } func TestGetLatestBlock(t *testing.T) { testT = t common.RawRequest = getblockStub lwd, cache := testsetup() // This argument is not used (it may be in the future) req := &walletrpc.ChainSpec{} blockID, err := lwd.GetLatestBlock(context.Background(), req) if err == nil { t.Fatal("GetLatestBlock should have failed, empty cache") } if err.Error() != "Cache is empty. Server is probably not yet ready" { t.Fatal("GetLatestBlock incorrect error", err) } if blockID != nil { t.Fatal("unexpected blockID", blockID) } // This does zcashd rpc "getblock", calls getblockStub() above block, err := common.GetBlock(cache, 380640) if err != nil { t.Fatal("getBlockFromRPC failed", err) } if err = cache.Add(380640, block); err != nil { t.Fatal("cache.Add failed:", err) } blockID, err = lwd.GetLatestBlock(context.Background(), req) if err != nil { t.Fatal("lwd.GetLatestBlock failed", err) } if blockID.Height != 380640 { t.Fatal("unexpected blockID.height") } step = 0 } // A valid address starts with "t", followed by 34 alpha characters; // these should all be detected as invalid. var addressTests = []string{ "", // too short "a", // too short "t123456789012345678901234567890123", // one byte too short "t12345678901234567890123456789012345", // one byte too long "t123456789012345678901234567890123*", // invalid "*" "s1234567890123456789012345678901234", // doesn't start with "t" " t1234567890123456789012345678901234", // extra stuff before "t1234567890123456789012345678901234 ", // extra stuff after "\nt1234567890123456789012345678901234", // newline before "t1234567890123456789012345678901234\n", // newline after } func zcashdrpcStub(method string, params []json.RawMessage) (json.RawMessage, error) { step++ switch method { case "getaddresstxids": var filter struct { Addresses []string Start float64 End float64 } err := json.Unmarshal(params[0], &filter) if err != nil { testT.Fatal("could not unmarshal block filter") } if len(filter.Addresses) != 1 { testT.Fatal("wrong number of addresses") } if filter.Addresses[0] != "t1234567890123456789012345678901234" { testT.Fatal("wrong address") } if filter.Start != 20 { testT.Fatal("wrong start") } if filter.End != 30 { testT.Fatal("wrong end") } return []byte("[\"6732cf8d67aac5b82a2a0f0217a7d4aa245b2adb0b97fd2d923dfc674415e221\"]"), nil case "getrawtransaction": switch step { case 2: tx := &struct { Hex string `json:"hex"` Height int `json:"height"` }{ Hex: hex.EncodeToString(rawTxData[0]), Height: 1234567, } return json.Marshal(tx) case 4: // empty return value, should be okay return []byte(""), errors.New("-5: test getrawtransaction error") } } testT.Fatal("unexpected call to zcashdrpcStub") return nil, nil } type testgettx struct { walletrpc.CompactTxStreamer_GetTaddressTxidsServer } func (tg *testgettx) Context() context.Context { return context.Background() } func (tg *testgettx) Send(tx *walletrpc.RawTransaction) error { if !bytes.Equal(tx.Data, rawTxData[0]) { testT.Fatal("mismatch transaction data") } if tx.Height != 1234567 { testT.Fatal("unexpected transaction height", tx.Height) } return nil } func TestGetTaddressTxids(t *testing.T) { testT = t common.RawRequest = zcashdrpcStub lwd, _ := testsetup() addressBlockFilter := &walletrpc.TransparentAddressBlockFilter{ Range: &walletrpc.BlockRange{ Start: &walletrpc.BlockID{Height: 20}, End: &walletrpc.BlockID{Height: 30}, }, } // Ensure that a bad address is detected for i, addressTest := range addressTests { addressBlockFilter.Address = addressTest err := lwd.GetTaddressTxids(addressBlockFilter, &testgettx{}) if err == nil { t.Fatal("GetTaddressTxids should have failed on bad address, case", i) } if err.Error() != "Invalid address" { t.Fatal("GetTaddressTxids incorrect error on bad address, case", i) } } // valid address addressBlockFilter.Address = "t1234567890123456789012345678901234" err := lwd.GetTaddressTxids(addressBlockFilter, &testgettx{}) if err != nil { t.Fatal("GetTaddressTxids failed", err) } // this time GetTransaction() will return an error err = lwd.GetTaddressTxids(addressBlockFilter, &testgettx{}) if err == nil { t.Fatal("GetTaddressTxids succeeded") } step = 0 } func TestGetBlock(t *testing.T) { testT = t common.RawRequest = getblockStub lwd, _ := testsetup() _, err := lwd.GetBlock(context.Background(), &walletrpc.BlockID{}) if err == nil { t.Fatal("GetBlock should have failed") } _, err = lwd.GetBlock(context.Background(), &walletrpc.BlockID{Height: 0}) if err == nil { t.Fatal("GetBlock should have failed") } _, err = lwd.GetBlock(context.Background(), &walletrpc.BlockID{Hash: []byte{0}}) if err == nil { t.Fatal("GetBlock should have failed") } if err.Error() != "GetBlock by Hash is not yet implemented" { t.Fatal("GetBlock hash unimplemented error message failed") } // getblockStub() case 1: return error block, err := lwd.GetBlock(context.Background(), &walletrpc.BlockID{Height: 380640}) if err != nil { t.Fatal("GetBlock failed:", err) } if block.Height != 380640 { t.Fatal("GetBlock returned unexpected block:", err) } // getblockStub() case 2: return error block, err = lwd.GetBlock(context.Background(), &walletrpc.BlockID{Height: 380640}) if err == nil { t.Fatal("GetBlock should have failed") } if block != nil { t.Fatal("GetBlock returned unexpected non-nil block") } step = 0 } type testgetbrange struct { walletrpc.CompactTxStreamer_GetTaddressTxidsServer } func (tg *testgetbrange) Context() context.Context { return context.Background() } func (tg *testgetbrange) Send(cb *walletrpc.CompactBlock) error { return nil } func TestGetBlockRange(t *testing.T) { testT = t common.RawRequest = getblockStub common.RawRequest = getblockStub lwd, _ := testsetup() blockrange := &walletrpc.BlockRange{ Start: &walletrpc.BlockID{Height: 380640}, End: &walletrpc.BlockID{Height: 380640}, } // getblockStub() case 1 (success) err := lwd.GetBlockRange(blockrange, &testgetbrange{}) if err != nil { t.Fatal("GetBlockRange failed", err) } // getblockStub() case 2 (failure) err = lwd.GetBlockRange(blockrange, &testgetbrange{}) if err == nil { t.Fatal("GetBlockRange should have failed") } step = 0 } func getblockchaininfoStub(method string, params []json.RawMessage) (json.RawMessage, error) { getsaplinginfo, _ := ioutil.ReadFile("../testdata/getsaplinginfo") getblockchaininfoReply, _ := hex.DecodeString(string(getsaplinginfo)) return getblockchaininfoReply, nil } func TestGetLightdInfo(t *testing.T) { testT = t common.RawRequest = getblockchaininfoStub lwd, _ := testsetup() ldinfo, err := lwd.GetLightdInfo(context.Background(), &walletrpc.Empty{}) if err != nil { t.Fatal("GetLightdInfo failed", err) } if ldinfo.Vendor != "ECC LightWalletD" { t.Fatal("GetLightdInfo: unexpected vendor", ldinfo) } step = 0 } func sendrawtransactionStub(method string, params []json.RawMessage) (json.RawMessage, error) { step++ if method != "sendrawtransaction" { testT.Fatal("unexpected method") } if string(params[0]) != "\"07\"" { testT.Fatal("unexpected tx data") } switch step { case 1: return []byte("sendtxresult"), nil case 2: return nil, errors.New("-17: some error") } testT.Fatal("unexpected call to sendrawtransactionStub") return nil, nil } func TestSendTransaction(t *testing.T) { testT = t lwd, _ := testsetup() common.RawRequest = sendrawtransactionStub rawtx := walletrpc.RawTransaction{Data: []byte{7}} sendresult, err := lwd.SendTransaction(context.Background(), &rawtx) if err != nil { t.Fatal("SendTransaction failed", err) } if sendresult.ErrorCode != 0 { t.Fatal("SendTransaction unexpected ErrorCode return") } if sendresult.ErrorMessage != "sendtxresult" { t.Fatal("SendTransaction unexpected ErrorMessage return") } // sendrawtransactionStub case 2 (error) // but note that the error is send within the response sendresult, err = lwd.SendTransaction(context.Background(), &rawtx) if err != nil { t.Fatal("SendTransaction failed:", err) } if sendresult.ErrorCode != -17 { t.Fatal("SendTransaction unexpected ErrorCode return") } if sendresult.ErrorMessage != "some error" { t.Fatal("SendTransaction unexpected ErrorMessage return") } step = 0 } var sampleconf = ` testnet = 1 rpcport = 18232 rpcbind = 127.0.0.1 rpcuser = testlightwduser rpcpassword = testlightwdpassword ` func TestNewZRPCFromConf(t *testing.T) { connCfg, err := connFromConf([]byte(sampleconf)) if err != nil { t.Fatal("connFromConf failed") } if connCfg.Host != "127.0.0.1:18232" { t.Fatal("connFromConf returned unexpected Host") } if connCfg.User != "testlightwduser" { t.Fatal("connFromConf returned unexpected User") } if connCfg.Pass != "testlightwdpassword" { t.Fatal("connFromConf returned unexpected User") } if !connCfg.HTTPPostMode { t.Fatal("connFromConf returned unexpected HTTPPostMode") } if !connCfg.DisableTLS { t.Fatal("connFromConf returned unexpected DisableTLS") } // can't pass an integer connCfg, err = connFromConf(10) if err == nil { t.Fatal("connFromConf unexpected success") } // Can't verify returned values, but at least run it _, err = NewZRPCFromConf([]byte(sampleconf)) if err != nil { t.Fatal("NewZRPCFromClient failed") } _, err = NewZRPCFromConf(10) if err == nil { t.Fatal("NewZRPCFromClient unexpected success") } } func TestMempoolFilter(t *testing.T) { txidlist := []string{ "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", "29e594c312eee49bc2c9ad37367ba58f857c4a7387ec9715", "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", } exclude := []string{ "98aa", // common prefix (98) but no match "19", // no match "29", // one match (should not appear) "d4", // 2 matches (both should appear in result) "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", // exact match "ce5a28854a509ab309faa433542e73414fef6e903a3d52f500", // extra stuff ignored } expected := []string{ "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", } actual := MempoolFilter(txidlist, exclude) if len(actual) != len(expected) { t.Fatal("mempool: wrong number of filter results") } for i := 0; i < len(actual); i++ { if actual[i] != expected[i] { t.Fatal(fmt.Sprintf("mempool: expected: %s actual: %s", expected[i], actual[i])) } } // If the exclude list is empty, return the entire mempool. actual = MempoolFilter(txidlist, []string{}) expected = []string{ "29e594c312eee49bc2c9ad37367ba58f857c4a7387ec9715", "2e819d0bab5c819dc7d5f92d1bfb4127ce321daf847f6602", "9839c1d4deca000656caff57c1f720f4fbd114b52239edde", "ce5a28854a509ab309faa433542e73414fef6e903a3d52f5", "d4714779c6dd32a72077bd79d4a70cb2153b552d7addec15", "d4d090e60bf9141c6573f0598b84cc1f9817543e55a4d84d", } if len(actual) != len(expected) { t.Fatal("mempool: wrong number of filter results") } for i := 0; i < len(actual); i++ { if actual[i] != expected[i] { t.Fatal(fmt.Sprintf("mempool: expected: %s actual: %s", expected[i], actual[i])) } } }