github.com/palcoin-project/palcd@v1.0.0/integration/rpctest/rpc_harness.go (about)

     1  // Copyright (c) 2016-2017 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package rpctest
     6  
     7  import (
     8  	"fmt"
     9  	"io/ioutil"
    10  	"math/rand"
    11  	"net"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/palcoin-project/palcd/chaincfg"
    20  	"github.com/palcoin-project/palcd/chaincfg/chainhash"
    21  	"github.com/palcoin-project/palcd/rpcclient"
    22  	"github.com/palcoin-project/palcd/wire"
    23  	"github.com/palcoin-project/palcutil"
    24  )
    25  
    26  const (
    27  	// These constants define the minimum and maximum p2p and rpc port
    28  	// numbers used by a test harness.  The min port is inclusive while the
    29  	// max port is exclusive.
    30  	minPeerPort = 10000
    31  	maxPeerPort = 35000
    32  	minRPCPort  = maxPeerPort
    33  	maxRPCPort  = 60000
    34  
    35  	// BlockVersion is the default block version used when generating
    36  	// blocks.
    37  	BlockVersion = 4
    38  
    39  	// DefaultMaxConnectionRetries is the default number of times we re-try
    40  	// to connect to the node after starting it.
    41  	DefaultMaxConnectionRetries = 20
    42  
    43  	// DefaultConnectionRetryTimeout is the default duration we wait between
    44  	// two connection attempts.
    45  	DefaultConnectionRetryTimeout = 50 * time.Millisecond
    46  )
    47  
    48  var (
    49  	// current number of active test nodes.
    50  	numTestInstances = 0
    51  
    52  	// testInstances is a private package-level slice used to keep track of
    53  	// all active test harnesses. This global can be used to perform
    54  	// various "joins", shutdown several active harnesses after a test,
    55  	// etc.
    56  	testInstances = make(map[string]*Harness)
    57  
    58  	// Used to protest concurrent access to above declared variables.
    59  	harnessStateMtx sync.RWMutex
    60  
    61  	// ListenAddressGenerator is a function that is used to generate two
    62  	// listen addresses (host:port), one for the P2P listener and one for
    63  	// the RPC listener. This is exported to allow overwriting of the
    64  	// default behavior which isn't very concurrency safe (just selecting
    65  	// a random port can produce collisions and therefore flakes).
    66  	ListenAddressGenerator = generateListeningAddresses
    67  )
    68  
    69  // HarnessTestCase represents a test-case which utilizes an instance of the
    70  // Harness to exercise functionality.
    71  type HarnessTestCase func(r *Harness, t *testing.T)
    72  
    73  // Harness fully encapsulates an active btcd process to provide a unified
    74  // platform for creating rpc driven integration tests involving palcd. The
    75  // active btcd node will typically be run in simnet mode in order to allow for
    76  // easy generation of test blockchains.  The active btcd process is fully
    77  // managed by Harness, which handles the necessary initialization, and teardown
    78  // of the process along with any temporary directories created as a result.
    79  // Multiple Harness instances may be run concurrently, in order to allow for
    80  // testing complex scenarios involving multiple nodes. The harness also
    81  // includes an in-memory wallet to streamline various classes of tests.
    82  type Harness struct {
    83  	// ActiveNet is the parameters of the blockchain the Harness belongs
    84  	// to.
    85  	ActiveNet *chaincfg.Params
    86  
    87  	// MaxConnRetries is the maximum number of times we re-try to connect to
    88  	// the node after starting it.
    89  	MaxConnRetries int
    90  
    91  	// ConnectionRetryTimeout is the duration we wait between two connection
    92  	// attempts.
    93  	ConnectionRetryTimeout time.Duration
    94  
    95  	Client      *rpcclient.Client
    96  	BatchClient *rpcclient.Client
    97  	node        *node
    98  	handlers    *rpcclient.NotificationHandlers
    99  
   100  	wallet *memWallet
   101  
   102  	testNodeDir string
   103  	nodeNum     int
   104  
   105  	sync.Mutex
   106  }
   107  
   108  // New creates and initializes new instance of the rpc test harness.
   109  // Optionally, websocket handlers and a specified configuration may be passed.
   110  // In the case that a nil config is passed, a default configuration will be
   111  // used. If a custom btcd executable is specified, it will be used to start the
   112  // harness node. Otherwise a new binary is built on demand.
   113  //
   114  // NOTE: This function is safe for concurrent access.
   115  func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers,
   116  	extraArgs []string, customExePath string) (*Harness, error) {
   117  
   118  	harnessStateMtx.Lock()
   119  	defer harnessStateMtx.Unlock()
   120  
   121  	// Add a flag for the appropriate network type based on the provided
   122  	// chain params.
   123  	switch activeNet.Net {
   124  	case wire.MainNet:
   125  		// No extra flags since mainnet is the default
   126  	case wire.TestNet3:
   127  		extraArgs = append(extraArgs, "--testnet")
   128  	case wire.TestNet:
   129  		extraArgs = append(extraArgs, "--regtest")
   130  	case wire.SimNet:
   131  		extraArgs = append(extraArgs, "--simnet")
   132  	default:
   133  		return nil, fmt.Errorf("rpctest.New must be called with one " +
   134  			"of the supported chain networks")
   135  	}
   136  
   137  	testDir, err := baseDir()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	harnessID := strconv.Itoa(numTestInstances)
   143  	nodeTestData, err := ioutil.TempDir(testDir, "harness-"+harnessID)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	certFile := filepath.Join(nodeTestData, "rpc.cert")
   149  	keyFile := filepath.Join(nodeTestData, "rpc.key")
   150  	if err := genCertPair(certFile, keyFile); err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	wallet, err := newMemWallet(activeNet, uint32(numTestInstances))
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr)
   160  	extraArgs = append(extraArgs, miningAddr)
   161  
   162  	config, err := newConfig(
   163  		"rpctest", certFile, keyFile, extraArgs, customExePath,
   164  	)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	// Generate p2p+rpc listening addresses.
   170  	config.listen, config.rpcListen = ListenAddressGenerator()
   171  
   172  	// Create the testing node bounded to the simnet.
   173  	node, err := newNode(config, nodeTestData)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	nodeNum := numTestInstances
   179  	numTestInstances++
   180  
   181  	if handlers == nil {
   182  		handlers = &rpcclient.NotificationHandlers{}
   183  	}
   184  
   185  	// If a handler for the OnFilteredBlock{Connected,Disconnected} callback
   186  	// callback has already been set, then create a wrapper callback which
   187  	// executes both the currently registered callback and the mem wallet's
   188  	// callback.
   189  	if handlers.OnFilteredBlockConnected != nil {
   190  		obc := handlers.OnFilteredBlockConnected
   191  		handlers.OnFilteredBlockConnected = func(height int32, header *wire.BlockHeader, filteredTxns []*palcutil.Tx) {
   192  			wallet.IngestBlock(height, header, filteredTxns)
   193  			obc(height, header, filteredTxns)
   194  		}
   195  	} else {
   196  		// Otherwise, we can claim the callback ourselves.
   197  		handlers.OnFilteredBlockConnected = wallet.IngestBlock
   198  	}
   199  	if handlers.OnFilteredBlockDisconnected != nil {
   200  		obd := handlers.OnFilteredBlockDisconnected
   201  		handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) {
   202  			wallet.UnwindBlock(height, header)
   203  			obd(height, header)
   204  		}
   205  	} else {
   206  		handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock
   207  	}
   208  
   209  	h := &Harness{
   210  		handlers:               handlers,
   211  		node:                   node,
   212  		MaxConnRetries:         DefaultMaxConnectionRetries,
   213  		ConnectionRetryTimeout: DefaultConnectionRetryTimeout,
   214  		testNodeDir:            nodeTestData,
   215  		ActiveNet:              activeNet,
   216  		nodeNum:                nodeNum,
   217  		wallet:                 wallet,
   218  	}
   219  
   220  	// Track this newly created test instance within the package level
   221  	// global map of all active test instances.
   222  	testInstances[h.testNodeDir] = h
   223  
   224  	return h, nil
   225  }
   226  
   227  // SetUp initializes the rpc test state. Initialization includes: starting up a
   228  // simnet node, creating a websockets client and connecting to the started
   229  // node, and finally: optionally generating and submitting a testchain with a
   230  // configurable number of mature coinbase outputs coinbase outputs.
   231  //
   232  // NOTE: This method and TearDown should always be called from the same
   233  // goroutine as they are not concurrent safe.
   234  func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
   235  	// Start the btcd node itself. This spawns a new process which will be
   236  	// managed
   237  	if err := h.node.start(); err != nil {
   238  		return err
   239  	}
   240  	if err := h.connectRPCClient(); err != nil {
   241  		return err
   242  	}
   243  
   244  	h.wallet.Start()
   245  
   246  	// Filter transactions that pay to the coinbase associated with the
   247  	// wallet.
   248  	filterAddrs := []palcutil.Address{h.wallet.coinbaseAddr}
   249  	if err := h.Client.LoadTxFilter(true, filterAddrs, nil); err != nil {
   250  		return err
   251  	}
   252  
   253  	// Ensure btcd properly dispatches our registered call-back for each new
   254  	// block. Otherwise, the memWallet won't function properly.
   255  	if err := h.Client.NotifyBlocks(); err != nil {
   256  		return err
   257  	}
   258  
   259  	// Create a test chain with the desired number of mature coinbase
   260  	// outputs.
   261  	if createTestChain && numMatureOutputs != 0 {
   262  		numToGenerate := (uint32(h.ActiveNet.CoinbaseMaturity) +
   263  			numMatureOutputs)
   264  		_, err := h.Client.Generate(numToGenerate)
   265  		if err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	// Block until the wallet has fully synced up to the tip of the main
   271  	// chain.
   272  	_, height, err := h.Client.GetBestBlock()
   273  	if err != nil {
   274  		return err
   275  	}
   276  	ticker := time.NewTicker(time.Millisecond * 100)
   277  	for range ticker.C {
   278  		walletHeight := h.wallet.SyncedHeight()
   279  		if walletHeight == height {
   280  			break
   281  		}
   282  	}
   283  	ticker.Stop()
   284  
   285  	return nil
   286  }
   287  
   288  // tearDown stops the running rpc test instance.  All created processes are
   289  // killed, and temporary directories removed.
   290  //
   291  // This function MUST be called with the harness state mutex held (for writes).
   292  func (h *Harness) tearDown() error {
   293  	if h.Client != nil {
   294  		h.Client.Shutdown()
   295  	}
   296  
   297  	if h.BatchClient != nil {
   298  		h.BatchClient.Shutdown()
   299  	}
   300  
   301  	if err := h.node.shutdown(); err != nil {
   302  		return err
   303  	}
   304  
   305  	if err := os.RemoveAll(h.testNodeDir); err != nil {
   306  		return err
   307  	}
   308  
   309  	delete(testInstances, h.testNodeDir)
   310  
   311  	return nil
   312  }
   313  
   314  // TearDown stops the running rpc test instance. All created processes are
   315  // killed, and temporary directories removed.
   316  //
   317  // NOTE: This method and SetUp should always be called from the same goroutine
   318  // as they are not concurrent safe.
   319  func (h *Harness) TearDown() error {
   320  	harnessStateMtx.Lock()
   321  	defer harnessStateMtx.Unlock()
   322  
   323  	return h.tearDown()
   324  }
   325  
   326  // connectRPCClient attempts to establish an RPC connection to the created btcd
   327  // process belonging to this Harness instance. If the initial connection
   328  // attempt fails, this function will retry h.maxConnRetries times, backing off
   329  // the time between subsequent attempts. If after h.maxConnRetries attempts,
   330  // we're not able to establish a connection, this function returns with an
   331  // error.
   332  func (h *Harness) connectRPCClient() error {
   333  	var client, batchClient *rpcclient.Client
   334  	var err error
   335  
   336  	rpcConf := h.node.config.rpcConnConfig()
   337  	batchConf := h.node.config.rpcConnConfig()
   338  	batchConf.HTTPPostMode = true
   339  	for i := 0; i < h.MaxConnRetries; i++ {
   340  		fail := false
   341  		if client == nil {
   342  			if client, err = rpcclient.New(&rpcConf, h.handlers); err != nil {
   343  				time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout)
   344  				fail = true
   345  			}
   346  		}
   347  		if batchClient == nil {
   348  			if batchClient, err = rpcclient.NewBatch(&batchConf); err != nil {
   349  				time.Sleep(time.Duration(i) * h.ConnectionRetryTimeout)
   350  				fail = true
   351  			}
   352  		}
   353  		if !fail {
   354  			break
   355  		}
   356  	}
   357  
   358  	if client == nil || batchClient == nil {
   359  		return fmt.Errorf("connection timeout")
   360  	}
   361  
   362  	h.Client = client
   363  	h.wallet.SetRPCClient(client)
   364  	h.BatchClient = batchClient
   365  	return nil
   366  }
   367  
   368  // NewAddress returns a fresh address spendable by the Harness' internal
   369  // wallet.
   370  //
   371  // This function is safe for concurrent access.
   372  func (h *Harness) NewAddress() (palcutil.Address, error) {
   373  	return h.wallet.NewAddress()
   374  }
   375  
   376  // ConfirmedBalance returns the confirmed balance of the Harness' internal
   377  // wallet.
   378  //
   379  // This function is safe for concurrent access.
   380  func (h *Harness) ConfirmedBalance() palcutil.Amount {
   381  	return h.wallet.ConfirmedBalance()
   382  }
   383  
   384  // SendOutputs creates, signs, and finally broadcasts a transaction spending
   385  // the harness' available mature coinbase outputs creating new outputs
   386  // according to targetOutputs.
   387  //
   388  // This function is safe for concurrent access.
   389  func (h *Harness) SendOutputs(targetOutputs []*wire.TxOut,
   390  	feeRate palcutil.Amount) (*chainhash.Hash, error) {
   391  
   392  	return h.wallet.SendOutputs(targetOutputs, feeRate)
   393  }
   394  
   395  // SendOutputsWithoutChange creates and sends a transaction that pays to the
   396  // specified outputs while observing the passed fee rate and ignoring a change
   397  // output. The passed fee rate should be expressed in sat/b.
   398  //
   399  // This function is safe for concurrent access.
   400  func (h *Harness) SendOutputsWithoutChange(targetOutputs []*wire.TxOut,
   401  	feeRate palcutil.Amount) (*chainhash.Hash, error) {
   402  
   403  	return h.wallet.SendOutputsWithoutChange(targetOutputs, feeRate)
   404  }
   405  
   406  // CreateTransaction returns a fully signed transaction paying to the specified
   407  // outputs while observing the desired fee rate. The passed fee rate should be
   408  // expressed in satoshis-per-byte. The transaction being created can optionally
   409  // include a change output indicated by the change boolean. Any unspent outputs
   410  // selected as inputs for the crafted transaction are marked as unspendable in
   411  // order to avoid potential double-spends by future calls to this method. If the
   412  // created transaction is cancelled for any reason then the selected inputs MUST
   413  // be freed via a call to UnlockOutputs. Otherwise, the locked inputs won't be
   414  // returned to the pool of spendable outputs.
   415  //
   416  // This function is safe for concurrent access.
   417  func (h *Harness) CreateTransaction(targetOutputs []*wire.TxOut,
   418  	feeRate palcutil.Amount, change bool) (*wire.MsgTx, error) {
   419  
   420  	return h.wallet.CreateTransaction(targetOutputs, feeRate, change)
   421  }
   422  
   423  // UnlockOutputs unlocks any outputs which were previously marked as
   424  // unspendabe due to being selected to fund a transaction via the
   425  // CreateTransaction method.
   426  //
   427  // This function is safe for concurrent access.
   428  func (h *Harness) UnlockOutputs(inputs []*wire.TxIn) {
   429  	h.wallet.UnlockOutputs(inputs)
   430  }
   431  
   432  // RPCConfig returns the harnesses current rpc configuration. This allows other
   433  // potential RPC clients created within tests to connect to a given test
   434  // harness instance.
   435  func (h *Harness) RPCConfig() rpcclient.ConnConfig {
   436  	return h.node.config.rpcConnConfig()
   437  }
   438  
   439  // P2PAddress returns the harness' P2P listening address. This allows potential
   440  // peers (such as SPV peers) created within tests to connect to a given test
   441  // harness instance.
   442  func (h *Harness) P2PAddress() string {
   443  	return h.node.config.listen
   444  }
   445  
   446  // GenerateAndSubmitBlock creates a block whose contents include the passed
   447  // transactions and submits it to the running simnet node. For generating
   448  // blocks with only a coinbase tx, callers can simply pass nil instead of
   449  // transactions to be mined. Additionally, a custom block version can be set by
   450  // the caller. A blockVersion of -1 indicates that the current default block
   451  // version should be used. An uninitialized time.Time should be used for the
   452  // blockTime parameter if one doesn't wish to set a custom time.
   453  //
   454  // This function is safe for concurrent access.
   455  func (h *Harness) GenerateAndSubmitBlock(txns []*palcutil.Tx, blockVersion int32,
   456  	blockTime time.Time) (*palcutil.Block, error) {
   457  	return h.GenerateAndSubmitBlockWithCustomCoinbaseOutputs(txns,
   458  		blockVersion, blockTime, []wire.TxOut{})
   459  }
   460  
   461  // GenerateAndSubmitBlockWithCustomCoinbaseOutputs creates a block whose
   462  // contents include the passed coinbase outputs and transactions and submits
   463  // it to the running simnet node. For generating blocks with only a coinbase tx,
   464  // callers can simply pass nil instead of transactions to be mined.
   465  // Additionally, a custom block version can be set by the caller. A blockVersion
   466  // of -1 indicates that the current default block version should be used. An
   467  // uninitialized time.Time should be used for the blockTime parameter if one
   468  // doesn't wish to set a custom time. The mineTo list of outputs will be added
   469  // to the coinbase; this is not checked for correctness until the block is
   470  // submitted; thus, it is the caller's responsibility to ensure that the outputs
   471  // are correct. If the list is empty, the coinbase reward goes to the wallet
   472  // managed by the Harness.
   473  //
   474  // This function is safe for concurrent access.
   475  func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs(
   476  	txns []*palcutil.Tx, blockVersion int32, blockTime time.Time,
   477  	mineTo []wire.TxOut) (*palcutil.Block, error) {
   478  
   479  	h.Lock()
   480  	defer h.Unlock()
   481  
   482  	if blockVersion == -1 {
   483  		blockVersion = BlockVersion
   484  	}
   485  
   486  	prevBlockHash, prevBlockHeight, err := h.Client.GetBestBlock()
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  	mBlock, err := h.Client.GetBlock(prevBlockHash)
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  	prevBlock := palcutil.NewBlock(mBlock)
   495  	prevBlock.SetHeight(prevBlockHeight)
   496  
   497  	// Create a new block including the specified transactions
   498  	newBlock, err := CreateBlock(prevBlock, txns, blockVersion,
   499  		blockTime, h.wallet.coinbaseAddr, mineTo, h.ActiveNet)
   500  	if err != nil {
   501  		return nil, err
   502  	}
   503  
   504  	// Submit the block to the simnet node.
   505  	if err := h.Client.SubmitBlock(newBlock, nil); err != nil {
   506  		return nil, err
   507  	}
   508  
   509  	return newBlock, nil
   510  }
   511  
   512  // generateListeningAddresses returns two strings representing listening
   513  // addresses designated for the current rpc test. If there haven't been any
   514  // test instances created, the default ports are used. Otherwise, in order to
   515  // support multiple test nodes running at once, the p2p and rpc port are
   516  // picked at random between {min/max}PeerPort and {min/max}RPCPort respectively.
   517  func generateListeningAddresses() (string, string) {
   518  	localhost := "127.0.0.1"
   519  
   520  	rand.Seed(time.Now().UnixNano())
   521  
   522  	portString := func(minPort, maxPort int) string {
   523  		port := minPort + rand.Intn(maxPort-minPort)
   524  		return strconv.Itoa(port)
   525  	}
   526  
   527  	p2p := net.JoinHostPort(localhost, portString(minPeerPort, maxPeerPort))
   528  	rpc := net.JoinHostPort(localhost, portString(minRPCPort, maxRPCPort))
   529  	return p2p, rpc
   530  }
   531  
   532  // baseDir is the directory path of the temp directory for all rpctest files.
   533  func baseDir() (string, error) {
   534  	dirPath := filepath.Join(os.TempDir(), "palcd", "rpctest")
   535  	err := os.MkdirAll(dirPath, 0755)
   536  	return dirPath, err
   537  }