rpctest: create new rpctest package

This commit adds a new package (rpctest) which provides functionality
for writing automated black box tests to exercise the RPC interface.

An instance of a rpctest consists of an active btcd process running in
(typically) --simnet mode, a btcrpcclient instance connected to said
node, and finally an embedded in-memory wallet instance (the memWallet)
which manages any created coinbase outputs created by the mining btcd
node.

As part of the SetUp process for an RPC test, a test author can
optionally opt to have a test blockchain created. The second argument
to SetUp dictates the number of mature coinbase outputs desired. The
btcd process will then be directed to generate a test chain of length:
100 + numMatureOutputs.

The embedded memWallet instance acts as a minimal, simple wallet for
each Harness instance. The memWallet itself is a BIP 32 HD wallet
capable of creating new addresses, creating fully signed transactions,
creating+broadcasting a transaction paying to an arbitrary set of
outputs, and querying the currently confirmed balance.

In order to test various scenarios of blocks containing arbitrary
transactions, one can use the Generate rpc call via the exposed
btcrpcclient connected to the active btcd node. Additionally, the
Harness also exposes a secondary block generation API allowing callers
to create blocks with a set of hand-selected transactions, and an
arbitrary BlockVersion or Timestamp.

After execution of test logic TearDown should be called, allowing the
test instance to clean up created temporary directories, and shut down
the running processes.

Running multiple concurrent rpctest.Harness instances is supported in
order to allow for test authors to exercise complex scenarios. As a
result, the primary interface to create, and initialize an
rpctest.Harness instance is concurrent safe, with shared package level
private global variables protected by a sync.Mutex.

Fixes #116.
This commit is contained in:
Olaoluwa Osuntokun 2016-08-19 17:36:56 -05:00
parent 763f731c5c
commit b86df0ba91
No known key found for this signature in database
GPG key ID: 9CC5B105D03521A2
8 changed files with 2143 additions and 0 deletions

33
rpctest/README.md Normal file
View file

