b86df0ba91
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.
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
// 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
|
|
}
|