Skip to content

Commit b93974c

Browse files
committed
Add comparison tool test runner, built on mininode
comptool.py creates a tool for running a test suite on top of the mininode p2p framework. It supports two types of tests: those for which we expect certain behavior (acceptance or rejection of a block or transaction) and those for which we are just comparing that the behavior of 2 or more nodes is the same. blockstore.py defines BlockStore and TxStore, which provide db-backed maps between block/tx hashes and the corresponding block or tx. blocktools.py defines utility functions for creating and manipulating blocks and transactions. invalidblockrequest.py is an example test in the comptool framework, which tests the behavior of a single node when sent two different types of invalid blocks (a block with a duplicated transaction and a block with a bad coinbase value).
1 parent 6c1d1ba commit b93974c

File tree

6 files changed

+667
-0
lines changed

6 files changed

+667
-0
lines changed

qa/pull-tester/rpc-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ testScripts=(
3131
'merkle_blocks.py'
3232
# 'forknotify.py'
3333
'maxblocksinflight.py'
34+
'invalidblockrequest.py'
3435
);
3536
if [ "x${ENABLE_BITCOIND}${ENABLE_UTILS}${ENABLE_WALLET}" = "x111" ]; then
3637
for (( i = 0; i < ${#testScripts[@]}; i++ ))

qa/rpc-tests/blockstore.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# BlockStore: a helper class that keeps a map of blocks and implements
2+
# helper functions for responding to getheaders and getdata,
3+
# and for constructing a getheaders message
4+
#
5+
6+
from mininode import *
7+
import dbm
8+
9+
class BlockStore(object):
10+
def __init__(self, datadir):
11+
self.blockDB = dbm.open(datadir + "/blocks", 'c')
12+
self.currentBlock = 0L
13+
14+
def close(self):
15+
self.blockDB.close()
16+
17+
def get(self, blockhash):
18+
serialized_block = None
19+
try:
20+
serialized_block = self.blockDB[repr(blockhash)]
21+
except KeyError:
22+
return None
23+
f = cStringIO.StringIO(serialized_block)
24+
ret = CBlock()
25+
ret.deserialize(f)
26+
ret.calc_sha256()
27+
return ret
28+
29+
# Note: this pulls full blocks out of the database just to retrieve
30+
# the headers -- perhaps we could keep a separate data structure
31+
# to avoid this overhead.
32+
def headers_for(self, locator, hash_stop, current_tip=None):
33+
if current_tip is None:
34+
current_tip = self.currentBlock
35+
current_block = self.get(current_tip)
36+
if current_block is None:
37+
return None
38+
39+
response = msg_headers()
40+
headersList = [ CBlockHeader(current_block) ]
41+
maxheaders = 2000
42+
while (headersList[0].sha256 not in locator.vHave):
43+
prevBlockHash = headersList[0].hashPrevBlock
44+
prevBlock = self.get(prevBlockHash)
45+
if prevBlock is not None:
46+
headersList.insert(0, CBlockHeader(prevBlock))
47+
else:
48+
break
49+
headersList = headersList[:maxheaders] # truncate if we have too many
50+
hashList = [x.sha256 for x in headersList]
51+
index = len(headersList)
52+
if (hash_stop in hashList):
53+
index = hashList.index(hash_stop)+1
54+
response.headers = headersList[:index]
55+
return response
56+
57+
def add_block(self, block):
58+
block.calc_sha256()
59+
try:
60+
self.blockDB[repr(block.sha256)] = bytes(block.serialize())
61+
except TypeError as e:
62+
print "Unexpected error: ", sys.exc_info()[0], e.args
63+
self.currentBlock = block.sha256
64+
65+
def get_blocks(self, inv):
66+
responses = []
67+
for i in inv:
68+
if (i.type == 2): # MSG_BLOCK
69+
block = self.get(i.hash)
70+
if block is not None:
71+
responses.append(msg_block(block))
72+
return responses
73+
74+
def get_locator(self, current_tip=None):
75+
if current_tip is None:
76+
current_tip = self.currentBlock
77+
r = []
78+
counter = 0
79+
step = 1
80+
lastBlock = self.get(current_tip)
81+
while lastBlock is not None:
82+
r.append(lastBlock.hashPrevBlock)
83+
for i in range(step):
84+
lastBlock = self.get(lastBlock.hashPrevBlock)
85+
if lastBlock is None:
86+
break
87+
counter += 1
88+
if counter > 10:
89+
step *= 2
90+
locator = CBlockLocator()
91+
locator.vHave = r
92+
return locator
93+
94+
class TxStore(object):
95+
def __init__(self, datadir):
96+
self.txDB = dbm.open(datadir + "/transactions", 'c')
97+
98+
def close(self):
99+
self.txDB.close()
100+
101+
def get(self, txhash):
102+
serialized_tx = None
103+
try:
104+
serialized_tx = self.txDB[repr(txhash)]
105+
except KeyError:
106+
return None
107+
f = cStringIO.StringIO(serialized_tx)
108+
ret = CTransaction()
109+
ret.deserialize(f)
110+
ret.calc_sha256()
111+
return ret
112+
113+
def add_transaction(self, tx):
114+
tx.calc_sha256()
115+
try:
116+
self.txDB[repr(tx.sha256)] = bytes(tx.serialize())
117+
except TypeError as e:
118+
print "Unexpected error: ", sys.exc_info()[0], e.args
119+
120+
def get_transactions(self, inv):
121+
responses = []
122+
for i in inv:
123+
if (i.type == 1): # MSG_TX
124+
tx = self.get(i.hash)
125+
if tx is not None:
126+
responses.append(msg_tx(tx))
127+
return responses

qa/rpc-tests/blocktools.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# blocktools.py - utilities for manipulating blocks and transactions
2+
#
3+
# Distributed under the MIT/X11 software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
#
6+
7+
from mininode import *
8+
from script import CScript, CScriptOp
9+
10+
# Create a block (with regtest difficulty)
11+
def create_block(hashprev, coinbase, nTime=None):
12+
block = CBlock()
13+
if nTime is None:
14+
import time
15+
block.nTime = int(time.time()+600)
16+
else:
17+
block.nTime = nTime
18+
block.hashPrevBlock = hashprev
19+
block.nBits = 0x207fffff # Will break after a difficulty adjustment...
20+
block.vtx.append(coinbase)
21+
block.hashMerkleRoot = block.calc_merkle_root()
22+
block.calc_sha256()
23+
return block
24+
25+
def serialize_script_num(value):
26+
r = bytearray(0)
27+
if value == 0:
28+
return r
29+
neg = value < 0
30+
absvalue = -value if neg else value
31+
while (absvalue):
32+
r.append(chr(absvalue & 0xff))
33+
absvalue >>= 8
34+
if r[-1] & 0x80:
35+
r.append(0x80 if neg else 0)
36+
elif neg:
37+
r[-1] |= 0x80
38+
return r
39+
40+
counter=1
41+
# Create an anyone-can-spend coinbase transaction, assuming no miner fees
42+
def create_coinbase(heightAdjust = 0):
43+
global counter
44+
coinbase = CTransaction()
45+
coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff),
46+
ser_string(serialize_script_num(counter+heightAdjust)), 0xffffffff))
47+
counter += 1
48+
coinbaseoutput = CTxOut()
49+
coinbaseoutput.nValue = 50*100000000
50+
halvings = int((counter+heightAdjust)/150) # regtest
51+
coinbaseoutput.nValue >>= halvings
52+
coinbaseoutput.scriptPubKey = ""
53+
coinbase.vout = [ coinbaseoutput ]
54+
coinbase.calc_sha256()
55+
return coinbase
56+
57+
# Create a transaction with an anyone-can-spend output, that spends the
58+
# nth output of prevtx.
59+
def create_transaction(prevtx, n, sig, value):
60+
tx = CTransaction()
61+
assert(n < len(prevtx.vout))
62+
tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), sig, 0xffffffff))
63+
tx.vout.append(CTxOut(value, ""))
64+
tx.calc_sha256()
65+
return tx

0 commit comments

Comments
 (0)