@ -0,0 +1,33 @@
rpctest
=======
[![Build Status](http://img.shields.io/travis/btcsuite/btcd.svg)]
(https://travis-ci.org/btcsuite/btcd) [![ISC License]
(http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)]
(http://godoc.org/github.com/btcsuite/btcd/rpctest)
Package rpctest provides a btcd-specific RPC testing harness crafting and
executing integration tests by driving a `btcd` instance via the `RPC`
interface. Each instance of an active harness comes equipped with a simple
in-memory HD wallet capable of properly syncing to the generated chain,
creating new addresses, and crafting fully signed transactions paying to an
arbitrary set of outputs.
This package was designed specifically to act as an RPC testing harness for
`btcd`. However, the constructs presented are general enough to be adapted to
any project wishing to programmatically drive a `btcd` instance of its
systems/integration tests.
## Installation and Updating
```bash
$ go get -u github.com/btcsuite/btcd/rpctest
```
## License
Package rpctest is licensed under the [copyfree](http://copyfree.org) ISC
License.

173
rpctest/blockgen.go Normal file
View file

@ -0,0 +1,173 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"errors"
"math"
"math/big"
"runtime"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
// solveBlock attempts to find a nonce which makes the passed block header hash
// to a value less than the target difficulty. When a successful solution is
// found true is returned and the nonce field of the passed header is updated
// with the solution. False is returned if no solution exists.
func solveBlock(header *wire.BlockHeader, targetDifficulty *big.Int) bool {
// sbResult is used by the solver goroutines to send results.
type sbResult struct {
found bool
nonce uint32
}
// solver accepts a block header and a nonce range to test. It is
// intended to be run as a goroutine.
quit := make(chan bool)
results := make(chan sbResult)
solver := func(hdr wire.BlockHeader, startNonce, stopNonce uint32) {
// We need to modify the nonce field of the header, so make sure
// we work with a copy of the original header.
for i := startNonce; i >= startNonce && i <= stopNonce; i++ {
select {
case <-quit:
return
default:
hdr.Nonce = i
hash := hdr.BlockHash()
if blockchain.HashToBig(&hash).Cmp(targetDifficulty) <= 0 {
results <- sbResult{true, i}
return
}
}
}
results <- sbResult{false, 0}
}
startNonce := uint32(0)
stopNonce := uint32(math.MaxUint32)
numCores := uint32(runtime.NumCPU())
noncesPerCore := (stopNonce - startNonce) / numCores
for i := uint32(0); i < numCores; i++ {
rangeStart := startNonce + (noncesPerCore * i)
rangeStop := startNonce + (noncesPerCore * (i + 1)) - 1
if i == numCores-1 {
rangeStop = stopNonce
}
go solver(*header, rangeStart, rangeStop)
}
for i := uint32(0); i < numCores; i++ {
result := <-results
if result.found {
close(quit)
header.Nonce = result.nonce
return true
}
}
return false
}
// standardCoinbaseScript returns a standard script suitable for use as the
// signature script of the coinbase transaction of a new block. In particular,
// it starts with the block height that is required by version 2 blocks.
func standardCoinbaseScript(nextBlockHeight int32, extraNonce uint64) ([]byte, error) {
return txscript.NewScriptBuilder().AddInt64(int64(nextBlockHeight)).
AddInt64(int64(extraNonce)).Script()
}
// createCoinbaseTx returns a coinbase transaction paying an appropriate
// subsidy based on the passed block height to the provided address.
func createCoinbaseTx(coinbaseScript []byte, nextBlockHeight int32,
addr btcutil.Address, net *chaincfg.Params) (*btcutil.Tx, error) {
// Create the script to pay to the provided payment address.
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, err
}
tx := wire.NewMsgTx()
tx.AddTxIn(&wire.TxIn{
// Coinbase transactions have no inputs, so previous outpoint is
// zero hash and max index.
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
wire.MaxPrevOutIndex),
SignatureScript: coinbaseScript,
Sequence: wire.MaxTxInSequenceNum,
})
tx.AddTxOut(&wire.TxOut{
Value: blockchain.CalcBlockSubsidy(nextBlockHeight, net),
PkScript: pkScript,
})
return btcutil.NewTx(tx), nil
}
// createBlock creates a new block building from the previous block.
func createBlock(prevBlock *btcutil.Block, inclusionTxs []*btcutil.Tx,
blockVersion int32, blockTime time.Time,
miningAddr btcutil.Address, net *chaincfg.Params) (*btcutil.Block, error) {
prevHash := prevBlock.Hash()
blockHeight := prevBlock.Height() + 1
// If a target block time was specified, then use that as the header's
// timestamp. Otherwise, add one second to the previous block unless
// it's the genesis block in which case use the current time.
var ts time.Time
switch {
case !blockTime.IsZero():
ts = blockTime
default:
ts = prevBlock.MsgBlock().Header.Timestamp.Add(time.Second)
}
extraNonce := uint64(0)
coinbaseScript, err := standardCoinbaseScript(blockHeight, extraNonce)
if err != nil {
return nil, err
}
coinbaseTx, err := createCoinbaseTx(coinbaseScript, blockHeight,
miningAddr, net)
if err != nil {
return nil, err
}
// Create a new block ready to be solved.
blockTxns := []*btcutil.Tx{coinbaseTx}
if inclusionTxs != nil {
blockTxns = append(blockTxns, inclusionTxs...)
}
merkles := blockchain.BuildMerkleTreeStore(blockTxns)
var block wire.MsgBlock
block.Header = wire.BlockHeader{
Version: blockVersion,
PrevBlock: *prevHash,
MerkleRoot: *merkles[len(merkles)-1],
Timestamp: ts,
Bits: net.PowLimitBits,
}
for _, tx := range blockTxns {
if err := block.AddTransaction(tx.MsgTx()); err != nil {
return nil, err
}
}
found := solveBlock(&block.Header, net.PowLimit)
if !found {
return nil, errors.New("Unable to solve block")
}
utilBlock := btcutil.NewBlock(&block)
utilBlock.SetHeight(blockHeight)
return utilBlock, nil
}

12
rpctest/doc.go Normal file
View file

@ -0,0 +1,12 @@
// Package rpctest provides a btcd-specific RPC testing harness crafting and
// executing integration tests by driving a `btcd` instance via the `RPC`
// interface. Each instance of an active harness comes equipped with a simple
// in-memory HD wallet capable of properly syncing to the generated chain,
// creating new addresses, and crafting fully signed transactions paying to an
// arbitrary set of outputs.
//
// This package was designed specifically to act as an RPC testing harness for
// `btcd`. However, the constructs presented are general enough to be adapted to
// any project wishing to programmatically drive a `btcd` instance of its
// systems/integration tests.
package rpctest

543
rpctest/memwallet.go Normal file
View file

@ -0,0 +1,543 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"bytes"
"encoding/binary"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
)
var (
// hdSeed is the BIP 32 seed used by the memWallet to initialize it's
// HD root key. This value is hard coded in order to ensure
// deterministic behavior across test runs.
hdSeed = [chainhash.HashSize]byte{
0x79, 0xa6, 0x1a, 0xdb, 0xc6, 0xe5, 0xa2, 0xe1,
0x39, 0xd2, 0x71, 0x3a, 0x54, 0x6e, 0xc7, 0xc8,
0x75, 0x63, 0x2e, 0x75, 0xf1, 0xdf, 0x9c, 0x3f,
0xa6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
)
// utxo represents an unspent output spendable by the memWallet. The maturity
// height of the transaction is recorded in order to properly observe the
// maturity period of direct coinbase outputs.
type utxo struct {
pkScript []byte
value btcutil.Amount
keyIndex uint32
maturityHeight int32
isLocked bool
}
// isMature returns true if the target utxo is considered "mature" at the
// passed block height. Otherwise, false is returned.
func (u *utxo) isMature(height int32) bool {
return height >= u.maturityHeight
}
// chainUpdate encapsulates an update to the current main chain. This struct is
// used to sync up the memWallet each time a new block is connected to the main
// chain.
type chainUpdate struct {
blockHash *chainhash.Hash
blockHeight int32
}
// undoEntry is functionally the opposite of a chainUpdate. An undoEntry is
// created for each new block received, then stored in a log in order to
// properly handle block re-orgs.
type undoEntry struct {
utxosDestroyed map[wire.OutPoint]*utxo
utxosCreated []wire.OutPoint
}
// memWallet is a simple in-memory wallet whose purpose is to provide basic
// wallet functionality to the harness. The wallet uses a hard-coded HD key
// hierarchy which promotes reproducibility between harness test runs.
type memWallet struct {
coinbaseKey *btcec.PrivateKey
coinbaseAddr btcutil.Address
// hdRoot is the root master private key for the wallet.
hdRoot *hdkeychain.ExtendedKey
// hdIndex is the next available key index offset from the hdRoot.
hdIndex uint32
// currentHeight is the latest height the wallet is known to be synced
// to.
currentHeight int32
// addrs tracks all addresses belonging to the wallet. The addresses
// are indexed by their keypath from the hdRoot.
addrs map[uint32]btcutil.Address
// utxos is the set of utxos spendable by the wallet.
utxos map[wire.OutPoint]*utxo
// reorgJournal is a map storing an undo entry for each new block
// received. Once a block is disconnected, the undo entry for the
// particular height is evaluated, thereby rewinding the effect of the
// disconnected block on the wallet's set of spendable utxos.
reorgJournal map[int32]*undoEntry
chainUpdates []*chainUpdate
chainUpdateSignal chan struct{}
chainMtx sync.Mutex
net *chaincfg.Params
rpc *btcrpcclient.Client
sync.RWMutex
}
// newMemWallet creates and returns a fully initialized instance of the
// memWallet given a particular blockchain's parameters.
func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) {
// The wallet's final HD seed is: hdSeed || harnessID. This method
// ensures that each harness instance uses a deterministic root seed
// based on its harness ID.
var harnessHDSeed [chainhash.HashSize + 4]byte
copy(harnessHDSeed[:], hdSeed[:])
binary.BigEndian.PutUint32(harnessHDSeed[:chainhash.HashSize], harnessID)
hdRoot, err := hdkeychain.NewMaster(harnessHDSeed[:], net)
if err != nil {
return nil, nil
}
// The first child key from the hd root is reserved as the coinbase
// generation address.
coinbaseChild, err := hdRoot.Child(0)
if err != nil {
return nil, err
}
coinbaseKey, err := coinbaseChild.ECPrivKey()
if err != nil {
return nil, err
}
coinbaseAddr, err := keyToAddr(coinbaseKey, net)
if err != nil {
return nil, err
}
// Track the coinbase generation address to ensure we properly track
// newly generated bitcoin we can spend.
addrs := make(map[uint32]btcutil.Address)
addrs[0] = coinbaseAddr
return &memWallet{
net: net,
coinbaseKey: coinbaseKey,
coinbaseAddr: coinbaseAddr,
hdIndex: 1,
hdRoot: hdRoot,
addrs: addrs,
utxos: make(map[wire.OutPoint]*utxo),
chainUpdateSignal: make(chan struct{}),
reorgJournal: make(map[int32]*undoEntry),
}, nil
}
// Start launches all goroutines required for the wallet to function properly.
func (m *memWallet) Start() {
go m.chainSyncer()
}
// SyncedHeight returns the height the wallet is known to be synced to.
//
// This function is safe for concurrent access.
func (m *memWallet) SyncedHeight() int32 {
m.RLock()
defer m.RUnlock()
return m.currentHeight
}
// SetRPCClient saves the passed rpc connection to btcd as the wallet's
// personal rpc connection.
func (m *memWallet) SetRPCClient(rpcClient *btcrpcclient.Client) {
m.rpc = rpcClient
}
// IngestBlock is a call-back which is to be triggered each time a new block is
// connected to the main chain. Ingesting a block updates the wallet's internal
// utxo state based on the outputs created and destroyed within each block.
func (m *memWallet) IngestBlock(blockHash *chainhash.Hash, height int32, t time.Time) {
// Append this new chain update to the end of the queue of new chain
// updates.
m.chainMtx.Lock()
m.chainUpdates = append(m.chainUpdates, &chainUpdate{blockHash, height})
m.chainMtx.Unlock()
// Launch a goroutine to signal the chainSyncer that a new update is
// available. We do this in a new goroutine in order to avoid blocking
// the main loop of the rpc client.
go func() {
m.chainUpdateSignal <- struct{}{}
}()
}
// chainSyncer is a goroutine dedicated to processing new blocks in order to
// keep the wallet's utxo state up to date.
//
// NOTE: This MUST be run as a goroutine.
func (m *memWallet) chainSyncer() {
var update *chainUpdate
for range m.chainUpdateSignal {
// A new update is available, so pop the new chain update from
// the front of the update queue.
m.chainMtx.Lock()
update = m.chainUpdates[0]
m.chainUpdates[0] = nil // Set to nil to prevent GC leak.
m.chainUpdates = m.chainUpdates[1:]
m.chainMtx.Unlock()
// Fetch the new block so we can process it shortly below.
block, err := m.rpc.GetBlock(update.blockHash)
if err != nil {
return
}
// Update the latest synced height, then process each
// transaction in the block creating and destroying utxos
// within the wallet as a result.
m.Lock()
m.currentHeight = update.blockHeight
undo := &undoEntry{
utxosDestroyed: make(map[wire.OutPoint]*utxo),
}
for _, tx := range block.Transactions() {
mtx := tx.MsgTx()
isCoinbase := blockchain.IsCoinBaseTx(mtx)
m.evalOutputs(mtx.TxOut, tx.Hash(), isCoinbase, undo)
m.evalInputs(mtx.TxIn, undo)
}
// Finally, record the undo entry for this block so we can
// properly update our internal state in response to the block
// being re-org'd from the main chain.
m.reorgJournal[update.blockHeight] = undo
m.Unlock()
}
}
// evalOutputs evaluates each of the passed outputs, creating a new matching
// utxo within the wallet if we're able to spend the output.
func (m *memWallet) evalOutputs(outputs []*wire.TxOut, txHash *chainhash.Hash,
isCoinbase bool, undo *undoEntry) {
for i, output := range outputs {
pkScript := output.PkScript
// Scan all the addresses we currently control to see if the
// output is paying to us.
for keyIndex, addr := range m.addrs {
pkHash := addr.ScriptAddress()
if !bytes.Contains(pkScript, pkHash) {
continue
}
// If this is a coinbase output, then we mark the
// maturity height at the proper block height in the
// future.
var maturityHeight int32
if isCoinbase {
maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity)
}
op := wire.OutPoint{Hash: *txHash, Index: uint32(i)}
m.utxos[op] = &utxo{
value: btcutil.Amount(output.Value),
keyIndex: keyIndex,
maturityHeight: maturityHeight,
pkScript: pkScript,
}
undo.utxosCreated = append(undo.utxosCreated, op)
}
}
}
// evalInputs scans all the passed inputs, destroying any utxos within the
// wallet which are spent by an input.
func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) {
for _, txIn := range inputs {
op := txIn.PreviousOutPoint
oldUtxo, ok := m.utxos[op]
if !ok {
continue
}
undo.utxosDestroyed[op] = oldUtxo
delete(m.utxos, op)
}
}
// UnwindBlock is a call-back which is to be executed each time a block is
// disconnected from the main chain. Unwinding a block undoes the effect that a
// particular block had on the wallet's internal utxo state.
func (m *memWallet) UnwindBlock(hash *chainhash.Hash, height int32, t time.Time) {
m.Lock()
defer m.Unlock()
undo := m.reorgJournal[height]
for _, utxo := range undo.utxosCreated {
delete(m.utxos, utxo)
}
for outPoint, utxo := range undo.utxosDestroyed {
m.utxos[outPoint] = utxo
}
delete(m.reorgJournal, height)
}
// newAddress returns a new address from the wallet's hd key chain.
func (m *memWallet) newAddress() (btcutil.Address, error) {
index := m.hdIndex
childKey, err := m.hdRoot.Child(index)
if err != nil {
return nil, err
}
privKey, err := childKey.ECPrivKey()
if err != nil {
return nil, err
}
addr, err := keyToAddr(privKey, m.net)
if err != nil {
return nil, err
}
m.addrs[index] = addr
m.hdIndex++
return addr, nil
}
// NewAddress returns a fresh address spendable by the wallet.
//
// This function is safe for concurrent access.
func (m *memWallet) NewAddress() (btcutil.Address, error) {
m.Lock()
defer m.Unlock()
return m.newAddress()
}
// fundTx attempts to fund a transaction sending amt bitcoin. The coins are
// selected such that the final amount spent pays enough fees as dictated by
// the passed fee rate. The passed fee rate should be expressed in
// satoshis-per-byte.
//
// NOTE: The memWallet's mutex must be held when this function is called.
func (m *memWallet) fundTx(tx *wire.MsgTx, amt btcutil.Amount, feeRate btcutil.Amount) error {
const (
// spendSize is the largest number of bytes of a sigScript
// which spends a p2pkh output: OP_DATA_73 <sig> OP_DATA_33 <pubkey>
spendSize = 1 + 73 + 1 + 33
)
var (
amtSelected btcutil.Amount
txSize int
)
for outPoint, utxo := range m.utxos {
// Skip any outputs that are still currently immature or are
// currently locked.
if !utxo.isMature(m.currentHeight) || utxo.isLocked {
continue
}
amtSelected += utxo.value
// Add the selected output to the transaction, updating the
// current tx size while accounting for the size of the future
// sigScript.
tx.AddTxIn(wire.NewTxIn(&outPoint, nil))
txSize = tx.SerializeSize() + spendSize*len(tx.TxIn)
// Calculate the fee required for the txn at this point
// observing the specified fee rate. If we don't have enough
// coins from he current amount selected to pay the fee, then
// continue to grab more coins.
reqFee := btcutil.Amount(txSize * int(feeRate))
if amtSelected-reqFee < amt {
continue
}
// If we have any change left over, then add an additional
// output to the transaction reserved for change.
changeVal := amtSelected - amt - reqFee
if changeVal > 0 {
addr, err := m.newAddress()
if err != nil {
return err
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return err
}
changeOutput := &wire.TxOut{
Value: int64(changeVal),
PkScript: pkScript,
}
tx.AddTxOut(changeOutput)
}
return nil
}
// If we've reached this point, then coin selection failed due to an
// insufficient amount of coins.
return fmt.Errorf("not enough funds for coin selection")
}
// SendOutputs creates, then sends a transaction paying to the specified output
// while observing the passed fee rate. The passed fee rate should be expressed
// in satoshis-per-byte.
func (m *memWallet) SendOutputs(outputs []*wire.TxOut,
feeRate btcutil.Amount) (*chainhash.Hash, error) {
tx, err := m.CreateTransaction(outputs, feeRate)
if err != nil {
return nil, err
}
return m.rpc.SendRawTransaction(tx, true)
}
// CreateTransaction returns a fully signed transaction paying to the specified
// outputs while observing the desired fee rate. The passed fee rate should be
// expressed in satoshis-per-byte.
//
// This function is safe for concurrent access.
func (m *memWallet) CreateTransaction(outputs []*wire.TxOut, feeRate btcutil.Amount) (*wire.MsgTx, error) {
m.Lock()
defer m.Unlock()
tx := wire.NewMsgTx()
// Tally up the total amount to be sent in order to perform coin
// selection shortly below.
var outputAmt btcutil.Amount
for _, output := range outputs {
outputAmt += btcutil.Amount(output.Value)
tx.AddTxOut(output)
}
// Attempt to fund the transaction with spendable utxos.
if err := m.fundTx(tx, outputAmt, btcutil.Amount(feeRate)); err != nil {
return nil, err
}
// Populate all the selected inputs with valid sigScript for spending.
// Along the way record all outputs being spent in order to avoid a
// potential double spend.
spentOutputs := make([]*utxo, 0, len(tx.TxIn))
for i, txIn := range tx.TxIn {
outPoint := txIn.PreviousOutPoint
utxo := m.utxos[outPoint]
extendedKey, err := m.hdRoot.Child(utxo.keyIndex)
if err != nil {
return nil, err
}
privKey, err := extendedKey.ECPrivKey()
if err != nil {
return nil, err
}
sigScript, err := txscript.SignatureScript(tx, i, utxo.pkScript,
txscript.SigHashAll, privKey, true)
if err != nil {
return nil, err
}
txIn.SignatureScript = sigScript
spentOutputs = append(spentOutputs, utxo)
}
// As these outputs are now being spent by this newly created
// transaction, mark the outputs are "locked". This action ensures
// these outputs won't be double spent by any subsequent transactions.
// These locked outputs can be freed via a call to UnlockOutputs.
for _, utxo := range spentOutputs {
utxo.isLocked = true
}
return tx, nil
}
// UnlockOutputs unlocks any outputs which were previously locked due to
// being selected to fund a transaction via the CreateTransaction method.
//
// This function is safe for concurrent access.
func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) {
m.Lock()
defer m.Unlock()
for _, input := range inputs {
utxo, ok := m.utxos[input.PreviousOutPoint]
if !ok {
continue
}
utxo.isLocked = false
}
}
// ConfirmedBalance returns the confirmed balance of the wallet.
//
// This function is safe for concurrent access.
func (m *memWallet) ConfirmedBalance() btcutil.Amount {
m.RLock()
defer m.RUnlock()
var balance btcutil.Amount
for _, utxo := range m.utxos {
// Prevent any immature or locked outputs from contributing to
// the wallet's total confirmed balance.
if !utxo.isMature(m.currentHeight) || utxo.isLocked {
continue
}
balance += utxo.value
}
return balance
}
// keyToAddr maps the passed private to corresponding p2pkh address.
func keyToAddr(key *btcec.PrivateKey, net *chaincfg.Params) (btcutil.Address, error) {
serializedKey := key.PubKey().SerializeCompressed()
pubKeyAddr, err := btcutil.NewAddressPubKey(serializedKey, net)
if err != nil {
return nil, err
}
return pubKeyAddr.AddressPubKeyHash(), nil
}

