From 9fc6ed6003da42f035309240c715ce0fd063ec03 Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Mon, 7 Dec 2015 14:47:58 +0100 Subject: [PATCH 1/2] net: Fix sent reject messages for blocks and transactions Ever since we #5913 have been sending invalid reject messages for transactions and blocks. --- src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a0e996ae7..84f737258 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4824,7 +4824,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, pfrom->id, FormatStateMessage(state)); if (state.GetRejectCode() < REJECT_INTERNAL) // Never send AcceptToMemoryPool's internal codes over P2P - pfrom->PushMessage("reject", strCommand, state.GetRejectCode(), + pfrom->PushMessage("reject", strCommand, (unsigned char)state.GetRejectCode(), state.GetRejectReason().substr(0, MAX_REJECT_MESSAGE_LENGTH), inv.hash); if (nDoS > 0) Misbehaving(pfrom->GetId(), nDoS); @@ -4954,7 +4954,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv, int nDoS; if (state.IsInvalid(nDoS)) { assert (state.GetRejectCode() < REJECT_INTERNAL); // Blocks are never rejected with internal reject codes - pfrom->PushMessage("reject", strCommand, state.GetRejectCode(), + pfrom->PushMessage("reject", strCommand, (unsigned char)state.GetRejectCode(), state.GetRejectReason().substr(0, MAX_REJECT_MESSAGE_LENGTH), inv.hash); if (nDoS > 0) { LOCK(cs_main); From 20411903d7b356ebb174df2daad1dcd5d6117f79 Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Tue, 8 Dec 2015 17:10:41 +0100 Subject: [PATCH 2/2] test: Add basic test for `reject` code Extend P2P test framework to make it possible to expect reject codes for transactions and blocks. --- qa/pull-tester/rpc-tests.py | 3 +- qa/rpc-tests/invalidblockrequest.py | 6 +- qa/rpc-tests/invalidtxrequest.py | 76 +++++++++++++++++++++++++ qa/rpc-tests/test_framework/comptool.py | 40 +++++++++++++ 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100755 qa/rpc-tests/invalidtxrequest.py diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index df71e44b6..0cb721b03 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -100,6 +100,8 @@ testScripts = [ 'sendheaders.py', 'keypool.py', 'prioritise_transaction.py', + 'invalidblockrequest.py', + 'invalidtxrequest.py', ] testScriptsExt = [ 'bip65-cltv.py', @@ -116,7 +118,6 @@ testScriptsExt = [ # 'rpcbind_test.py', #temporary, bug in libevent, see #6655 'smartfees.py', 'maxblocksinflight.py', - 'invalidblockrequest.py', 'p2p-acceptblock.py', 'mempool_packages.py', 'maxuploadtarget.py', diff --git a/qa/rpc-tests/invalidblockrequest.py b/qa/rpc-tests/invalidblockrequest.py index 6a7980cd4..a74ecb128 100755 --- a/qa/rpc-tests/invalidblockrequest.py +++ b/qa/rpc-tests/invalidblockrequest.py @@ -6,7 +6,7 @@ from test_framework.test_framework import ComparisonTestFramework from test_framework.util import * -from test_framework.comptool import TestManager, TestInstance +from test_framework.comptool import TestManager, TestInstance, RejectResult from test_framework.mininode import * from test_framework.blocktools import * import logging @@ -97,7 +97,7 @@ class InvalidBlockRequestTest(ComparisonTestFramework): assert(block2_orig.vtx != block2.vtx) self.tip = block2.sha256 - yield TestInstance([[block2, False], [block2_orig, True]]) + yield TestInstance([[block2, RejectResult(16,'bad-txns-duplicate')], [block2_orig, True]]) height += 1 ''' @@ -112,7 +112,7 @@ class InvalidBlockRequestTest(ComparisonTestFramework): block3.rehash() block3.solve() - yield TestInstance([[block3, False]]) + yield TestInstance([[block3, RejectResult(16,'bad-cb-amount')]]) if __name__ == '__main__': diff --git a/qa/rpc-tests/invalidtxrequest.py b/qa/rpc-tests/invalidtxrequest.py new file mode 100755 index 000000000..d17b3d098 --- /dev/null +++ b/qa/rpc-tests/invalidtxrequest.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python2 +# +# Distributed under the MIT/X11 software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# + +from test_framework.test_framework import ComparisonTestFramework +from test_framework.util import * +from test_framework.comptool import TestManager, TestInstance, RejectResult +from test_framework.mininode import * +from test_framework.blocktools import * +import logging +import copy +import time + + +''' +In this test we connect to one node over p2p, and test tx requests. +''' + +# Use the ComparisonTestFramework with 1 node: only use --testbinary. +class InvalidTxRequestTest(ComparisonTestFramework): + + ''' Can either run this test as 1 node with expected answers, or two and compare them. + Change the "outcome" variable from each TestInstance object to only do the comparison. ''' + def __init__(self): + self.num_nodes = 1 + + def run_test(self): + test = TestManager(self, self.options.tmpdir) + test.add_all_connections(self.nodes) + self.tip = None + self.block_time = None + NetworkThread().start() # Start up network handling in another thread + test.run() + + def get_tests(self): + if self.tip is None: + self.tip = int ("0x" + self.nodes[0].getbestblockhash() + "L", 0) + self.block_time = int(time.time())+1 + + ''' + Create a new block with an anyone-can-spend coinbase + ''' + height = 1 + block = create_block(self.tip, create_coinbase(height), self.block_time) + self.block_time += 1 + block.solve() + # Save the coinbase for later + self.block1 = block + self.tip = block.sha256 + height += 1 + yield TestInstance([[block, True]]) + + ''' + Now we need that block to mature so we can spend the coinbase. + ''' + test = TestInstance(sync_every_block=False) + for i in xrange(100): + block = create_block(self.tip, create_coinbase(height), self.block_time) + block.solve() + self.tip = block.sha256 + self.block_time += 1 + test.blocks_and_transactions.append([block, True]) + height += 1 + yield test + + # chr(100) is OP_NOTIF + # Transaction will be rejected with code 16 (REJECT_INVALID) + tx1 = create_transaction(self.block1.vtx[0], 0, chr(100), 50*100000000) + yield TestInstance([[tx1, RejectResult(16, 'mandatory-script-verify-flag-failed')]]) + + # TODO: test further transactions... + +if __name__ == '__main__': + InvalidTxRequestTest().main() diff --git a/qa/rpc-tests/test_framework/comptool.py b/qa/rpc-tests/test_framework/comptool.py index 9444424dc..badbc0a1f 100755 --- a/qa/rpc-tests/test_framework/comptool.py +++ b/qa/rpc-tests/test_framework/comptool.py @@ -41,6 +41,20 @@ def wait_until(predicate, attempts=float('inf'), timeout=float('inf')): return False +class RejectResult(object): + ''' + Outcome that expects rejection of a transaction or block. + ''' + def __init__(self, code, reason=''): + self.code = code + self.reason = reason + def match(self, other): + if self.code != other.code: + return False + return other.reason.startswith(self.reason) + def __repr__(self): + return '%i:%s' % (self.code,self.reason or '*') + class TestNode(NodeConnCB): def __init__(self, block_store, tx_store): @@ -51,6 +65,8 @@ class TestNode(NodeConnCB): self.block_request_map = {} self.tx_store = tx_store self.tx_request_map = {} + self.block_reject_map = {} + self.tx_reject_map = {} # When the pingmap is non-empty we're waiting for # a response @@ -94,6 +110,12 @@ class TestNode(NodeConnCB): except KeyError: raise AssertionError("Got pong for unknown ping [%s]" % repr(message)) + def on_reject(self, conn, message): + if message.message == 'tx': + self.tx_reject_map[message.data] = RejectResult(message.code, message.reason) + if message.message == 'block': + self.block_reject_map[message.data] = RejectResult(message.code, message.reason) + def send_inv(self, obj): mtype = 2 if isinstance(obj, CBlock) else 1 self.conn.send_message(msg_inv([CInv(mtype, obj.sha256)])) @@ -243,6 +265,15 @@ class TestManager(object): if outcome is None: if c.cb.bestblockhash != self.connections[0].cb.bestblockhash: return False + elif isinstance(outcome, RejectResult): # Check that block was rejected w/ code + if c.cb.bestblockhash == blockhash: + return False + if blockhash not in c.cb.block_reject_map: + print 'Block not in reject map: %064x' % (blockhash) + return False + if not outcome.match(c.cb.block_reject_map[blockhash]): + print 'Block rejected with %s instead of expected %s: %064x' % (c.cb.block_reject_map[blockhash], outcome, blockhash) + return False elif ((c.cb.bestblockhash == blockhash) != outcome): # print c.cb.bestblockhash, blockhash, outcome return False @@ -262,6 +293,15 @@ class TestManager(object): if c.cb.lastInv != self.connections[0].cb.lastInv: # print c.rpc.getrawmempool() return False + elif isinstance(outcome, RejectResult): # Check that tx was rejected w/ code + if txhash in c.cb.lastInv: + return False + if txhash not in c.cb.tx_reject_map: + print 'Tx not in reject map: %064x' % (txhash) + return False + if not outcome.match(c.cb.tx_reject_map[txhash]): + print 'Tx rejected with %s instead of expected %s: %064x' % (c.cb.tx_reject_map[txhash], outcome, txhash) + return False elif ((txhash in c.cb.lastInv) != outcome): # print c.rpc.getrawmempool(), c.cb.lastInv return False