forked from cerc-io/ipld-eth-server
* Add vendor dir so builds dont require dep * Pin specific version go-eth version
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
integration
|
||||
===========
|
||||
|
||||
[](https://travis-ci.org/btcsuite/btcd)
|
||||
[](http://copyfree.org)
|
||||
|
||||
This contains integration tests which make use of the
|
||||
[rpctest](https://github.com/btcsuite/btcd/tree/master/integration/rpctest)
|
||||
package to programmatically drive nodes via RPC.
|
||||
|
||||
## License
|
||||
|
||||
This code is licensed under the [copyfree](http://copyfree.org) ISC License.
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
// 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.
|
||||
|
||||
// This file is ignored during the regular tests due to the following build tag.
|
||||
// +build rpctest
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
)
|
||||
|
||||
const (
|
||||
// vbLegacyBlockVersion is the highest legacy block version before the
|
||||
// version bits scheme became active.
|
||||
vbLegacyBlockVersion = 4
|
||||
|
||||
// vbTopBits defines the bits to set in the version to signal that the
|
||||
// version bits scheme is being used.
|
||||
vbTopBits = 0x20000000
|
||||
)
|
||||
|
||||
// assertVersionBit gets the passed block hash from the given test harness and
|
||||
// ensures its version either has the provided bit set or unset per the set
|
||||
// flag.
|
||||
func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bit uint8, set bool) {
|
||||
block, err := r.Node.GetBlock(hash)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve block %v: %v", hash, err)
|
||||
}
|
||||
switch {
|
||||
case set && block.Header.Version&(1<<bit) == 0:
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: block %s, version 0x%x "+
|
||||
"does not have bit %d set", line, hash,
|
||||
block.Header.Version, bit)
|
||||
case !set && block.Header.Version&(1<<bit) != 0:
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: block %s, version 0x%x "+
|
||||
"has bit %d set", line, hash, block.Header.Version, bit)
|
||||
}
|
||||
}
|
||||
|
||||
// assertChainHeight retrieves the current chain height from the given test
|
||||
// harness and ensures it matches the provided expected height.
|
||||
func assertChainHeight(r *rpctest.Harness, t *testing.T, expectedHeight uint32) {
|
||||
height, err := r.Node.GetBlockCount()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve block height: %v", err)
|
||||
}
|
||||
if uint32(height) != expectedHeight {
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: block height of %d "+
|
||||
"is not the expected %d", line, height, expectedHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// thresholdStateToStatus converts the passed threshold state to the equivalent
|
||||
// status string returned in the getblockchaininfo RPC.
|
||||
func thresholdStateToStatus(state blockchain.ThresholdState) (string, error) {
|
||||
switch state {
|
||||
case blockchain.ThresholdDefined:
|
||||
return "defined", nil
|
||||
case blockchain.ThresholdStarted:
|
||||
return "started", nil
|
||||
case blockchain.ThresholdLockedIn:
|
||||
return "lockedin", nil
|
||||
case blockchain.ThresholdActive:
|
||||
return "active", nil
|
||||
case blockchain.ThresholdFailed:
|
||||
return "failed", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unrecognized threshold state: %v", state)
|
||||
}
|
||||
|
||||
// assertSoftForkStatus retrieves the current blockchain info from the given
|
||||
// test harness and ensures the provided soft fork key is both available and its
|
||||
// status is the equivalent of the passed state.
|
||||
func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, state blockchain.ThresholdState) {
|
||||
// Convert the expected threshold state into the equivalent
|
||||
// getblockchaininfo RPC status string.
|
||||
status, err := thresholdStateToStatus(state)
|
||||
if err != nil {
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: unable to convert "+
|
||||
"threshold state %v to string", line, state)
|
||||
}
|
||||
|
||||
info, err := r.Node.GetBlockChainInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve chain info: %v", err)
|
||||
}
|
||||
|
||||
// Ensure the key is available.
|
||||
desc, ok := info.Bip9SoftForks[forkKey]
|
||||
if !ok {
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: softfork status for %q "+
|
||||
"is not in getblockchaininfo results", line, forkKey)
|
||||
}
|
||||
|
||||
// Ensure the status it the expected value.
|
||||
if desc.Status != status {
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %d: softfork status for %q "+
|
||||
"is %v instead of expected %v", line, forkKey,
|
||||
desc.Status, status)
|
||||
}
|
||||
}
|
||||
|
||||
// testBIP0009 ensures the BIP0009 soft fork mechanism follows the state
|
||||
// transition rules set forth by the BIP for the provided soft fork key. It
|
||||
// uses the regression test network to signal support and advance through the
|
||||
// various threshold states including failure to achieve locked in status.
|
||||
//
|
||||
// See TestBIP0009 for an overview of what is tested.
|
||||
//
|
||||
// NOTE: This only differs from the exported version in that it accepts the
|
||||
// specific soft fork deployment to test.
|
||||
func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
|
||||
// Initialize the primary mining node with only the genesis block.
|
||||
r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create primary harness: %v", err)
|
||||
}
|
||||
if err := r.SetUp(false, 0); err != nil {
|
||||
t.Fatalf("unable to setup test chain: %v", err)
|
||||
}
|
||||
defer r.TearDown()
|
||||
|
||||
// *** ThresholdDefined ***
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status starts out as defined.
|
||||
assertChainHeight(r, t, 0)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdDefined)
|
||||
|
||||
// *** ThresholdDefined part 2 - 1 block prior to ThresholdStarted ***
|
||||
//
|
||||
// Generate enough blocks to reach the height just before the first
|
||||
// state transition without signalling support since the state should
|
||||
// move to started once the start time has been reached regardless of
|
||||
// support signalling.
|
||||
//
|
||||
// NOTE: This is two blocks before the confirmation window because the
|
||||
// getblockchaininfo RPC reports the status for the block AFTER the
|
||||
// current one. All of the heights below are thus offset by one to
|
||||
// compensate.
|
||||
//
|
||||
// Assert the chain height is the expected value and soft fork status is
|
||||
// still defined and did NOT move to started.
|
||||
confirmationWindow := r.ActiveNet.MinerConfirmationWindow
|
||||
for i := uint32(0); i < confirmationWindow-2; i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
assertChainHeight(r, t, confirmationWindow-2)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdDefined)
|
||||
|
||||
// *** ThresholdStarted ***
|
||||
//
|
||||
// Generate another block to reach the next window.
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status is started.
|
||||
_, err = r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block: %v", err)
|
||||
}
|
||||
assertChainHeight(r, t, confirmationWindow-1)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdStarted)
|
||||
|
||||
// *** ThresholdStarted part 2 - Fail to achieve ThresholdLockedIn ***
|
||||
//
|
||||
// Generate enough blocks to reach the next window in such a way that
|
||||
// the number blocks with the version bit set to signal support is 1
|
||||
// less than required to achieve locked in status.
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status is still started and did NOT move to locked in.
|
||||
if deploymentID > uint32(len(r.ActiveNet.Deployments)) {
|
||||
t.Fatalf("deployment ID %d does not exist", deploymentID)
|
||||
}
|
||||
deployment := &r.ActiveNet.Deployments[deploymentID]
|
||||
activationThreshold := r.ActiveNet.RuleChangeActivationThreshold
|
||||
signalForkVersion := int32(1<<deployment.BitNumber) | vbTopBits
|
||||
for i := uint32(0); i < activationThreshold-1; i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, signalForkVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
for i := uint32(0); i < confirmationWindow-(activationThreshold-1); i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
assertChainHeight(r, t, (confirmationWindow*2)-1)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdStarted)
|
||||
|
||||
// *** ThresholdLockedIn ***
|
||||
//
|
||||
// Generate enough blocks to reach the next window in such a way that
|
||||
// the number blocks with the version bit set to signal support is
|
||||
// exactly the number required to achieve locked in status.
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status moved to locked in.
|
||||
for i := uint32(0); i < activationThreshold; i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, signalForkVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
for i := uint32(0); i < confirmationWindow-activationThreshold; i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
assertChainHeight(r, t, (confirmationWindow*3)-1)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdLockedIn)
|
||||
|
||||
// *** ThresholdLockedIn part 2 -- 1 block prior to ThresholdActive ***
|
||||
//
|
||||
// Generate enough blocks to reach the height just before the next
|
||||
// window without continuing to signal support since it is already
|
||||
// locked in.
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status is still locked in and did NOT move to active.
|
||||
for i := uint32(0); i < confirmationWindow-1; i++ {
|
||||
_, err := r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion,
|
||||
time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
assertChainHeight(r, t, (confirmationWindow*4)-2)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdLockedIn)
|
||||
|
||||
// *** ThresholdActive ***
|
||||
//
|
||||
// Generate another block to reach the next window without continuing to
|
||||
// signal support since it is already locked in.
|
||||
//
|
||||
// Assert the chain height is the expected value and the soft fork
|
||||
// status moved to active.
|
||||
_, err = r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated block: %v", err)
|
||||
}
|
||||
assertChainHeight(r, t, (confirmationWindow*4)-1)
|
||||
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
|
||||
}
|
||||
|
||||
// TestBIP0009 ensures the BIP0009 soft fork mechanism follows the state
|
||||
// transition rules set forth by the BIP for all soft forks. It uses the
|
||||
// regression test network to signal support and advance through the various
|
||||
// threshold states including failure to achieve locked in status.
|
||||
//
|
||||
// Overview:
|
||||
// - Assert the chain height is 0 and the state is ThresholdDefined
|
||||
// - Generate 1 fewer blocks than needed to reach the first state transition
|
||||
// - Assert chain height is expected and state is still ThresholdDefined
|
||||
// - Generate 1 more block to reach the first state transition
|
||||
// - Assert chain height is expected and state moved to ThresholdStarted
|
||||
// - Generate enough blocks to reach the next state transition window, but only
|
||||
// signal support in 1 fewer than the required number to achieve
|
||||
// ThresholdLockedIn
|
||||
// - Assert chain height is expected and state is still ThresholdStarted
|
||||
// - Generate enough blocks to reach the next state transition window with only
|
||||
// the exact number of blocks required to achieve locked in status signalling
|
||||
// support.
|
||||
// - Assert chain height is expected and state moved to ThresholdLockedIn
|
||||
// - Generate 1 fewer blocks than needed to reach the next state transition
|
||||
// - Assert chain height is expected and state is still ThresholdLockedIn
|
||||
// - Generate 1 more block to reach the next state transition
|
||||
// - Assert chain height is expected and state moved to ThresholdActive
|
||||
func TestBIP0009(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testBIP0009(t, "dummy", chaincfg.DeploymentTestDummy)
|
||||
testBIP0009(t, "segwit", chaincfg.DeploymentSegwit)
|
||||
}
|
||||
|
||||
// TestBIP0009Mining ensures blocks built via btcd's CPU miner follow the rules
|
||||
// set forth by BIP0009 by using the test dummy deployment.
|
||||
//
|
||||
// Overview:
|
||||
// - Generate block 1
|
||||
// - Assert bit is NOT set (ThresholdDefined)
|
||||
// - Generate enough blocks to reach first state transition
|
||||
// - Assert bit is NOT set for block prior to state transition
|
||||
// - Assert bit is set for block at state transition (ThresholdStarted)
|
||||
// - Generate enough blocks to reach second state transition
|
||||
// - Assert bit is set for block at state transition (ThresholdLockedIn)
|
||||
// - Generate enough blocks to reach third state transition
|
||||
// - Assert bit is set for block prior to state transition (ThresholdLockedIn)
|
||||
// - Assert bit is NOT set for block at state transition (ThresholdActive)
|
||||
func TestBIP0009Mining(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Initialize the primary mining node with only the genesis block.
|
||||
r, err := rpctest.New(&chaincfg.SimNetParams, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create primary harness: %v", err)
|
||||
}
|
||||
if err := r.SetUp(true, 0); err != nil {
|
||||
t.Fatalf("unable to setup test chain: %v", err)
|
||||
}
|
||||
defer r.TearDown()
|
||||
|
||||
// Assert the chain only consists of the gensis block.
|
||||
assertChainHeight(r, t, 0)
|
||||
|
||||
// *** ThresholdDefined ***
|
||||
//
|
||||
// Generate a block that extends the genesis block. It should not have
|
||||
// the test dummy bit set in the version since the first window is
|
||||
// in the defined threshold state.
|
||||
deployment := &r.ActiveNet.Deployments[chaincfg.DeploymentTestDummy]
|
||||
testDummyBitNum := deployment.BitNumber
|
||||
hashes, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate blocks: %v", err)
|
||||
}
|
||||
assertChainHeight(r, t, 1)
|
||||
assertVersionBit(r, t, hashes[0], testDummyBitNum, false)
|
||||
|
||||
// *** ThresholdStarted ***
|
||||
//
|
||||
// Generate enough blocks to reach the first state transition.
|
||||
//
|
||||
// The second to last generated block should not have the test bit set
|
||||
// in the version.
|
||||
//
|
||||
// The last generated block should now have the test bit set in the
|
||||
// version since the btcd mining code will have recognized the test
|
||||
// dummy deployment as started.
|
||||
confirmationWindow := r.ActiveNet.MinerConfirmationWindow
|
||||
numNeeded := confirmationWindow - 1
|
||||
hashes, err = r.Node.Generate(numNeeded)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated %d blocks: %v", numNeeded, err)
|
||||
}
|
||||
assertChainHeight(r, t, confirmationWindow)
|
||||
assertVersionBit(r, t, hashes[len(hashes)-2], testDummyBitNum, false)
|
||||
assertVersionBit(r, t, hashes[len(hashes)-1], testDummyBitNum, true)
|
||||
|
||||
// *** ThresholdLockedIn ***
|
||||
//
|
||||
// Generate enough blocks to reach the next state transition.
|
||||
//
|
||||
// The last generated block should still have the test bit set in the
|
||||
// version since the btcd mining code will have recognized the test
|
||||
// dummy deployment as locked in.
|
||||
hashes, err = r.Node.Generate(confirmationWindow)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated %d blocks: %v", confirmationWindow,
|
||||
err)
|
||||
}
|
||||
assertChainHeight(r, t, confirmationWindow*2)
|
||||
assertVersionBit(r, t, hashes[len(hashes)-1], testDummyBitNum, true)
|
||||
|
||||
// *** ThresholdActivated ***
|
||||
//
|
||||
// Generate enough blocks to reach the next state transition.
|
||||
//
|
||||
// The second to last generated block should still have the test bit set
|
||||
// in the version since it is still locked in.
|
||||
//
|
||||
// The last generated block should NOT have the test bit set in the
|
||||
// version since the btcd mining code will have recognized the test
|
||||
// dummy deployment as activated and thus there is no longer any need
|
||||
// to set the bit.
|
||||
hashes, err = r.Node.Generate(confirmationWindow)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generated %d blocks: %v", confirmationWindow,
|
||||
err)
|
||||
}
|
||||
assertChainHeight(r, t, confirmationWindow*3)
|
||||
assertVersionBit(r, t, hashes[len(hashes)-2], testDummyBitNum, true)
|
||||
assertVersionBit(r, t, hashes[len(hashes)-1], testDummyBitNum, false)
|
||||
}
|
||||
+695
@@ -0,0 +1,695 @@
|
||||
// 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.
|
||||
|
||||
// This file is ignored during the regular tests due to the following build tag.
|
||||
// +build rpctest
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"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/integration/rpctest"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
const (
|
||||
csvKey = "csv"
|
||||
)
|
||||
|
||||
// makeTestOutput creates an on-chain output paying to a freshly generated
|
||||
// p2pkh output with the specified amount.
|
||||
func makeTestOutput(r *rpctest.Harness, t *testing.T,
|
||||
amt btcutil.Amount) (*btcec.PrivateKey, *wire.OutPoint, []byte, error) {
|
||||
|
||||
// Create a fresh key, then send some coins to an address spendable by
|
||||
// that key.
|
||||
key, err := btcec.NewPrivateKey(btcec.S256())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Using the key created above, generate a pkScript which it's able to
|
||||
// spend.
|
||||
a, err := btcutil.NewAddressPubKey(key.PubKey().SerializeCompressed(), r.ActiveNet)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
selfAddrScript, err := txscript.PayToAddrScript(a.AddressPubKeyHash())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
output := &wire.TxOut{PkScript: selfAddrScript, Value: 1e8}
|
||||
|
||||
// Next, create and broadcast a transaction paying to the output.
|
||||
fundTx, err := r.CreateTransaction([]*wire.TxOut{output}, 10)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
txHash, err := r.Node.SendRawTransaction(fundTx, true)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// The transaction created above should be included within the next
|
||||
// generated block.
|
||||
blockHash, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
assertTxInBlock(r, t, blockHash[0], txHash)
|
||||
|
||||
// Locate the output index of the coins spendable by the key we
|
||||
// generated above, this is needed in order to create a proper utxo for
|
||||
// this output.
|
||||
var outputIndex uint32
|
||||
if bytes.Equal(fundTx.TxOut[0].PkScript, selfAddrScript) {
|
||||
outputIndex = 0
|
||||
} else {
|
||||
outputIndex = 1
|
||||
}
|
||||
|
||||
utxo := &wire.OutPoint{
|
||||
Hash: fundTx.TxHash(),
|
||||
Index: outputIndex,
|
||||
}
|
||||
|
||||
return key, utxo, selfAddrScript, nil
|
||||
}
|
||||
|
||||
// TestBIP0113Activation tests for proper adherance of the BIP 113 rule
|
||||
// constraint which requires all transaction finality tests to use the MTP of
|
||||
// the last 11 blocks, rather than the timestamp of the block which includes
|
||||
// them.
|
||||
//
|
||||
// Overview:
|
||||
// - Pre soft-fork:
|
||||
// - Transactions with non-final lock-times from the PoV of MTP should be
|
||||
// rejected from the mempool.
|
||||
// - Transactions within non-final MTP based lock-times should be accepted
|
||||
// in valid blocks.
|
||||
//
|
||||
// - Post soft-fork:
|
||||
// - Transactions with non-final lock-times from the PoV of MTP should be
|
||||
// rejected from the mempool and when found within otherwise valid blocks.
|
||||
// - Transactions with final lock-times from the PoV of MTP should be
|
||||
// accepted to the mempool and mined in future block.
|
||||
func TestBIP0113Activation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
btcdCfg := []string{"--rejectnonstd"}
|
||||
r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg)
|
||||
if err != nil {
|
||||
t.Fatal("unable to create primary harness: ", err)
|
||||
}
|
||||
if err := r.SetUp(true, 1); err != nil {
|
||||
t.Fatalf("unable to setup test chain: %v", err)
|
||||
}
|
||||
defer r.TearDown()
|
||||
|
||||
// Create a fresh output for usage within the test below.
|
||||
const outputValue = btcutil.SatoshiPerBitcoin
|
||||
outputKey, testOutput, testPkScript, err := makeTestOutput(r, t,
|
||||
outputValue)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create test output: %v", err)
|
||||
}
|
||||
|
||||
// Fetch a fresh address from the harness, we'll use this address to
|
||||
// send funds back into the Harness.
|
||||
addr, err := r.NewAddress()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate address: %v", err)
|
||||
}
|
||||
addrScript, err := txscript.PayToAddrScript(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate addr script: %v", err)
|
||||
}
|
||||
|
||||
// Now create a transaction with a lock time which is "final" according
|
||||
// to the latest block, but not according to the current median time
|
||||
// past.
|
||||
tx := wire.NewMsgTx(1)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *testOutput,
|
||||
})
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
PkScript: addrScript,
|
||||
Value: outputValue - 1000,
|
||||
})
|
||||
|
||||
// We set the lock-time of the transaction to just one minute after the
|
||||
// current MTP of the chain.
|
||||
chainInfo, err := r.Node.GetBlockChainInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for chain info: %v", err)
|
||||
}
|
||||
tx.LockTime = uint32(chainInfo.MedianTime) + 1
|
||||
|
||||
sigScript, err := txscript.SignatureScript(tx, 0, testPkScript,
|
||||
txscript.SigHashAll, outputKey, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate sig: %v", err)
|
||||
}
|
||||
tx.TxIn[0].SignatureScript = sigScript
|
||||
|
||||
// This transaction should be rejected from the mempool as using MTP
|
||||
// for transactions finality is now a policy rule. Additionally, the
|
||||
// exact error should be the rejection of a non-final transaction.
|
||||
_, err = r.Node.SendRawTransaction(tx, true)
|
||||
if err == nil {
|
||||
t.Fatalf("transaction accepted, but should be non-final")
|
||||
} else if !strings.Contains(err.Error(), "not finalized") {
|
||||
t.Fatalf("transaction should be rejected due to being "+
|
||||
"non-final, instead: %v", err)
|
||||
}
|
||||
|
||||
// However, since the block validation consensus rules haven't yet
|
||||
// activated, a block including the transaction should be accepted.
|
||||
txns := []*btcutil.Tx{btcutil.NewTx(tx)}
|
||||
block, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to submit block: %v", err)
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
assertTxInBlock(r, t, block.Hash(), &txid)
|
||||
|
||||
// At this point, the block height should be 103: we mined 101 blocks
|
||||
// to create a single mature output, then an additional block to create
|
||||
// a new output, and then mined a single block above to include our
|
||||
// transation.
|
||||
assertChainHeight(r, t, 103)
|
||||
|
||||
// Next, mine enough blocks to ensure that the soft-fork becomes
|
||||
// activated. Assert that the block version of the second-to-last block
|
||||
// in the final range is active.
|
||||
|
||||
// Next, mine ensure blocks to ensure that the soft-fork becomes
|
||||
// active. We're at height 103 and we need 200 blocks to be mined after
|
||||
// the genesis target period, so we mine 196 blocks. This'll put us at
|
||||
// height 299. The getblockchaininfo call checks the state for the
|
||||
// block AFTER the current height.
|
||||
numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 4
|
||||
if _, err := r.Node.Generate(numBlocks); err != nil {
|
||||
t.Fatalf("unable to generate blocks: %v", err)
|
||||
}
|
||||
|
||||
assertChainHeight(r, t, 299)
|
||||
assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdActive)
|
||||
|
||||
// The timeLockDeltas slice represents a series of deviations from the
|
||||
// current MTP which will be used to test border conditions w.r.t
|
||||
// transaction finality. -1 indicates 1 second prior to the MTP, 0
|
||||
// indicates the current MTP, and 1 indicates 1 second after the
|
||||
// current MTP.
|
||||
//
|
||||
// This time, all transactions which are final according to the MTP
|
||||
// *should* be accepted to both the mempool and within a valid block.
|
||||
// While transactions with lock-times *after* the current MTP should be
|
||||
// rejected.
|
||||
timeLockDeltas := []int64{-1, 0, 1}
|
||||
for _, timeLockDelta := range timeLockDeltas {
|
||||
chainInfo, err = r.Node.GetBlockChainInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query for chain info: %v", err)
|
||||
}
|
||||
medianTimePast := chainInfo.MedianTime
|
||||
|
||||
// Create another test output to be spent shortly below.
|
||||
outputKey, testOutput, testPkScript, err = makeTestOutput(r, t,
|
||||
outputValue)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create test output: %v", err)
|
||||
}
|
||||
|
||||
// Create a new transaction with a lock-time past the current known
|
||||
// MTP.
|
||||
tx = wire.NewMsgTx(1)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *testOutput,
|
||||
})
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
PkScript: addrScript,
|
||||
Value: outputValue - 1000,
|
||||
})
|
||||
tx.LockTime = uint32(medianTimePast + timeLockDelta)
|
||||
sigScript, err = txscript.SignatureScript(tx, 0, testPkScript,
|
||||
txscript.SigHashAll, outputKey, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate sig: %v", err)
|
||||
}
|
||||
tx.TxIn[0].SignatureScript = sigScript
|
||||
|
||||
// If the time-lock delta is greater than -1, then the
|
||||
// transaction should be rejected from the mempool and when
|
||||
// included within a block. A time-lock delta of -1 should be
|
||||
// accepted as it has a lock-time of one
|
||||
// second _before_ the current MTP.
|
||||
|
||||
_, err = r.Node.SendRawTransaction(tx, true)
|
||||
if err == nil && timeLockDelta >= 0 {
|
||||
t.Fatal("transaction was accepted into the mempool " +
|
||||
"but should be rejected!")
|
||||
} else if err != nil && !strings.Contains(err.Error(), "not finalized") {
|
||||
t.Fatalf("transaction should be rejected from mempool "+
|
||||
"due to being non-final, instead: %v", err)
|
||||
}
|
||||
|
||||
txns = []*btcutil.Tx{btcutil.NewTx(tx)}
|
||||
_, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{})
|
||||
if err == nil && timeLockDelta >= 0 {
|
||||
t.Fatal("block should be rejected due to non-final " +
|
||||
"txn, but was accepted")
|
||||
} else if err != nil && !strings.Contains(err.Error(), "unfinalized") {
|
||||
t.Fatalf("block should be rejected due to non-final "+
|
||||
"tx, instead: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createCSVOutput creates an output paying to a trivially redeemable CSV
|
||||
// pkScript with the specified time-lock.
|
||||
func createCSVOutput(r *rpctest.Harness, t *testing.T,
|
||||
numSatoshis btcutil.Amount, timeLock int32,
|
||||
isSeconds bool) ([]byte, *wire.OutPoint, *wire.MsgTx, error) {
|
||||
|
||||
// Convert the time-lock to the proper sequence lock based according to
|
||||
// if the lock is seconds or time based.
|
||||
sequenceLock := blockchain.LockTimeToSequence(isSeconds,
|
||||
uint32(timeLock))
|
||||
|
||||
// Our CSV script is simply: <sequenceLock> OP_CSV OP_DROP
|
||||
b := txscript.NewScriptBuilder().
|
||||
AddInt64(int64(sequenceLock)).
|
||||
AddOp(txscript.OP_CHECKSEQUENCEVERIFY).
|
||||
AddOp(txscript.OP_DROP)
|
||||
csvScript, err := b.Script()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Using the script generated above, create a P2SH output which will be
|
||||
// accepted into the mempool.
|
||||
p2shAddr, err := btcutil.NewAddressScriptHash(csvScript, r.ActiveNet)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
p2shScript, err := txscript.PayToAddrScript(p2shAddr)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
output := &wire.TxOut{
|
||||
PkScript: p2shScript,
|
||||
Value: int64(numSatoshis),
|
||||
}
|
||||
|
||||
// Finally create a valid transaction which creates the output crafted
|
||||
// above.
|
||||
tx, err := r.CreateTransaction([]*wire.TxOut{output}, 10)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var outputIndex uint32
|
||||
if !bytes.Equal(tx.TxOut[0].PkScript, p2shScript) {
|
||||
outputIndex = 1
|
||||
}
|
||||
|
||||
utxo := &wire.OutPoint{
|
||||
Hash: tx.TxHash(),
|
||||
Index: outputIndex,
|
||||
}
|
||||
|
||||
return csvScript, utxo, tx, nil
|
||||
}
|
||||
|
||||
// spendCSVOutput spends an output previously created by the createCSVOutput
|
||||
// function. The sigScript is a trivial push of OP_TRUE followed by the
|
||||
// redeemScript to pass P2SH evaluation.
|
||||
func spendCSVOutput(redeemScript []byte, csvUTXO *wire.OutPoint,
|
||||
sequence uint32, targetOutput *wire.TxOut,
|
||||
txVersion int32) (*wire.MsgTx, error) {
|
||||
|
||||
tx := wire.NewMsgTx(txVersion)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *csvUTXO,
|
||||
Sequence: sequence,
|
||||
})
|
||||
tx.AddTxOut(targetOutput)
|
||||
|
||||
b := txscript.NewScriptBuilder().
|
||||
AddOp(txscript.OP_TRUE).
|
||||
AddData(redeemScript)
|
||||
|
||||
sigScript, err := b.Script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx.TxIn[0].SignatureScript = sigScript
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// assertTxInBlock asserts a transaction with the specified txid is found
|
||||
// within the block with the passed block hash.
|
||||
func assertTxInBlock(r *rpctest.Harness, t *testing.T, blockHash *chainhash.Hash,
|
||||
txid *chainhash.Hash) {
|
||||
|
||||
block, err := r.Node.GetBlock(blockHash)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get block: %v", err)
|
||||
}
|
||||
if len(block.Transactions) < 2 {
|
||||
t.Fatal("target transaction was not mined")
|
||||
}
|
||||
|
||||
for _, txn := range block.Transactions {
|
||||
txHash := txn.TxHash()
|
||||
if txn.TxHash() == txHash {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
t.Fatalf("assertion failed at line %v: txid %v was not found in "+
|
||||
"block %v", line, txid, blockHash)
|
||||
}
|
||||
|
||||
// TestBIP0068AndBIP0112Activation tests for the proper adherence to the BIP
|
||||
// 112 and BIP 68 rule-set after the activation of the CSV-package soft-fork.
|
||||
//
|
||||
// Overview:
|
||||
// - Pre soft-fork:
|
||||
// - A transaction spending a CSV output validly should be rejected from the
|
||||
// mempool, but accepted in a valid generated block including the
|
||||
// transaction.
|
||||
// - Post soft-fork:
|
||||
// - See the cases exercised within the table driven tests towards the end
|
||||
// of this test.
|
||||
func TestBIP0068AndBIP0112Activation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// We'd like the test proper evaluation and validation of the BIP 68
|
||||
// (sequence locks) and BIP 112 rule-sets which add input-age based
|
||||
// relative lock times.
|
||||
|
||||
btcdCfg := []string{"--rejectnonstd"}
|
||||
r, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg)
|
||||
if err != nil {
|
||||
t.Fatal("unable to create primary harness: ", err)
|
||||
}
|
||||
if err := r.SetUp(true, 1); err != nil {
|
||||
t.Fatalf("unable to setup test chain: %v", err)
|
||||
}
|
||||
defer r.TearDown()
|
||||
|
||||
assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdStarted)
|
||||
|
||||
harnessAddr, err := r.NewAddress()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to obtain harness address: %v", err)
|
||||
}
|
||||
harnessScript, err := txscript.PayToAddrScript(harnessAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate pkScript: %v", err)
|
||||
}
|
||||
|
||||
const (
|
||||
outputAmt = btcutil.SatoshiPerBitcoin
|
||||
relativeBlockLock = 10
|
||||
)
|
||||
|
||||
sweepOutput := &wire.TxOut{
|
||||
Value: outputAmt - 5000,
|
||||
PkScript: harnessScript,
|
||||
}
|
||||
|
||||
// As the soft-fork hasn't yet activated _any_ transaction version
|
||||
// which uses the CSV opcode should be accepted. Since at this point,
|
||||
// CSV doesn't actually exist, it's just a NOP.
|
||||
for txVersion := int32(0); txVersion < 3; txVersion++ {
|
||||
// Create a trivially spendable output with a CSV lock-time of
|
||||
// 10 relative blocks.
|
||||
redeemScript, testUTXO, tx, err := createCSVOutput(r, t, outputAmt,
|
||||
relativeBlockLock, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create CSV encumbered output: %v", err)
|
||||
}
|
||||
|
||||
// As the transaction is p2sh it should be accepted into the
|
||||
// mempool and found within the next generated block.
|
||||
if _, err := r.Node.SendRawTransaction(tx, true); err != nil {
|
||||
t.Fatalf("unable to broadcast tx: %v", err)
|
||||
}
|
||||
blocks, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate blocks: %v", err)
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
assertTxInBlock(r, t, blocks[0], &txid)
|
||||
|
||||
// Generate a custom transaction which spends the CSV output.
|
||||
sequenceNum := blockchain.LockTimeToSequence(false, 10)
|
||||
spendingTx, err := spendCSVOutput(redeemScript, testUTXO,
|
||||
sequenceNum, sweepOutput, txVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to spend csv output: %v", err)
|
||||
}
|
||||
|
||||
// This transaction should be rejected from the mempool since
|
||||
// CSV validation is already mempool policy pre-fork.
|
||||
_, err = r.Node.SendRawTransaction(spendingTx, true)
|
||||
if err == nil {
|
||||
t.Fatalf("transaction should have been rejected, but was " +
|
||||
"instead accepted")
|
||||
}
|
||||
|
||||
// However, this transaction should be accepted in a custom
|
||||
// generated block as CSV validation for scripts within blocks
|
||||
// shouldn't yet be active.
|
||||
txns := []*btcutil.Tx{btcutil.NewTx(spendingTx)}
|
||||
block, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to submit block: %v", err)
|
||||
}
|
||||
txid = spendingTx.TxHash()
|
||||
assertTxInBlock(r, t, block.Hash(), &txid)
|
||||
}
|
||||
|
||||
// At this point, the block height should be 107: we started at height
|
||||
// 101, then generated 2 blocks in each loop iteration above.
|
||||
assertChainHeight(r, t, 107)
|
||||
|
||||
// With the height at 107 we need 200 blocks to be mined after the
|
||||
// genesis target period, so we mine 192 blocks. This'll put us at
|
||||
// height 299. The getblockchaininfo call checks the state for the
|
||||
// block AFTER the current height.
|
||||
numBlocks := (r.ActiveNet.MinerConfirmationWindow * 2) - 8
|
||||
if _, err := r.Node.Generate(numBlocks); err != nil {
|
||||
t.Fatalf("unable to generate blocks: %v", err)
|
||||
}
|
||||
|
||||
assertChainHeight(r, t, 299)
|
||||
assertSoftForkStatus(r, t, csvKey, blockchain.ThresholdActive)
|
||||
|
||||
// Knowing the number of outputs needed for the tests below, create a
|
||||
// fresh output for use within each of the test-cases below.
|
||||
const relativeTimeLock = 512
|
||||
const numTests = 8
|
||||
type csvOutput struct {
|
||||
RedeemScript []byte
|
||||
Utxo *wire.OutPoint
|
||||
Timelock int32
|
||||
}
|
||||
var spendableInputs [numTests]csvOutput
|
||||
|
||||
// Create three outputs which have a block-based sequence locks, and
|
||||
// three outputs which use the above time based sequence lock.
|
||||
for i := 0; i < numTests; i++ {
|
||||
timeLock := relativeTimeLock
|
||||
isSeconds := true
|
||||
if i < 7 {
|
||||
timeLock = relativeBlockLock
|
||||
isSeconds = false
|
||||
}
|
||||
|
||||
redeemScript, utxo, tx, err := createCSVOutput(r, t, outputAmt,
|
||||
int32(timeLock), isSeconds)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create CSV output: %v", err)
|
||||
}
|
||||
|
||||
if _, err := r.Node.SendRawTransaction(tx, true); err != nil {
|
||||
t.Fatalf("unable to broadcast transaction: %v", err)
|
||||
}
|
||||
|
||||
spendableInputs[i] = csvOutput{
|
||||
RedeemScript: redeemScript,
|
||||
Utxo: utxo,
|
||||
Timelock: int32(timeLock),
|
||||
}
|
||||
}
|
||||
|
||||
// Mine a single block including all the transactions generated above.
|
||||
if _, err := r.Node.Generate(1); err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
// Now mine 10 additional blocks giving the inputs generated above a
|
||||
// age of 11. Space out each block 10 minutes after the previous block.
|
||||
prevBlockHash, err := r.Node.GetBestBlockHash()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get prior block hash: %v", err)
|
||||
}
|
||||
prevBlock, err := r.Node.GetBlock(prevBlockHash)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get block: %v", err)
|
||||
}
|
||||
for i := 0; i < relativeBlockLock; i++ {
|
||||
timeStamp := prevBlock.Header.Timestamp.Add(time.Minute * 10)
|
||||
b, err := r.GenerateAndSubmitBlock(nil, -1, timeStamp)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
prevBlock = b.MsgBlock()
|
||||
}
|
||||
|
||||
// A helper function to create fully signed transactions in-line during
|
||||
// the array initialization below.
|
||||
var inputIndex uint32
|
||||
makeTxCase := func(sequenceNum uint32, txVersion int32) *wire.MsgTx {
|
||||
csvInput := spendableInputs[inputIndex]
|
||||
|
||||
tx, err := spendCSVOutput(csvInput.RedeemScript, csvInput.Utxo,
|
||||
sequenceNum, sweepOutput, txVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to spend CSV output: %v", err)
|
||||
}
|
||||
|
||||
inputIndex++
|
||||
return tx
|
||||
}
|
||||
|
||||
tests := [numTests]struct {
|
||||
tx *wire.MsgTx
|
||||
accept bool
|
||||
}{
|
||||
// A valid transaction with a single input a sequence number
|
||||
// creating a 100 block relative time-lock. This transaction
|
||||
// should be rejected as its version number is 1, and only tx
|
||||
// of version > 2 will trigger the CSV behavior.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(false, 100), 1),
|
||||
accept: false,
|
||||
},
|
||||
// A transaction of version 2 spending a single input. The
|
||||
// input has a relative time-lock of 1 block, but the disable
|
||||
// bit it set. The transaction should be rejected as a result.
|
||||
{
|
||||
tx: makeTxCase(
|
||||
blockchain.LockTimeToSequence(false, 1)|wire.SequenceLockTimeDisabled,
|
||||
2,
|
||||
),
|
||||
accept: false,
|
||||
},
|
||||
// A v2 transaction with a single input having a 9 block
|
||||
// relative time lock. The referenced input is 11 blocks old,
|
||||
// but the CSV output requires a 10 block relative lock-time.
|
||||
// Therefore, the transaction should be rejected.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(false, 9), 2),
|
||||
accept: false,
|
||||
},
|
||||
// A v2 transaction with a single input having a 10 block
|
||||
// relative time lock. The referenced input is 11 blocks old so
|
||||
// the transaction should be accepted.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(false, 10), 2),
|
||||
accept: true,
|
||||
},
|
||||
// A v2 transaction with a single input having a 11 block
|
||||
// relative time lock. The input referenced has an input age of
|
||||
// 11 and the CSV op-code requires 10 blocks to have passed, so
|
||||
// this transaction should be accepted.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(false, 11), 2),
|
||||
accept: true,
|
||||
},
|
||||
// A v2 transaction whose input has a 1000 blck relative time
|
||||
// lock. This should be rejected as the input's age is only 11
|
||||
// blocks.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(false, 1000), 2),
|
||||
accept: false,
|
||||
},
|
||||
// A v2 transaction with a single input having a 512,000 second
|
||||
// relative time-lock. This transaction should be rejected as 6
|
||||
// days worth of blocks haven't yet been mined. The referenced
|
||||
// input doesn't have sufficient age.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(true, 512000), 2),
|
||||
accept: false,
|
||||
},
|
||||
// A v2 transaction whose single input has a 512 second
|
||||
// relative time-lock. This transaction should be accepted as
|
||||
// finalized.
|
||||
{
|
||||
tx: makeTxCase(blockchain.LockTimeToSequence(true, 512), 2),
|
||||
accept: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
txid, err := r.Node.SendRawTransaction(test.tx, true)
|
||||
switch {
|
||||
// Test case passes, nothing further to report.
|
||||
case test.accept && err == nil:
|
||||
|
||||
// Transaction should have been accepted but we have a non-nil
|
||||
// error.
|
||||
case test.accept && err != nil:
|
||||
t.Fatalf("test #%d, transaction should be accepted, "+
|
||||
"but was rejected: %v", i, err)
|
||||
|
||||
// Transaction should have been rejected, but it was accepted.
|
||||
case !test.accept && err == nil:
|
||||
t.Fatalf("test #%d, transaction should be rejected, "+
|
||||
"but was accepted", i)
|
||||
|
||||
// Transaction was rejected as wanted, nothing more to do.
|
||||
case !test.accept && err != nil:
|
||||
}
|
||||
|
||||
// If the transaction should be rejected, manually mine a block
|
||||
// with the non-final transaction. It should be rejected.
|
||||
if !test.accept {
|
||||
txns := []*btcutil.Tx{btcutil.NewTx(test.tx)}
|
||||
_, err := r.GenerateAndSubmitBlock(txns, -1, time.Time{})
|
||||
if err == nil {
|
||||
t.Fatalf("test #%d, invalid block accepted", i)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate a block, the transaction should be included within
|
||||
// the newly mined block.
|
||||
blockHashes, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to mine block: %v", err)
|
||||
}
|
||||
assertTxInBlock(r, t, blockHashes[0], txid)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// 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 integration
|
||||
|
||||
// This file only exists to prevent warnings due to no buildable source files
|
||||
// when the build tag for enabling the tests is not specified.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// 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.
|
||||
|
||||
// This file is ignored during the regular tests due to the following build tag.
|
||||
// +build rpctest
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
)
|
||||
|
||||
func testGetBestBlock(r *rpctest.Harness, t *testing.T) {
|
||||
_, prevbestHeight, err := r.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("Call to `getbestblock` failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a new block connecting to the current tip.
|
||||
generatedBlockHashes, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
bestHash, bestHeight, err := r.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("Call to `getbestblock` failed: %v", err)
|
||||
}
|
||||
|
||||
// Hash should be the same as the newly submitted block.
|
||||
if !bytes.Equal(bestHash[:], generatedBlockHashes[0][:]) {
|
||||
t.Fatalf("Block hashes do not match. Returned hash %v, wanted "+
|
||||
"hash %v", bestHash, generatedBlockHashes[0][:])
|
||||
}
|
||||
|
||||
// Block height should now reflect newest height.
|
||||
if bestHeight != prevbestHeight+1 {
|
||||
t.Fatalf("Block heights do not match. Got %v, wanted %v",
|
||||
bestHeight, prevbestHeight+1)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetBlockCount(r *rpctest.Harness, t *testing.T) {
|
||||
// Save the current count.
|
||||
currentCount, err := r.Node.GetBlockCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to get block count: %v", err)
|
||||
}
|
||||
|
||||
if _, err := r.Node.Generate(1); err != nil {
|
||||
t.Fatalf("Unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
// Count should have increased by one.
|
||||
newCount, err := r.Node.GetBlockCount()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to get block count: %v", err)
|
||||
}
|
||||
if newCount != currentCount+1 {
|
||||
t.Fatalf("Block count incorrect. Got %v should be %v",
|
||||
newCount, currentCount+1)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetBlockHash(r *rpctest.Harness, t *testing.T) {
|
||||
// Create a new block connecting to the current tip.
|
||||
generatedBlockHashes, err := r.Node.Generate(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
info, err := r.Node.GetInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("call to getinfo cailed: %v", err)
|
||||
}
|
||||
|
||||
blockHash, err := r.Node.GetBlockHash(int64(info.Blocks))
|
||||
if err != nil {
|
||||
t.Fatalf("Call to `getblockhash` failed: %v", err)
|
||||
}
|
||||
|
||||
// Block hashes should match newly created block.
|
||||
if !bytes.Equal(generatedBlockHashes[0][:], blockHash[:]) {
|
||||
t.Fatalf("Block hashes do not match. Returned hash %v, wanted "+
|
||||
"hash %v", blockHash, generatedBlockHashes[0][:])
|
||||
}
|
||||
}
|
||||
|
||||
var rpcTestCases = []rpctest.HarnessTestCase{
|
||||
testGetBestBlock,
|
||||
testGetBlockCount,
|
||||
testGetBlockHash,
|
||||
}
|
||||
|
||||
var primaryHarness *rpctest.Harness
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
|
||||
// In order to properly test scenarios on as if we were on mainnet,
|
||||
// ensure that non-standard transactions aren't accepted into the
|
||||
// mempool or relayed.
|
||||
btcdCfg := []string{"--rejectnonstd"}
|
||||
primaryHarness, err = rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg)
|
||||
if err != nil {
|
||||
fmt.Println("unable to create primary harness: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize the primary mining node with a chain of length 125,
|
||||
// providing 25 mature coinbases to allow spending from for testing
|
||||
// purposes.
|
||||
if err := primaryHarness.SetUp(true, 25); err != nil {
|
||||
fmt.Println("unable to setup test chain: ", err)
|
||||
|
||||
// Even though the harness was not fully setup, it still needs
|
||||
// to be torn down to ensure all resources such as temp
|
||||
// directories are cleaned up. The error is intentionally
|
||||
// ignored since this is already an error path and nothing else
|
||||
// could be done about it anyways.
|
||||
_ = primaryHarness.TearDown()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
|
||||
// Clean up any active harnesses that are still currently running.This
|
||||
// includes removing all temporary directories, and shutting down any
|
||||
// created processes.
|
||||
if err := rpctest.TearDownAll(); err != nil {
|
||||
fmt.Println("unable to tear down all harnesses: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func TestRpcServer(t *testing.T) {
|
||||
var currentTestNum int
|
||||
defer func() {
|
||||
// If one of the integration tests caused a panic within the main
|
||||
// goroutine, then tear down all the harnesses in order to avoid
|
||||
// any leaked btcd processes.
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("recovering from test panic: ", r)
|
||||
if err := rpctest.TearDownAll(); err != nil {
|
||||
fmt.Println("unable to tear down all harnesses: ", err)
|
||||
}
|
||||
t.Fatalf("test #%v panicked: %s", currentTestNum, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
for _, testCase := range rpcTestCases {
|
||||
testCase(primaryHarness, t)
|
||||
|
||||
currentTestNum++
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
rpctest
|
||||
=======
|
||||
|
||||
[](https://travis-ci.org/btcsuite/btcd)
|
||||
[](http://copyfree.org)
|
||||
[](http://godoc.org/github.com/btcsuite/btcd/integration/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/integration/rpctest
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Package rpctest is licensed under the [copyfree](http://copyfree.org) ISC
|
||||
License.
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
// 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(wire.TxVersion)
|
||||
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 with a
|
||||
// specified blockversion and timestamp. If the timestamp passed is zero (not
|
||||
// initialized), then the timestamp of the previous block will be used plus 1
|
||||
// second is used. Passing nil for the previous block results in a block that
|
||||
// builds off of the genesis block for the specified chain.
|
||||
func CreateBlock(prevBlock *btcutil.Block, inclusionTxs []*btcutil.Tx,
|
||||
blockVersion int32, blockTime time.Time,
|
||||
miningAddr btcutil.Address, net *chaincfg.Params) (*btcutil.Block, error) {
|
||||
|
||||
var (
|
||||
prevHash *chainhash.Hash
|
||||
blockHeight int32
|
||||
prevBlockTime time.Time
|
||||
)
|
||||
|
||||
// If the previous block isn't specified, then we'll construct a block
|
||||
// that builds off of the genesis block for the chain.
|
||||
if prevBlock == nil {
|
||||
prevHash = net.GenesisHash
|
||||
blockHeight = 1
|
||||
prevBlockTime = net.GenesisBlock.Header.Timestamp.Add(time.Minute)
|
||||
} else {
|
||||
prevHash = prevBlock.Hash()
|
||||
blockHeight = prevBlock.Height() + 1
|
||||
prevBlockTime = prevBlock.MsgBlock().Header.Timestamp
|
||||
}
|
||||
|
||||
// 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 = prevBlockTime.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, false)
|
||||
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
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2017 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"
|
||||
"go/build"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// compileMtx guards access to the executable path so that the project is
|
||||
// only compiled once.
|
||||
compileMtx sync.Mutex
|
||||
|
||||
// executablePath is the path to the compiled executable. This is the empty
|
||||
// string until btcd is compiled. This should not be accessed directly;
|
||||
// instead use the function btcdExecutablePath().
|
||||
executablePath string
|
||||
)
|
||||
|
||||
// btcdExecutablePath returns a path to the btcd executable to be used by
|
||||
// rpctests. To ensure the code tests against the most up-to-date version of
|
||||
// btcd, this method compiles btcd the first time it is called. After that, the
|
||||
// generated binary is used for subsequent test harnesses. The executable file
|
||||
// is not cleaned up, but since it lives at a static path in a temp directory,
|
||||
// it is not a big deal.
|
||||
func btcdExecutablePath() (string, error) {
|
||||
compileMtx.Lock()
|
||||
defer compileMtx.Unlock()
|
||||
|
||||
// If btcd has already been compiled, just use that.
|
||||
if len(executablePath) != 0 {
|
||||
return executablePath, nil
|
||||
}
|
||||
|
||||
testDir, err := baseDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Determine import path of this package. Not necessarily btcsuite/btcd if
|
||||
// this is a forked repo.
|
||||
_, rpctestDir, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Cannot get path to btcd source code")
|
||||
}
|
||||
btcdPkgPath := filepath.Join(rpctestDir, "..", "..", "..")
|
||||
btcdPkg, err := build.ImportDir(btcdPkgPath, build.FindOnly)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to build btcd: %v", err)
|
||||
}
|
||||
|
||||
// Build btcd and output an executable in a static temp path.
|
||||
outputPath := filepath.Join(testDir, "btcd")
|
||||
if runtime.GOOS == "windows" {
|
||||
outputPath += ".exe"
|
||||
}
|
||||
cmd := exec.Command("go", "build", "-o", outputPath, btcdPkg.ImportPath)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to build btcd: %v", err)
|
||||
}
|
||||
|
||||
// Save executable path so future calls do not recompile.
|
||||
executablePath = outputPath
|
||||
return executablePath, nil
|
||||
}
|
||||
+12
@@ -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
@@ -0,0 +1,543 @@
|
||||
// Copyright (c) 2016-2017 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"
|
||||
|
||||
"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/rpcclient"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"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 {
|
||||
blockHeight int32
|
||||
filteredTxns []*btcutil.Tx
|
||||
}
|
||||
|
||||
// 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 *rpcclient.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 *rpcclient.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(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) {
|
||||
// Append this new chain update to the end of the queue of new chain
|
||||
// updates.
|
||||
m.chainMtx.Lock()
|
||||
m.chainUpdates = append(m.chainUpdates, &chainUpdate{height, filteredTxns})
|
||||
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()
|
||||
|
||||
// Update the latest synced height, then process each filtered
|
||||
// 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 update.filteredTxns {
|
||||
mtx := tx.MsgTx()
|
||||
isCoinbase := blockchain.IsCoinBaseTx(mtx)
|
||||
txHash := mtx.TxHash()
|
||||
m.evalOutputs(mtx.TxOut, &txHash, 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(height int32, header *wire.BlockHeader) {
|
||||
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. It also
|
||||
// loads the address into the RPC client's transaction filter to ensure any
|
||||
// transactions that involve it are delivered via the notifications.
|
||||
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
|
||||
}
|
||||
|
||||
err = m.rpc.LoadTxFilter(false, []btcutil.Address{addr}, nil)
|
||||
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, 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(wire.TxVersion)
|
||||
|
||||
// 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, 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
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
rpc "github.com/btcsuite/btcd/rpcclient"
|
||||
"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) {
|
||||
btcdPath, err := btcdExecutablePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &nodeConfig{
|
||||
listen: "127.0.0.1:18555",
|
||||
rpcListen: "127.0.0.1:18556",
|
||||
rpcUser: "user",
|
||||
rpcPass: "pass",
|
||||
extra: extra,
|
||||
prefix: prefix,
|
||||
exe: btcdPath,
|
||||
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{}
|
||||
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
|
||||
}
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
// Copyright (c) 2016-2017 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/rpcclient"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// These constants define the minimum and maximum p2p and rpc port
|
||||
// numbers used by a test harness. The min port is inclusive while the
|
||||
// max port is exclusive.
|
||||
minPeerPort = 10000
|
||||
maxPeerPort = 35000
|
||||
minRPCPort = maxPeerPort
|
||||
maxRPCPort = 60000
|
||||
|
||||
// BlockVersion is the default block version used when generating
|
||||
// blocks.
|
||||
BlockVersion = 4
|
||||
)
|
||||
|
||||
var (
|
||||
// current number of active test nodes.
|
||||
numTestInstances = 0
|
||||
|
||||
// processID is the process ID of the current running process. It is
|
||||
// used to calculate ports based upon it when launching an rpc
|
||||
// harnesses. The intent is to allow multiple process to run in
|
||||
// parallel without port collisions.
|
||||
//
|
||||
// It should be noted however that there is still some small probability
|
||||
// that there will be port collisions either due to other processes
|
||||
// running or simply due to the stars aligning on the process IDs.
|
||||
processID = os.Getpid()
|
||||
|
||||
// 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 *rpcclient.Client
|
||||
node *node
|
||||
handlers *rpcclient.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 *rpcclient.NotificationHandlers,
|
||||
extraArgs []string) (*Harness, error) {
|
||||
|
||||
harnessStateMtx.Lock()
|
||||
defer harnessStateMtx.Unlock()
|
||||
|
||||
// Add a flag for the appropriate network type based on the provided
|
||||
// chain params.
|
||||
switch activeNet.Net {
|
||||
case wire.MainNet:
|
||||
// No extra flags since mainnet is the default
|
||||
case wire.TestNet3:
|
||||
extraArgs = append(extraArgs, "--testnet")
|
||||
case wire.TestNet:
|
||||
extraArgs = append(extraArgs, "--regtest")
|
||||
case wire.SimNet:
|
||||
extraArgs = append(extraArgs, "--simnet")
|
||||
default:
|
||||
return nil, fmt.Errorf("rpctest.New must be called with one " +
|
||||
"of the supported chain networks")
|
||||
}
|
||||
|
||||
testDir, err := baseDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
harnessID := strconv.Itoa(numTestInstances)
|
||||
nodeTestData, err := ioutil.TempDir(testDir, "harness-"+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 = &rpcclient.NotificationHandlers{}
|
||||
}
|
||||
|
||||
// If a handler for the OnFilteredBlock{Connected,Disconnected} callback
|
||||
// callback has already been set, then create a wrapper callback which
|
||||
// executes both the currently registered callback and the mem wallet's
|
||||
// callback.
|
||||
if handlers.OnFilteredBlockConnected != nil {
|
||||
obc := handlers.OnFilteredBlockConnected
|
||||
handlers.OnFilteredBlockConnected = func(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) {
|
||||
wallet.IngestBlock(height, header, filteredTxns)
|
||||
obc(height, header, filteredTxns)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we can claim the callback ourselves.
|
||||
handlers.OnFilteredBlockConnected = wallet.IngestBlock
|
||||
}
|
||||
if handlers.OnFilteredBlockDisconnected != nil {
|
||||
obd := handlers.OnFilteredBlockDisconnected
|
||||
handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) {
|
||||
wallet.UnwindBlock(height, header)
|
||||
obd(height, header)
|
||||
}
|
||||
} else {
|
||||
handlers.OnFilteredBlockDisconnected = 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()
|
||||
|
||||
// Filter transactions that pay to the coinbase associated with the
|
||||
// wallet.
|
||||
filterAddrs := []btcutil.Address{h.wallet.coinbaseAddr}
|
||||
if err := h.Node.LoadTxFilter(true, filterAddrs, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure 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)
|
||||
for range ticker.C {
|
||||
walletHeight := h.wallet.SyncedHeight()
|
||||
if walletHeight == height {
|
||||
break
|
||||
}
|
||||
}
|
||||
ticker.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tearDown stops the running rpc test instance. All created processes are
|
||||
// killed, and temporary directories removed.
|
||||
//
|
||||
// This function MUST be called with the harness state mutex held (for writes).
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
harnessStateMtx.Lock()
|
||||
defer harnessStateMtx.Unlock()
|
||||
|
||||
return h.tearDown()
|
||||
}
|
||||
|
||||
// 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 *rpcclient.Client
|
||||
var err error
|
||||
|
||||
rpcConf := h.node.config.rpcConnConfig()
|
||||
for i := 0; i < h.maxConnRetries; i++ {
|
||||
if client, err = rpcclient.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() rpcclient.ConnConfig {
|
||||
return h.node.config.rpcConnConfig()
|
||||
}
|
||||
|
||||
// P2PAddress returns the harness' P2P listening address. This allows potential
|
||||
// peers (such as SPV peers) created within tests to connect to a given test
|
||||
// harness instance.
|
||||
func (h *Harness) P2PAddress() string {
|
||||
return h.node.config.listen
|
||||
}
|
||||
|
||||
// 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 = BlockVersion
|
||||
}
|
||||
|
||||
prevBlockHash, prevBlockHeight, err := h.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mBlock, err := h.Node.GetBlock(prevBlockHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prevBlock := btcutil.NewBlock(mBlock)
|
||||
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) {
|
||||
localhost := "127.0.0.1"
|
||||
|
||||
portString := func(minPort, maxPort int) string {
|
||||
port := minPort + numTestInstances + ((20 * processID) %
|
||||
(maxPort - minPort))
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
|
||||
p2p := net.JoinHostPort(localhost, portString(minPeerPort, maxPeerPort))
|
||||
rpc := net.JoinHostPort(localhost, portString(minRPCPort, maxRPCPort))
|
||||
return p2p, rpc
|
||||
}
|
||||
|
||||
// baseDir is the directory path of the temp directory for all rpctest files.
|
||||
func baseDir() (string, error) {
|
||||
dirPath := filepath.Join(os.TempDir(), "btcd", "rpctest")
|
||||
err := os.MkdirAll(dirPath, 0755)
|
||||
return dirPath, err
|
||||
}
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
// 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.
|
||||
|
||||
// This file is ignored during the regular tests due to the following build tag.
|
||||
// +build rpctest
|
||||
|
||||
package rpctest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"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.TxHash()
|
||||
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) {
|
||||
nodeAPeers, err := nodeA.Node.GetPeerInfo()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get nodeA's peer info")
|
||||
}
|
||||
|
||||
nodeAddr := nodeB.node.config.listen
|
||||
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 harness.
|
||||
harness, err := New(&chaincfg.SimNetParams, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := harness.SetUp(false, 0); err != nil {
|
||||
t.Fatalf("unable to complete rpctest setup: %v", err)
|
||||
}
|
||||
defer harness.TearDown()
|
||||
|
||||
// Establish a p2p connection from our new local harness to the main
|
||||
// harness.
|
||||
if err := ConnectNode(harness, r); err != nil {
|
||||
t.Fatalf("unable to connect local to main harness: %v", err)
|
||||
}
|
||||
|
||||
// The main harness should show up in our local harness' peer's list,
|
||||
// and vice verse.
|
||||
assertConnectedTo(t, harness, r)
|
||||
}
|
||||
|
||||
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) {
|
||||
// Assert main test harness has no transactions in its mempool.
|
||||
pooledHashes, err := r.Node.GetRawMempool()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get mempool for main test harness: %v", err)
|
||||
}
|
||||
if len(pooledHashes) != 0 {
|
||||
t.Fatal("main test harness mempool not empty")
|
||||
}
|
||||
|
||||
// Create a local test harness with only the genesis block. The nodes
|
||||
// will be synced below so the same transaction can be sent to both
|
||||
// nodes without it being an orphan.
|
||||
harness, err := New(&chaincfg.SimNetParams, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := harness.SetUp(false, 0); 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 mempools: %v", err)
|
||||
}
|
||||
|
||||
// Generate a coinbase spend to a new address within the main harness'
|
||||
// mempool.
|
||||
addr, err := r.NewAddress()
|
||||
addrScript, err := txscript.PayToAddrScript(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate pkscript to addr: %v", err)
|
||||
}
|
||||
output := wire.NewTxOut(5e8, addrScript)
|
||||
testTx, err := r.CreateTransaction([]*wire.TxOut{output}, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("coinbase spend failed: %v", err)
|
||||
}
|
||||
if _, err := r.Node.SendRawTransaction(testTx, true); err != nil {
|
||||
t.Fatalf("send transaction failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait until the transaction shows up to ensure the two mempools are
|
||||
// not the same.
|
||||
harnessSynced := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
poolHashes, err := r.Node.GetRawMempool()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve harness mempool: %v", err)
|
||||
}
|
||||
if len(poolHashes) > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
harnessSynced <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-harnessSynced:
|
||||
case <-time.After(time.Minute):
|
||||
t.Fatalf("harness node never received transaction")
|
||||
}
|
||||
|
||||
// This select case should fall through to the default as the goroutine
|
||||
// should be blocked on the JoinNodes call.
|
||||
poolsSynced := make(chan struct{})
|
||||
go func() {
|
||||
if err := JoinNodes(nodeSlice, Mempools); err != nil {
|
||||
t.Fatalf("unable to join node on mempools: %v", err)
|
||||
}
|
||||
poolsSynced <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-poolsSynced:
|
||||
t.Fatalf("mempools detected as synced yet harness has a new tx")
|
||||
default:
|
||||
}
|
||||
|
||||
// Establish an outbound connection from the local harness to the main
|
||||
// harness and wait for the chains to be synced.
|
||||
if err := ConnectNode(harness, r); err != nil {
|
||||
t.Fatalf("unable to connect harnesses: %v", err)
|
||||
}
|
||||
if err := JoinNodes(nodeSlice, Blocks); err != nil {
|
||||
t.Fatalf("unable to join node on blocks: %v", err)
|
||||
}
|
||||
|
||||
// Send the transaction to the local harness which will result in synced
|
||||
// mempools.
|
||||
if _, err := harness.Node.SendRawTransaction(testTx, true); err != nil {
|
||||
t.Fatalf("send transaction failed: %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("mempools never detected as synced")
|
||||
}
|
||||
}
|
||||
|
||||
func testJoinBlocks(r *Harness, t *testing.T) {
|
||||
// Create a second harness with only the genesis block so it is behind
|
||||
// the main harness.
|
||||
harness, err := New(&chaincfg.SimNetParams, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := harness.SetUp(false, 0); err != nil {
|
||||
t.Fatalf("unable to complete rpctest setup: %v", err)
|
||||
}
|
||||
defer harness.TearDown()
|
||||
|
||||
nodeSlice := []*Harness{r, harness}
|
||||
blocksSynced := make(chan struct{})
|
||||
go func() {
|
||||
if err := JoinNodes(nodeSlice, Blocks); err != nil {
|
||||
t.Fatalf("unable to join node on blocks: %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 local harness is behind")
|
||||
default:
|
||||
}
|
||||
|
||||
// Connect the local harness to the main harness which will sync the
|
||||
// chains.
|
||||
if err := ConnectNode(harness, r); 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 <-blocksSynced:
|
||||
// fall through
|
||||
case <-time.After(time.Minute):
|
||||
t.Fatalf("blocks 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 != BlockVersion {
|
||||
t.Fatalf("block version is not default: expected %v, got %v",
|
||||
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(harness, r); 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 blocks: %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,
|
||||
testJoinBlocks,
|
||||
testJoinMempools, // Depends on results of 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)
|
||||
|
||||
// Even though the harness was not fully setup, it still needs
|
||||
// to be torn down to ensure all resources such as temp
|
||||
// directories are cleaned up. The error is intentionally
|
||||
// ignored since this is already an error path and nothing else
|
||||
// could be done about it anyways.
|
||||
_ = mainHarness.TearDown()
|
||||
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)
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// 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 (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
retry:
|
||||
for !poolsMatch {
|
||||
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)
|
||||
continue retry
|
||||
}
|
||||
}
|
||||
|
||||
poolsMatch = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncBlocks blocks until all nodes report the same best chain.
|
||||
func syncBlocks(nodes []*Harness) error {
|
||||
blocksMatch := false
|
||||
|
||||
retry:
|
||||
for !blocksMatch {
|
||||
var prevHash *chainhash.Hash
|
||||
var prevHeight int32
|
||||
for _, node := range nodes {
|
||||
blockHash, blockHeight, err := node.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prevHash != nil && (*blockHash != *prevHash ||
|
||||
blockHeight != prevHeight) {
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
continue retry
|
||||
}
|
||||
prevHash, prevHeight = blockHash, blockHeight
|
||||
}
|
||||
|
||||
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 {
|
||||
peerInfo, err := from.Node.GetPeerInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
numPeers := len(peerInfo)
|
||||
|
||||
targetAddr := to.node.config.listen
|
||||
if err := from.Node.AddNode(targetAddr, rpcclient.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
|
||||
}
|
||||
Reference in New Issue
Block a user