292
rpctest/node.go Normal file
View file

@ -0,0 +1,292 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/btcsuite/btcd/wire"
rpc "github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil"
)
// nodeConfig contains all the args, and data required to launch a btcd process
// and connect the rpc client to it.
type nodeConfig struct {
rpcUser string
rpcPass string
listen string
rpcListen string
rpcConnect string
dataDir string
logDir string
profile string
debugLevel string
extra []string
prefix string
exe string
endpoint string
certFile string
keyFile string
certificates []byte
}
// newConfig returns a newConfig with all default values.
func newConfig(prefix, certFile, keyFile string, extra []string) (*nodeConfig, error) {
a := &nodeConfig{
listen: "127.0.0.1:18555",
rpcListen: "127.0.0.1:18556",
rpcUser: "user",
rpcPass: "pass",
extra: extra,
prefix: prefix,
exe: "btcd",
endpoint: "ws",
certFile: certFile,
keyFile: keyFile,
}
if err := a.setDefaults(); err != nil {
return nil, err
}
return a, nil
}
// setDefaults sets the default values of the config. It also creates the
// temporary data, and log directories which must be cleaned up with a call to
// cleanup().
func (n *nodeConfig) setDefaults() error {
datadir, err := ioutil.TempDir("", n.prefix+"-data")
if err != nil {
return err
}
n.dataDir = datadir
logdir, err := ioutil.TempDir("", n.prefix+"-logs")
if err != nil {
return err
}
n.logDir = logdir
cert, err := ioutil.ReadFile(n.certFile)
if err != nil {
return err
}
n.certificates = cert
return nil
}
// arguments returns an array of arguments that be used to launch the btcd
// process.
func (n *nodeConfig) arguments() []string {
args := []string{}
// --simnet
args = append(args, fmt.Sprintf("--%s", strings.ToLower(wire.SimNet.String())))
if n.rpcUser != "" {
// --rpcuser
args = append(args, fmt.Sprintf("--rpcuser=%s", n.rpcUser))
}
if n.rpcPass != "" {
// --rpcpass
args = append(args, fmt.Sprintf("--rpcpass=%s", n.rpcPass))
}
if n.listen != "" {
// --listen
args = append(args, fmt.Sprintf("--listen=%s", n.listen))
}
if n.rpcListen != "" {
// --rpclisten
args = append(args, fmt.Sprintf("--rpclisten=%s", n.rpcListen))
}
if n.rpcConnect != "" {
// --rpcconnect
args = append(args, fmt.Sprintf("--rpcconnect=%s", n.rpcConnect))
}
// --rpccert
args = append(args, fmt.Sprintf("--rpccert=%s", n.certFile))
// --rpckey
args = append(args, fmt.Sprintf("--rpckey=%s", n.keyFile))
// --txindex
args = append(args, "--txindex")
// --addrindex
args = append(args, "--addrindex")
if n.dataDir != "" {
// --datadir
args = append(args, fmt.Sprintf("--datadir=%s", n.dataDir))
}
if n.logDir != "" {
// --logdir
args = append(args, fmt.Sprintf("--logdir=%s", n.logDir))
}
if n.profile != "" {
// --profile
args = append(args, fmt.Sprintf("--profile=%s", n.profile))
}
if n.debugLevel != "" {
// --debuglevel
args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel))
}
args = append(args, n.extra...)
return args
}
// command returns the exec.Cmd which will be used to start the btcd process.
func (n *nodeConfig) command() *exec.Cmd {
return exec.Command(n.exe, n.arguments()...)
}
// rpcConnConfig returns the rpc connection config that can be used to connect
// to the btcd process that is launched via Start().
func (n *nodeConfig) rpcConnConfig() rpc.ConnConfig {
return rpc.ConnConfig{
Host: n.rpcListen,
Endpoint: n.endpoint,
User: n.rpcUser,
Pass: n.rpcPass,
Certificates: n.certificates,
DisableAutoReconnect: true,
}
}
// String returns the string representation of this nodeConfig.
func (n *nodeConfig) String() string {
return n.prefix
}
// cleanup removes the tmp data and log directories.
func (n *nodeConfig) cleanup() error {
dirs := []string{
n.logDir,
n.dataDir,
}
var err error
for _, dir := range dirs {
if err = os.RemoveAll(dir); err != nil {
log.Printf("Cannot remove dir %s: %v", dir, err)
}
}
return err
}
// node houses the necessary state required to configure, launch, and manage a
// btcd process.
type node struct {
config *nodeConfig
cmd *exec.Cmd
pidFile string
dataDir string
}
// newNode creates a new node instance according to the passed config. dataDir
// will be used to hold a file recording the pid of the launched process, and
// as the base for the log and data directories for btcd.
func newNode(config *nodeConfig, dataDir string) (*node, error) {
return &node{
config: config,
dataDir: dataDir,
cmd: config.command(),
}, nil
}
// start creates a new btcd process, and writes its pid in a file reserved for
// recording the pid of the launched process. This file can be used to
// terminate the process in case of a hang, or panic. In the case of a failing
// test case, or panic, it is important that the process be stopped via stop(),
// otherwise, it will persist unless explicitly killed.
func (n *node) start() error {
if err := n.cmd.Start(); err != nil {
return err
}
pid, err := os.Create(filepath.Join(n.dataDir,
fmt.Sprintf("%s.pid", n.config)))
if err != nil {
return err
}
n.pidFile = pid.Name()
if _, err = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); err != nil {
return err
}
if err := pid.Close(); err != nil {
return err
}
return nil
}
// stop interrupts the running btcd process process, and waits until it exits
// properly. On windows, interrupt is not supported, so a kill signal is used
// instead
func (n *node) stop() error {
if n.cmd == nil || n.cmd.Process == nil {
// return if not properly initialized
// or error starting the process
return nil
}
defer n.cmd.Wait()
if runtime.GOOS == "windows" {
return n.cmd.Process.Signal(os.Kill)
}
return n.cmd.Process.Signal(os.Interrupt)
}
// cleanup cleanups process and args files. The file housing the pid of the
// created process will be deleted, as well as any directories created by the
// process.
func (n *node) cleanup() error {
if n.pidFile != "" {
if err := os.Remove(n.pidFile); err != nil {
log.Printf("unable to remove file %s: %v", n.pidFile,
err)
}
}
return n.config.cleanup()
}
// shutdown terminates the running btcd process, and cleans up all
// file/directories created by node.
func (n *node) shutdown() error {
if err := n.stop(); err != nil {
return err
}
if err := n.cleanup(); err != nil {
return err
}
return nil
}
// genCertPair generates a key/cert pair to the paths provided.
func genCertPair(certFile, keyFile string) error {
org := "rpctest autogenerated cert"
validUntil := time.Now().Add(10 * 365 * 24 * time.Hour)
cert, key, err := btcutil.NewTLSCertPair(org, validUntil, nil)
if err != nil {
return err
}
// Write cert and key files.
if err = ioutil.WriteFile(certFile, cert, 0666); err != nil {
return err
}
if err = ioutil.WriteFile(keyFile, key, 0600); err != nil {
os.Remove(certFile)
return err
}
return nil
}

