package main import ( "bytes" "errors" "io" "math" "strings" "sync" "testing" "time" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/consensus" "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc/server" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" "github.com/urfave/cli" "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/term" ) const ( validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" validatorAddr = "NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP" multisigAddr = "NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq" testWalletPath = "testdata/testwallet.json" testWalletAccount = "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG" validatorWallet = "testdata/wallet1_solo.json" validatorPass = "one" ) var ( validatorHash, _ = address.StringToUint160(validatorAddr) validatorPriv, _ = keys.NewPrivateKeyFromWIF(validatorWIF) ) // executor represents context for a test instance. // It can be safely used in multiple tests, but not in parallel. type executor struct { // CLI is a cli application to test. CLI *cli.App // Chain is a blockchain instance (can be empty). Chain *core.Blockchain // RPC is an RPC server to query (can be empty). RPC *server.Server // NetSrv is a network server (can be empty). NetSrv *network.Server // Out contains command output. Out *ConcurrentBuffer // Err contains command errors. Err *bytes.Buffer // In contains command input. In *bytes.Buffer } // ConcurrentBuffer is a wrapper over Buffer with mutex. type ConcurrentBuffer struct { lock sync.RWMutex buf *bytes.Buffer } // NewConcurrentBuffer returns new ConcurrentBuffer with underlying buffer initialized. func NewConcurrentBuffer() *ConcurrentBuffer { return &ConcurrentBuffer{ buf: bytes.NewBuffer(nil), } } // Write is a concurrent wrapper over the corresponding method of bytes.Buffer. func (w *ConcurrentBuffer) Write(p []byte) (int, error) { w.lock.Lock() defer w.lock.Unlock() return w.buf.Write(p) } // ReadString is a concurrent wrapper over the corresponding method of bytes.Buffer. func (w *ConcurrentBuffer) ReadString(delim byte) (string, error) { w.lock.RLock() defer w.lock.RUnlock() return w.buf.ReadString(delim) } // Bytes is a concurrent wrapper over the corresponding method of bytes.Buffer. func (w *ConcurrentBuffer) Bytes() []byte { w.lock.RLock() defer w.lock.RUnlock() return w.buf.Bytes() } // String is a concurrent wrapper over the corresponding method of bytes.Buffer. func (w *ConcurrentBuffer) String() string { w.lock.RLock() defer w.lock.RUnlock() return w.buf.String() } // Reset is a concurrent wrapper over the corresponding method of bytes.Buffer. func (w *ConcurrentBuffer) Reset() { w.lock.Lock() defer w.lock.Unlock() w.buf.Reset() } func newTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockchain, *server.Server, *network.Server) { configPath := "../config/protocol.unit_testnet.single.yml" cfg, err := config.LoadFile(configPath) require.NoError(t, err, "could not load config") if f != nil { f(&cfg) } memoryStore := storage.NewMemoryStore() logger := zaptest.NewLogger(t) chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger) require.NoError(t, err, "could not create chain") if run { go chain.Run() } serverConfig := network.NewServerConfig(cfg) netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop()) require.NoError(t, err) cons, err := consensus.NewService(consensus.Config{ Logger: zap.NewNop(), Broadcast: netSrv.BroadcastExtensible, Chain: chain, ProtocolConfiguration: chain.GetConfig(), RequestTx: netSrv.RequestTx, Wallet: serverConfig.Wallet, TimePerBlock: serverConfig.TimePerBlock, }) require.NoError(t, err) netSrv.AddExtensibleHPService(cons, consensus.Category, cons.OnPayload, cons.OnTransaction) go netSrv.Start(make(chan error, 1)) rpcServer := server.New(chain, cfg.ApplicationConfiguration.RPC, netSrv, nil, logger) errCh := make(chan error, 2) rpcServer.Start(errCh) return chain, &rpcServer, netSrv } func newExecutor(t *testing.T, needChain bool) *executor { return newExecutorWithConfig(t, needChain, true, nil) } func newExecutorSuspended(t *testing.T) *executor { return newExecutorWithConfig(t, true, false, nil) } func newExecutorWithConfig(t *testing.T, needChain, runChain bool, f func(*config.Config)) *executor { e := &executor{ CLI: newApp(), Out: NewConcurrentBuffer(), Err: bytes.NewBuffer(nil), In: bytes.NewBuffer(nil), } e.CLI.Writer = e.Out e.CLI.ErrWriter = e.Err if needChain { e.Chain, e.RPC, e.NetSrv = newTestChain(t, f, runChain) } t.Cleanup(func() { e.Close(t) }) return e } func (e *executor) Close(t *testing.T) { input.Terminal = nil if e.RPC != nil { require.NoError(t, e.RPC.Shutdown()) } if e.NetSrv != nil { e.NetSrv.Shutdown() } if e.Chain != nil { e.Chain.Close() } } // GetTransaction returns tx with hash h after it has persisted. // If it is in mempool, we can just wait for the next block, otherwise // it must be already in chain. 1 second is time per block in a unittest chain. func (e *executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) { var tx *transaction.Transaction var height uint32 require.Eventually(t, func() bool { var err error tx, height, err = e.Chain.GetTransaction(h) return err == nil && height != math.MaxUint32 }, time.Second*2, time.Millisecond*100, "too long time waiting for block") return tx, height } func (e *executor) getNextLine(t *testing.T) string { line, err := e.Out.ReadString('\n') require.NoError(t, err) return strings.TrimSuffix(line, "\n") } func (e *executor) checkNextLine(t *testing.T, expected string) { line := e.getNextLine(t) e.checkLine(t, line, expected) } func (e *executor) checkLine(t *testing.T, line, expected string) { require.Regexp(t, expected, line) } func (e *executor) checkEOF(t *testing.T) { _, err := e.Out.ReadString('\n') require.True(t, errors.Is(err, io.EOF)) } func setExitFunc() <-chan int { ch := make(chan int, 1) cli.OsExiter = func(code int) { ch <- code } return ch } func checkExit(t *testing.T, ch <-chan int, code int) { select { case c := <-ch: require.Equal(t, code, c) default: if code != 0 { require.Fail(t, "no exit was called") } } } // RunWithError runs command and checks that is exits with error. func (e *executor) RunWithError(t *testing.T, args ...string) { ch := setExitFunc() require.Error(t, e.run(args...)) checkExit(t, ch, 1) } // Run runs command and checks that there were no errors. func (e *executor) Run(t *testing.T, args ...string) { ch := setExitFunc() require.NoError(t, e.run(args...)) checkExit(t, ch, 0) } func (e *executor) run(args ...string) error { e.Out.Reset() e.Err.Reset() input.Terminal = term.NewTerminal(input.ReadWriter{ Reader: e.In, Writer: io.Discard, }, "") err := e.CLI.Run(args) input.Terminal = nil e.In.Reset() return err } func (e *executor) checkTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) { line, err := e.Out.ReadString('\n') require.NoError(t, err) line = strings.TrimSpace(line) if len(prefix) > 0 { line = strings.TrimPrefix(line, prefix[0]) } h, err := util.Uint256DecodeStringLE(line) require.NoError(t, err, "can't decode tx hash: %s", line) tx, height := e.GetTransaction(t, h) aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application) require.NoError(t, err) require.Equal(t, 1, len(aer)) require.Equal(t, vm.HaltState, aer[0].VMState) return tx, height } func generateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) { privs := make([]*keys.PrivateKey, n) pubs := make(keys.PublicKeys, n) for i := range privs { var err error privs[i], err = keys.NewPrivateKey() require.NoError(t, err) pubs[i] = privs[i].PublicKey() } return privs, pubs }