409
rpctest/rpc_harness.go Normal file
View file

@ -0,0 +1,409 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil"
)
var (
// current number of active test nodes.
numTestInstances = 0
// defaultP2pPort is the initial p2p port which will be used by the
// first created rpc harnesses to listen on for incoming p2p
// connections. Subsequent allocated ports for future rpc harness
// instances will be monotonically increasing odd numbers calculated as
// such: defaultP2pPort + (2 * harness.nodeNum).
defaultP2pPort = 18555
// defaultRPCPort is the initial rpc port which will be used by the
// first created rpc harnesses to listen on for incoming rpc
// connections. Subsequent allocated ports for future rpc harness
// instances will be monotonically increasing even numbers calculated
// as such: defaultP2pPort + (2 * harness.nodeNum).
defaultRPCPort = 18556
// testInstances is a private package-level slice used to keep track of
// all active test harnesses. This global can be used to perform
// various "joins", shutdown several active harnesses after a test,
// etc.
testInstances = make(map[string]*Harness)
// Used to protest concurrent access to above declared variables.
harnessStateMtx sync.RWMutex
)
// HarnessTestCase represents a test-case which utilizes an instance of the
// Harness to exercise functionality.
type HarnessTestCase func(r *Harness, t *testing.T)
// Harness fully encapsulates an active btcd process to provide a unified
// platform for creating rpc driven integration tests involving btcd. The
// active btcd node will typically be run in simnet mode in order to allow for
// easy generation of test blockchains. The active btcd process is fully
// managed by Harness, which handles the necessary initialization, and teardown
// of the process along with any temporary directories created as a result.
// Multiple Harness instances may be run concurrently, in order to allow for
// testing complex scenarios involving multiple nodes. The harness also
// includes an in-memory wallet to streamline various classes of tests.
type Harness struct {
// ActiveNet is the parameters of the blockchain the Harness belongs
// to.
ActiveNet *chaincfg.Params
Node *btcrpcclient.Client
node *node
handlers *btcrpcclient.NotificationHandlers
wallet *memWallet
testNodeDir string
maxConnRetries int
nodeNum int
sync.Mutex
}
// New creates and initializes new instance of the rpc test harness.
// Optionally, websocket handlers and a specified configuration may be passed.
// In the case that a nil config is passed, a default configuration will be
// used.
//
// NOTE: This function is safe for concurrent access.
func New(activeNet *chaincfg.Params, handlers *btcrpcclient.NotificationHandlers,
extraArgs []string) (*Harness, error) {
harnessStateMtx.Lock()
defer harnessStateMtx.Unlock()
harnessID := strconv.Itoa(int(numTestInstances))
nodeTestData, err := ioutil.TempDir("", "rpctest-"+harnessID)
if err != nil {
return nil, err
}
certFile := filepath.Join(nodeTestData, "rpc.cert")
keyFile := filepath.Join(nodeTestData, "rpc.key")
if err := genCertPair(certFile, keyFile); err != nil {
return nil, err
}
wallet, err := newMemWallet(activeNet, uint32(numTestInstances))
if err != nil {
return nil, err
}
miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr)
extraArgs = append(extraArgs, miningAddr)
config, err := newConfig("rpctest", certFile, keyFile, extraArgs)
if err != nil {
return nil, err
}
// Generate p2p+rpc listening addresses.
config.listen, config.rpcListen = generateListeningAddresses()
// Create the testing node bounded to the simnet.
node, err := newNode(config, nodeTestData)
if err != nil {
return nil, err
}
nodeNum := numTestInstances
numTestInstances++
if handlers == nil {
handlers = &btcrpcclient.NotificationHandlers{}
}
// If a handler for the OnBlockConnected/OnBlockDisconnected callback
// has already been set, then we create a wrapper callback which
// executes both the currently registered callback, and the mem
// wallet's callback.
if handlers.OnBlockConnected != nil {
obc := handlers.OnBlockConnected
handlers.OnBlockConnected = func(hash *chainhash.Hash, height int32, t time.Time) {
wallet.IngestBlock(hash, height, t)
obc(hash, height, t)
}
} else {
// Otherwise, we can claim the callback ourselves.
handlers.OnBlockConnected = wallet.IngestBlock
}
if handlers.OnBlockDisconnected != nil {
obd := handlers.OnBlockConnected
handlers.OnBlockDisconnected = func(hash *chainhash.Hash, height int32, t time.Time) {
wallet.UnwindBlock(hash, height, t)
obd(hash, height, t)
}
} else {
handlers.OnBlockDisconnected = wallet.UnwindBlock
}
h := &Harness{
handlers: handlers,
node: node,
maxConnRetries: 20,
testNodeDir: nodeTestData,
ActiveNet: activeNet,
nodeNum: nodeNum,
wallet: wallet,
}
// Track this newly created test instance within the package level
// global map of all active test instances.
testInstances[h.testNodeDir] = h
return h, nil
}
// SetUp initializes the rpc test state. Initialization includes: starting up a
// simnet node, creating a websockets client and connecting to the started
// node, and finally: optionally generating and submitting a testchain with a
// configurable number of mature coinbase outputs coinbase outputs.
//
// NOTE: This method and TearDown should always be called from the same
// goroutine as they are not concurrent safe.
func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error {
// Start the btcd node itself. This spawns a new process which will be
// managed
if err := h.node.start(); err != nil {
return err
}
if err := h.connectRPCClient(); err != nil {
return err
}
h.wallet.Start()
// Ensure the btcd properly dispatches our registered call-back for
// each new block. Otherwise, the memWallet won't function properly.
if err := h.Node.NotifyBlocks(); err != nil {
return err
}
// Create a test chain with the desired number of mature coinbase
// outputs.
if createTestChain && numMatureOutputs != 0 {
numToGenerate := (uint32(h.ActiveNet.CoinbaseMaturity) +
numMatureOutputs)
_, err := h.Node.Generate(numToGenerate)
if err != nil {
return err
}
}
// Block until the wallet has fully synced up to the tip of the main
// chain.
_, height, err := h.Node.GetBestBlock()
if err != nil {
return err
}
ticker := time.NewTicker(time.Millisecond * 100)
out:
for {
select {
case <-ticker.C:
walletHeight := h.wallet.SyncedHeight()
if walletHeight == height {
break out
}
}
}
return nil
}
// TearDown stops the running rpc test instance. All created processes are
// killed, and temporary directories removed.
//
// NOTE: This method and SetUp should always be called from the same goroutine
// as they are not concurrent safe.
func (h *Harness) TearDown() error {
if h.Node != nil {
h.Node.Shutdown()
}
if err := h.node.shutdown(); err != nil {
return err
}
if err := os.RemoveAll(h.testNodeDir); err != nil {
return err
}
delete(testInstances, h.testNodeDir)
return nil
}
// connectRPCClient attempts to establish an RPC connection to the created btcd
// process belonging to this Harness instance. If the initial connection
// attempt fails, this function will retry h.maxConnRetries times, backing off
// the time between subsequent attempts. If after h.maxConnRetries attempts,
// we're not able to establish a connection, this function returns with an
// error.
func (h *Harness) connectRPCClient() error {
var client *btcrpcclient.Client
var err error
rpcConf := h.node.config.rpcConnConfig()
for i := 0; i < h.maxConnRetries; i++ {
if client, err = btcrpcclient.New(&rpcConf, h.handlers); err != nil {
time.Sleep(time.Duration(i) * 50 * time.Millisecond)
continue
}
break
}
if client == nil {
return fmt.Errorf("connection timeout")
}
h.Node = client
h.wallet.SetRPCClient(client)
return nil
}
// NewAddress returns a fresh address spendable by the Harness' internal
// wallet.
//
// This function is safe for concurrent access.
func (h *Harness) NewAddress() (btcutil.Address, error) {
return h.wallet.NewAddress()
}
// ConfirmedBalance returns the confirmed balance of the Harness' internal
// wallet.
//
// This function is safe for concurrent access.
func (h *Harness) ConfirmedBalance() btcutil.Amount {
return h.wallet.ConfirmedBalance()
}
// SendOutputs creates, signs, and finally broadcasts a transaction spending
// the harness' available mature coinbase outputs creating new outputs
// according to targetOutputs.
//
// This function is safe for concurrent access.
func (h *Harness) SendOutputs(targetOutputs []*wire.TxOut,
feeRate btcutil.Amount) (*chainhash.Hash, error) {
return h.wallet.SendOutputs(targetOutputs, feeRate)
}
// CreateTransaction returns a fully signed transaction paying to the specified
// outputs while observing the desired fee rate. The passed fee rate should be
// expressed in satoshis-per-byte. Any unspent outputs selected as inputs for
// the crafted transaction are marked as unspendable in order to avoid
// potential double-spends by future calls to this method. If the created
// transaction is cancelled for any reason then the selected inputs MUST be
// freed via a call to UnlockOutputs. Otherwise, the locked inputs won't be
// returned to the pool of spendable outputs.
//
// This function is safe for concurrent access.
func (h *Harness) CreateTransaction(targetOutputs []*wire.TxOut,
feeRate btcutil.Amount) (*wire.MsgTx, error) {
return h.wallet.CreateTransaction(targetOutputs, feeRate)
}
// UnlockOutputs unlocks any outputs which were previously marked as
// unspendabe due to being selected to fund a transaction via the
// CreateTransaction method.
//
// This function is safe for concurrent access.
func (h *Harness) UnlockOutputs(inputs []*wire.TxIn) {
h.wallet.UnlockOutputs(inputs)
}
// RPCConfig returns the harnesses current rpc configuration. This allows other
// potential RPC clients created within tests to connect to a given test
// harness instance.
func (h *Harness) RPCConfig() btcrpcclient.ConnConfig {
return h.node.config.rpcConnConfig()
}
// GenerateAndSubmitBlock creates a block whose contents include the passed
// transactions and submits it to the running simnet node. For generating
// blocks with only a coinbase tx, callers can simply pass nil instead of
// transactions to be mined. Additionally, a custom block version can be set by
// the caller. A blockVersion of -1 indicates that the current default block
// version should be used. An uninitialized time.Time should be used for the
// blockTime parameter if one doesn't wish to set a custom time.
//
// This function is safe for concurrent access.
func (h *Harness) GenerateAndSubmitBlock(txns []*btcutil.Tx, blockVersion int32,
blockTime time.Time) (*btcutil.Block, error) {
h.Lock()
defer h.Unlock()
if blockVersion == -1 {
blockVersion = wire.BlockVersion
}
prevBlockHash, prevBlockHeight, err := h.Node.GetBestBlock()
if err != nil {
return nil, err
}
prevBlock, err := h.Node.GetBlock(prevBlockHash)
if err != nil {
return nil, err
}
prevBlock.SetHeight(prevBlockHeight)
// Create a new block including the specified transactions
newBlock, err := createBlock(prevBlock, txns, blockVersion,
blockTime, h.wallet.coinbaseAddr, h.ActiveNet)
if err != nil {
return nil, err
}
// Submit the block to the simnet node.
if err := h.Node.SubmitBlock(newBlock, nil); err != nil {
return nil, err
}
return newBlock, nil
}
// generateListeningAddresses returns two strings representing listening
// addresses designated for the current rpc test. If there haven't been any
// test instances created, the default ports are used. Otherwise, in order to
// support multiple test nodes running at once, the p2p and rpc port are
// incremented after each initialization.
func generateListeningAddresses() (string, string) {
var p2p, rpc string
localhost := "127.0.0.1"
if numTestInstances == 0 {
p2p = net.JoinHostPort(localhost, strconv.Itoa(defaultP2pPort))
rpc = net.JoinHostPort(localhost, strconv.Itoa(defaultRPCPort))
} else {
p2p = net.JoinHostPort(localhost,
strconv.Itoa(defaultP2pPort+(2*numTestInstances)))
rpc = net.JoinHostPort(localhost,
strconv.Itoa(defaultRPCPort+(2*numTestInstances)))
}
return p2p, rpc
}

512
rpctest/rpc_harness_test.go Normal file
View file

@ -0,0 +1,512 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"fmt"
"net"
"os"
"strconv"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
func testSendOutputs(r *Harness, t *testing.T) {
genSpend := func(amt btcutil.Amount) *chainhash.Hash {
// Grab a fresh address from the wallet.
addr, err := r.NewAddress()
if err != nil {
t.Fatalf("unable to get new address: %v", err)
}
// Next, send amt BTC to this address, spending from one of our mature
// coinbase outputs.
addrScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("unable to generate pkscript to addr: %v", err)
}
output := wire.NewTxOut(int64(amt), addrScript)
txid, err := r.SendOutputs([]*wire.TxOut{output}, 10)
if err != nil {
t.Fatalf("coinbase spend failed: %v", err)
}
return txid
}
assertTxMined := func(txid *chainhash.Hash, blockHash *chainhash.Hash) {
block, err := r.Node.GetBlock(blockHash)
if err != nil {
t.Fatalf("unable to get block: %v", err)
}
numBlockTxns := len(block.Transactions())
if numBlockTxns < 2 {
t.Fatalf("crafted transaction wasn't mined, block should have "+
"at least %v transactions instead has %v", 2, numBlockTxns)
}
minedTx := block.Transactions()[1]
txHash := minedTx.Hash()
if *txHash != *txid {
t.Fatalf("txid's don't match, %v vs %v", txHash, txid)
}
}
// First, generate a small spend which will require only a single
// input.
txid := genSpend(btcutil.Amount(5 * btcutil.SatoshiPerBitcoin))
// Generate a single block, the transaction the wallet created should
// be found in this block.
blockHashes, err := r.Node.Generate(1)
if err != nil {
t.Fatalf("unable to generate single block: %v", err)
}
assertTxMined(txid, blockHashes[0])
// Next, generate a spend much greater than the block reward. This
// transaction should also have been mined properly.
txid = genSpend(btcutil.Amount(500 * btcutil.SatoshiPerBitcoin))
blockHashes, err = r.Node.Generate(1)
if err != nil {
t.Fatalf("unable to generate single block: %v", err)
}
assertTxMined(txid, blockHashes[0])
}
func assertConnectedTo(t *testing.T, nodeA *Harness, nodeB *Harness) {
nodePort := defaultP2pPort + (2 * nodeB.nodeNum)
nodeAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(nodePort))
nodeAPeers, err := nodeA.Node.GetPeerInfo()
if err != nil {
t.Fatalf("unable to get nodeA's peer info")
}
addrFound := false
for _, peerInfo := range nodeAPeers {
if peerInfo.Addr == nodeAddr {
addrFound = true
break
}
}
if !addrFound {
t.Fatal("nodeA not connected to nodeB")
}
}
func testConnectNode(r *Harness, t *testing.T) {
// Create a fresh test harnesses.
harness, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := harness.SetUp(true, 0); err != nil {
t.Fatalf("unable to complete rpctest setup: %v", err)
}
defer harness.TearDown()
// Establish a p2p connection the main harness to our new local
// harness.
if err := ConnectNode(r, harness); err != nil {
t.Fatalf("unable to connect harness1 to harness2: %v", err)
}
// The main harness should show up in our loca harness' peer's list,
// and vice verse.
assertConnectedTo(t, r, harness)
}
func testTearDownAll(t *testing.T) {
// Grab a local copy of the currently active harnesses before
// attempting to tear them all down.
initialActiveHarnesses := ActiveHarnesses()
// Tear down all currently active harnesses.
if err := TearDownAll(); err != nil {
t.Fatalf("unable to teardown all harnesses: %v", err)
}
// The global testInstances map should now be fully purged with no
// active test harnesses remaining.
if len(ActiveHarnesses()) != 0 {
t.Fatalf("test harnesses still active after TearDownAll")
}
for _, harness := range initialActiveHarnesses {
// Ensure all test directories have been deleted.
if _, err := os.Stat(harness.testNodeDir); err == nil {
t.Errorf("created test datadir was not deleted.")
}
}
}
func testActiveHarnesses(r *Harness, t *testing.T) {
numInitialHarnesses := len(ActiveHarnesses())
// Create a single test harness.
harness1, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
defer harness1.TearDown()
// With the harness created above, a single harness should be detected
// as active.
numActiveHarnesses := len(ActiveHarnesses())
if !(numActiveHarnesses > numInitialHarnesses) {
t.Fatalf("ActiveHarnesses not updated, should have an " +
"additional test harness listed.")
}
}
func testJoinMempools(r *Harness, t *testing.T) {
// Create a new local test harnesses, starting at the same height.
harness, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := harness.SetUp(true, 25); err != nil {
t.Fatalf("unable to complete rpctest setup: %v", err)
}
defer harness.TearDown()
nodeSlice := []*Harness{r, harness}
// Both mempools should be considered synced as they are empty.
// Therefore, this should return instantly.
if err := JoinNodes(nodeSlice, Mempools); err != nil {
t.Fatalf("unable to join node on block height: %v", err)
}
// Generate a coinbase spend to a new address within harness1's
// mempool.
addr, err := harness.NewAddress()
addrScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("unable to generate pkscript to addr: %v", err)
}
output := wire.NewTxOut(5e8, addrScript)
if _, err = harness.SendOutputs([]*wire.TxOut{output}, 10); err != nil {
t.Fatalf("coinbase spend failed: %v", err)
}
poolsSynced := make(chan struct{})
go func() {
if err := JoinNodes(nodeSlice, Mempools); err != nil {
t.Fatalf("unable to join node on node mempools: %v", err)
}
poolsSynced <- struct{}{}
}()
// This select case should fall through to the default as the goroutine
// should be blocked on the JoinNodes calls.
select {
case <-poolsSynced:
t.Fatalf("mempools detected as synced yet harness1 has a new tx")
default:
}
// Establish an outbound connection from harness1 to harness2. After
// the initial handshake both nodes should exchange inventory resulting
// in a synced mempool.
if err := ConnectNode(r, harness); err != nil {
t.Fatalf("unable to connect harnesses: %v", err)
}
// Select once again with a special timeout case after 1 minute. The
// goroutine above should now be blocked on sending into the unbuffered
// channel. The send should immediately succeed. In order to avoid the
// test hanging indefinitely, a 1 minute timeout is in place.
select {
case <-poolsSynced:
// fall through
case <-time.After(time.Minute):
t.Fatalf("block heights never detected as synced")
}
}
func testJoinBlocks(r *Harness, t *testing.T) {
// Create two test harnesses, with one being 5 block ahead of the other
// with respect to block height.
harness1, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := harness1.SetUp(true, 30); err != nil {
t.Fatalf("unable to complete rpctest setup: %v", err)
}
defer harness1.TearDown()
harness2, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := harness2.SetUp(true, 25); err != nil {
t.Fatalf("unable to complete rpctest setup: %v", err)
}
defer harness2.TearDown()
nodeSlice := []*Harness{harness1, harness2}
blocksSynced := make(chan struct{})
go func() {
if err := JoinNodes(nodeSlice, Blocks); err != nil {
t.Fatalf("unable to join node on block height: %v", err)
}
blocksSynced <- struct{}{}
}()
// This select case should fall through to the default as the goroutine
// should be blocked on the JoinNodes calls.
select {
case <-blocksSynced:
t.Fatalf("blocks detected as synced yet harness2 is 5 blocks behind")
default:
}
// Extend harness2's chain by 5 blocks, this should cause JoinNodes to
// finally unblock and return.
if _, err := harness2.Node.Generate(5); err != nil {
t.Fatalf("unable to generate blocks: %v", err)
}
// Select once again with a special timeout case after 1 minute. The
// goroutine above should now be blocked on sending into the unbuffered
// channel. The send should immediately succeed. In order to avoid the
// test hanging indefinitely, a 1 minute timeout is in place.
select {
case <-blocksSynced:
// fall through
case <-time.After(time.Minute):
t.Fatalf("block heights never detected as synced")
}
}
func testGenerateAndSubmitBlock(r *Harness, t *testing.T) {
// Generate a few test spend transactions.
addr, err := r.NewAddress()
if err != nil {
t.Fatalf("unable to generate new address: %v", err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("unable to create script: %v", err)
}
output := wire.NewTxOut(btcutil.SatoshiPerBitcoin, pkScript)
const numTxns = 5
txns := make([]*btcutil.Tx, 0, numTxns)
for i := 0; i < numTxns; i++ {
tx, err := r.CreateTransaction([]*wire.TxOut{output}, 10)
if err != nil {
t.Fatalf("unable to create tx: %v", err)
}
txns = append(txns, btcutil.NewTx(tx))
}
// Now generate a block with the default block version, and a zero'd
// out time.
block, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{})
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// Ensure that all created transactions were included, and that the
// block version was properly set to the default.
numBlocksTxns := len(block.Transactions())
if numBlocksTxns != numTxns+1 {
t.Fatalf("block did not include all transactions: "+
"expected %v, got %v", numTxns+1, numBlocksTxns)
}
blockVersion := block.MsgBlock().Header.Version
if blockVersion != wire.BlockVersion {
t.Fatalf("block version is not default: expected %v, got %v",
wire.BlockVersion, blockVersion)
}
// Next generate a block with a "non-standard" block version along with
// time stamp a minute after the previous block's timestamp.
timestamp := block.MsgBlock().Header.Timestamp.Add(time.Minute)
targetBlockVersion := int32(1337)
block, err = r.GenerateAndSubmitBlock(nil, targetBlockVersion, timestamp)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// Finally ensure that the desired block version and timestamp were set
// properly.
header := block.MsgBlock().Header
blockVersion = header.Version
if blockVersion != targetBlockVersion {
t.Fatalf("block version mismatch: expected %v, got %v",
targetBlockVersion, blockVersion)
}
if !timestamp.Equal(header.Timestamp) {
t.Fatalf("header time stamp mismatch: expected %v, got %v",
timestamp, header.Timestamp)
}
}
func testMemWalletReorg(r *Harness, t *testing.T) {
// Create a fresh harness, we'll be using the main harness to force a
// re-org on this local harness.
harness, err := New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := harness.SetUp(true, 5); err != nil {
t.Fatalf("unable to complete rpctest setup: %v", err)
}
defer harness.TearDown()
// The internal wallet of this harness should now have 250 BTC.
expectedBalance := btcutil.Amount(250 * btcutil.SatoshiPerBitcoin)
walletBalance := harness.ConfirmedBalance()
if expectedBalance != walletBalance {
t.Fatalf("wallet balance incorrect: expected %v, got %v",
expectedBalance, walletBalance)
}
// Now connect this local harness to the main harness then wait for
// their chains to synchronize.
if err := ConnectNode(r, harness); err != nil {
t.Fatalf("unable to connect harnesses: %v", err)
}
nodeSlice := []*Harness{r, harness}
if err := JoinNodes(nodeSlice, Blocks); err != nil {
t.Fatalf("unable to join node on block height: %v", err)
}
// The original wallet should now have a balance of 0 BTC as its entire
// chain should have been decimated in favor of the main harness'
// chain.
expectedBalance = btcutil.Amount(0)
walletBalance = harness.ConfirmedBalance()
if expectedBalance != walletBalance {
t.Fatalf("wallet balance incorrect: expected %v, got %v",
expectedBalance, walletBalance)
}
}
func testMemWalletLockedOutputs(r *Harness, t *testing.T) {
// Obtain the initial balance of the wallet at this point.
startingBalance := r.ConfirmedBalance()
// First, create a signed transaction spending some outputs.
addr, err := r.NewAddress()
if err != nil {
t.Fatalf("unable to generate new address: %v", err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("unable to create script: %v", err)
}
outputAmt := btcutil.Amount(50 * btcutil.SatoshiPerBitcoin)
output := wire.NewTxOut(int64(outputAmt), pkScript)
tx, err := r.CreateTransaction([]*wire.TxOut{output}, 10)
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// The current wallet balance should now be at least 50 BTC less
// (accounting for fees) than the period balance
currentBalance := r.ConfirmedBalance()
if !(currentBalance <= startingBalance-outputAmt) {
t.Fatalf("spent outputs not locked: previous balance %v, "+
"current balance %v", startingBalance, currentBalance)
}
// Now unlocked all the spent inputs within the unbroadcast signed
// transaction. The current balance should now be exactly that of the
// starting balance.
r.UnlockOutputs(tx.TxIn)
currentBalance = r.ConfirmedBalance()
if currentBalance != startingBalance {
t.Fatalf("current and starting balance should now match: "+
"expected %v, got %v", startingBalance, currentBalance)
}
}
var harnessTestCases = []HarnessTestCase{
testSendOutputs,
testConnectNode,
testActiveHarnesses,
testJoinMempools,
testJoinBlocks,
testGenerateAndSubmitBlock,
testMemWalletReorg,
testMemWalletLockedOutputs,
}
var mainHarness *Harness
const (
numMatureOutputs = 25
)
func TestMain(m *testing.M) {
var err error
mainHarness, err = New(&chaincfg.SimNetParams, nil, nil)
if err != nil {
fmt.Println("unable to create main harness: ", err)
os.Exit(1)
}
// Initialize the main mining node with a chain of length 125,
// providing 25 mature coinbases to allow spending from for testing
// purposes.
if err = mainHarness.SetUp(true, numMatureOutputs); err != nil {
fmt.Println("unable to setup test chain: ", err)
os.Exit(1)
}
exitCode := m.Run()
// Clean up any active harnesses that are still currently running.
if len(ActiveHarnesses()) > 0 {
if err := TearDownAll(); err != nil {
fmt.Println("unable to tear down chain: ", err)
os.Exit(1)
}
}
os.Exit(exitCode)
}
func TestHarness(t *testing.T) {
// We should have (numMatureOutputs * 50 BTC) of mature unspendable
// outputs.
expectedBalance := btcutil.Amount(numMatureOutputs * 50 * btcutil.SatoshiPerBitcoin)
harnessBalance := mainHarness.ConfirmedBalance()
if harnessBalance != expectedBalance {
t.Fatalf("expected wallet balance of %v instead have %v",
expectedBalance, harnessBalance)
}
// Current tip should be at a height of numMatureOutputs plus the
// required number of blocks for coinbase maturity.
nodeInfo, err := mainHarness.Node.GetInfo()
if err != nil {
t.Fatalf("unable to execute getinfo on node: %v", err)
}
expectedChainHeight := numMatureOutputs + uint32(mainHarness.ActiveNet.CoinbaseMaturity)
if uint32(nodeInfo.Blocks) != expectedChainHeight {
t.Errorf("Chain height is %v, should be %v",
nodeInfo.Blocks, expectedChainHeight)
}
for _, testCase := range harnessTestCases {
testCase(mainHarness, t)
}
testTearDownAll(t)
}

169
rpctest/utils.go Normal file
View file

@ -0,0 +1,169 @@
// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"net"
"reflect"
"strconv"
"time"
"github.com/btcsuite/btcrpcclient"
)
// JoinType is an enum representing a particular type of "node join". A node
// join is a synchronization tool used to wait until a subset of nodes have a
// consistent state with respect to an attribute.
type JoinType uint8
const (
// Blocks is a JoinType which waits until all nodes share the same
// block height.
Blocks JoinType = iota
// Mempools is a JoinType which blocks until all nodes have identical
// mempool.
Mempools
)
// JoinNodes is a synchronization tool used to block until all passed nodes are
// fully synced with respect to an attribute. This function will block for a
// period of time, finally returning once all nodes are synced according to the
// passed JoinType. This function be used to to ensure all active test
// harnesses are at a consistent state before proceeding to an assertion or
// check within rpc tests.
func JoinNodes(nodes []*Harness, joinType JoinType) error {
switch joinType {
case Blocks:
return syncBlocks(nodes)
case Mempools:
return syncMempools(nodes)
}
return nil
}
// syncMempools blocks until all nodes have identical mempools.
func syncMempools(nodes []*Harness) error {
poolsMatch := false
for !poolsMatch {
retry:
firstPool, err := nodes[0].Node.GetRawMempool()
if err != nil {
return err
}
// If all nodes have an identical mempool with respect to the
// first node, then we're done. Otherwise, drop back to the top
// of the loop and retry after a short wait period.
for _, node := range nodes[:1] {
nodePool, err := node.Node.GetRawMempool()
if err != nil {
return err
}
if !reflect.DeepEqual(firstPool, nodePool) {
time.Sleep(time.Millisecond * 100)
goto retry
}
}
poolsMatch = true
}
return nil
}
// syncBlocks blocks until all nodes report the same block height.
func syncBlocks(nodes []*Harness) error {
blocksMatch := false
for !blocksMatch {
retry:
blockHeights := make(map[int64]struct{})
for _, node := range nodes {
blockHeight, err := node.Node.GetBlockCount()
if err != nil {
return err
}
blockHeights[blockHeight] = struct{}{}
if len(blockHeights) > 1 {
time.Sleep(time.Millisecond * 100)
goto retry
}
}
blocksMatch = true
}
return nil
}
// ConnectNode establishes a new peer-to-peer connection between the "from"
// harness and the "to" harness. The connection made is flagged as persistent,
// therefore in the case of disconnects, "from" will attempt to reestablish a
// connection to the "to" harness.
func ConnectNode(from *Harness, to *Harness) error {
// Calculate the target p2p addr+port for the node to be connected to.
// p2p ports uses within the package are always even, so we multiply
// the node number by two before offsetting from the defaultP2pPort.
targetPort := defaultP2pPort + (2 * to.nodeNum)
targetAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(targetPort))
peerInfo, err := from.Node.GetPeerInfo()
if err != nil {
return err
}
numPeers := len(peerInfo)
if err := from.Node.AddNode(targetAddr, btcrpcclient.ANAdd); err != nil {
return err
}
// Block until a new connection has been established.
peerInfo, err = from.Node.GetPeerInfo()
if err != nil {
return err
}
for len(peerInfo) <= numPeers {
peerInfo, err = from.Node.GetPeerInfo()
if err != nil {
return err
}
}
return nil
}
// TearDownAll tears down all active test harnesses.
func TearDownAll() error {
harnessStateMtx.Lock()
defer harnessStateMtx.Unlock()
for _, harness := range testInstances {
if err := harness.TearDown(); err != nil {
return err
}
}
return nil
}
// ActiveHarnesses returns a slice of all currently active test harnesses. A
// test harness if considered "active" if it has been created, but not yet torn
// down.
func ActiveHarnesses() []*Harness {
harnessStateMtx.RLock()
defer harnessStateMtx.RUnlock()
activeNodes := make([]*Harness, 0, len(testInstances))
for _, harness := range testInstances {
activeNodes = append(activeNodes, harness)
}
return activeNodes
}