Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-mempool-coherence-on-activations.py
- This file was copied from test/functional/abc-replay-protection.py.
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Copyright (c) 2015-2016 The Bitcoin Core developers | # Copyright (c) 2015-2016 The Bitcoin Core developers | ||||
# Copyright (c) 2017 The Bitcoin developers | # Copyright (c) 2017 The Bitcoin developers | ||||
# Distributed under the MIT software license, see the accompanying | # Distributed under the MIT software license, see the accompanying | ||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
""" | """ | ||||
This test checks activation of UAHF and the different consensus | This test checks the mempool coherence when changing validation rulesets, | ||||
related to this activation. | which happens on (de)activations of network upgrades. | ||||
It is derived from the much more complex p2p-fullblocktest. | |||||
""" | """ | ||||
from test_framework.test_framework import ComparisonTestFramework | from test_framework.test_framework import ComparisonTestFramework | ||||
from test_framework.util import assert_equal, assert_raises_rpc_error | from test_framework.util import assert_equal, assert_raises_rpc_error | ||||
from test_framework.comptool import TestManager, TestInstance, RejectResult | from test_framework.comptool import TestManager, TestInstance | ||||
from test_framework.blocktools import * | from test_framework.blocktools import * | ||||
import time | import time | ||||
from test_framework.key import CECKey | from test_framework.key import CECKey | ||||
from test_framework.script import * | from test_framework.script import * | ||||
# far into the future | # We use the replay protection activation for this test, as it's always | ||||
REPLAY_PROTECTION_START_TIME = 2000000000 | # present in the client code | ||||
ACTIVATION_TIME = 2000000000 | |||||
# Error due to invalid signature | # Error due to invalid signature | ||||
INVALID_SIGNATURE_ERROR = b'mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)' | INVALID_SIGNATURE_ERROR = b'mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)' | ||||
RPC_INVALID_SIGNATURE_ERROR = "16: " + \ | RPC_INVALID_SIGNATURE_ERROR = "16: " + \ | ||||
INVALID_SIGNATURE_ERROR.decode("utf-8") | INVALID_SIGNATURE_ERROR.decode("utf-8") | ||||
class PreviousSpendableOutput(object): | class PreviousSpendableOutput(object): | ||||
def __init__(self, tx=CTransaction(), n=-1): | def __init__(self, tx=CTransaction(), n=-1): | ||||
self.tx = tx | self.tx = tx | ||||
self.n = n # the output we're spending | self.n = n # the output we're spending | ||||
class ReplayProtectionTest(ComparisonTestFramework): | class ReplayProtectionTest(ComparisonTestFramework): | ||||
def set_test_params(self): | def set_test_params(self): | ||||
self.num_nodes = 1 | self.num_nodes = 1 | ||||
self.setup_clean_chain = True | self.setup_clean_chain = True | ||||
self.block_heights = {} | self.block_heights = {} | ||||
self.tip = None | self.tip = None | ||||
self.blocks = {} | self.blocks = {} | ||||
self.extra_args = [['-whitelist=127.0.0.1', | self.extra_args = [['-whitelist=127.0.0.1', | ||||
"-replayprotectionactivationtime=%d" % REPLAY_PROTECTION_START_TIME]] | "-replayprotectionactivationtime=%d" % ACTIVATION_TIME]] | ||||
def run_test(self): | def run_test(self): | ||||
self.test = TestManager(self, self.options.tmpdir) | self.test = TestManager(self, self.options.tmpdir) | ||||
self.test.add_all_connections(self.nodes) | self.test.add_all_connections(self.nodes) | ||||
network_thread_start() | network_thread_start() | ||||
self.nodes[0].setmocktime(REPLAY_PROTECTION_START_TIME) | self.nodes[0].setmocktime(ACTIVATION_TIME) | ||||
self.test.run() | self.test.run() | ||||
def next_block(self, number): | def next_block(self, number): | ||||
if self.tip == None: | if self.tip == None: | ||||
base_block_hash = self.genesis_hash | base_block_hash = self.genesis_hash | ||||
block_time = int(time.time()) + 1 | block_time = int(time.time()) + 1 | ||||
else: | else: | ||||
base_block_hash = self.tip.sha256 | base_block_hash = self.tip.sha256 | ||||
▲ Show 20 Lines • Show All 64 Lines • ▼ Show 20 Lines | def get_tests(self): | ||||
# Create a new block | # Create a new block | ||||
block(0) | block(0) | ||||
save_spendable_output() | save_spendable_output() | ||||
yield accepted() | yield accepted() | ||||
# Now we need that block to mature so we can spend the coinbase. | # Now we need that block to mature so we can spend the coinbase. | ||||
test = TestInstance(sync_every_block=False) | test = TestInstance(sync_every_block=False) | ||||
for i in range(99): | for i in range(110): | ||||
block(5000 + i) | block(5000 + i) | ||||
test.blocks_and_transactions.append([self.tip, True]) | test.blocks_and_transactions.append([self.tip, True]) | ||||
save_spendable_output() | save_spendable_output() | ||||
yield test | yield test | ||||
# collect spendable outputs now to avoid cluttering the code later on | # collect spendable outputs now to avoid cluttering the code later on | ||||
out = [] | out = [] | ||||
for i in range(100): | for i in range(100): | ||||
out.append(get_spendable_output()) | out.append(get_spendable_output()) | ||||
# Generate a key pair to test P2SH sigops count | # Generate a key pair to test | ||||
private_key = CECKey() | private_key = CECKey() | ||||
private_key.set_secretbytes(b"replayprotection") | private_key.set_secretbytes(b"replayprotection") | ||||
public_key = private_key.get_pubkey() | public_key = private_key.get_pubkey() | ||||
# This is a little handier to use than the version in blocktools.py | # Creates fund and spend transactions using the forkid replay | ||||
# protection mechanism. It's used to create transactions that are valid | |||||
# on only one side of the fork. | |||||
def create_fund_and_spend_tx(spend, forkvalue=0): | def create_fund_and_spend_tx(spend, forkvalue=0): | ||||
# Fund transaction | # Fund transaction | ||||
script = CScript([public_key, OP_CHECKSIG]) | script = CScript([public_key, OP_CHECKSIG]) | ||||
txfund = create_transaction( | txfund = create_transaction( | ||||
spend.tx, spend.n, b'', 50 * COIN, script) | spend.tx, spend.n, b'', 50 * COIN, script) | ||||
txfund.rehash() | txfund.rehash() | ||||
# Spend transaction | # Spend transaction | ||||
txspend = CTransaction() | txspend = CTransaction() | ||||
txspend.vout.append(CTxOut(50 * COIN - 1000, CScript([OP_TRUE]))) | txspend.vout.append(CTxOut(50 * COIN - 1000, CScript([OP_TRUE]))) | ||||
txspend.vin.append(CTxIn(COutPoint(txfund.sha256, 0), b'')) | txspend.vin.append(CTxIn(COutPoint(txfund.sha256, 0), b'')) | ||||
# Sign the transaction | # Sign the transaction | ||||
sighashtype = (forkvalue << 8) | SIGHASH_ALL | SIGHASH_FORKID | sighashtype = (forkvalue << 8) | SIGHASH_ALL | SIGHASH_FORKID | ||||
sighash = SignatureHashForkId( | sighash = SignatureHashForkId( | ||||
script, txspend, 0, sighashtype, 50 * COIN) | script, txspend, 0, sighashtype, 50 * COIN) | ||||
sig = private_key.sign(sighash) + \ | sig = private_key.sign(sighash) + \ | ||||
bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])) | bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])) | ||||
txspend.vin[0].scriptSig = CScript([sig]) | txspend.vin[0].scriptSig = CScript([sig]) | ||||
txspend.rehash() | txspend.rehash() | ||||
return [txfund, txspend] | return txfund, txspend | ||||
# Creates a chainable tx, spending a scriptPub=OP_TRUE coin into | |||||
# another | |||||
def create_chaining_tx(spend): | |||||
if isinstance(spend, CTransaction): | |||||
# After the first iteration, we start chaining txns here | |||||
tx = create_transaction( | |||||
spend, 0, b'', spend.vout[0].nValue - 1000, CScript([OP_TRUE])) | |||||
else: | |||||
tx = create_transaction( | |||||
spend.tx, spend.n, b'', 50 * COIN, CScript([OP_TRUE])) | |||||
tx.rehash() | |||||
return tx | |||||
def send_transaction_to_mempool(tx): | def send_transaction_to_mempool(tx): | ||||
tx_id = node.sendrawtransaction(ToHex(tx)) | tx_id = node.sendrawtransaction(ToHex(tx)) | ||||
assert(tx_id in set(node.getrawmempool())) | assert(tx_id in node.getrawmempool()) | ||||
return tx_id | return tx_id | ||||
# Before the fork, no replay protection required to get in the mempool. | def check_mempool_equal(txns): | ||||
txns = create_fund_and_spend_tx(out[0]) | assert(set(node.getrawmempool()) == set([tx.hash for tx in txns])) | ||||
send_transaction_to_mempool(txns[0]) | |||||
send_transaction_to_mempool(txns[1]) | |||||
# And txns get mined in a block properly. | |||||
block(1) | |||||
update_block(1, txns) | |||||
yield accepted() | |||||
# Replay protected transactions are rejected. | |||||
replay_txns = create_fund_and_spend_tx(out[1], 0xffdead) | |||||
send_transaction_to_mempool(replay_txns[0]) | |||||
assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | |||||
node.sendrawtransaction, ToHex(replay_txns[1])) | |||||
# And block containing them are rejected as well. | |||||
block(2) | |||||
update_block(2, replay_txns) | |||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | |||||
# Rewind bad block | # Create 2 transactions that are only valid before activation | ||||
tip(1) | # (pre-fork-only) | ||||
txfund0, tx_pre0 = create_fund_and_spend_tx(out[0], 0) | |||||
txfund1, tx_pre1 = create_fund_and_spend_tx(out[1], 0) | |||||
# Create 2 transactions that are only valid after activation | |||||
# (post-fork-only) | |||||
txfund2, tx_post0 = create_fund_and_spend_tx(out[2], 0xffdead) | |||||
txfund3, tx_post1 = create_fund_and_spend_tx(out[3], 0xffdead) | |||||
# Create a block that would activate the replay protection. | # Create blocks to activate the fork. Mine all funding transactions | ||||
bfork = block(5555) | bfork = block(5555) | ||||
bfork.nTime = REPLAY_PROTECTION_START_TIME - 1 | bfork.nTime = ACTIVATION_TIME - 1 | ||||
update_block(5555, []) | update_block(5555, [txfund0, txfund1, txfund2, txfund3]) | ||||
yield accepted() | yield accepted() | ||||
for i in range(5): | for i in range(5): | ||||
block(5100 + i) | block(5200 + i) | ||||
test.blocks_and_transactions.append([self.tip, True]) | test.blocks_and_transactions.append([self.tip, True]) | ||||
yield test | yield test | ||||
# Check we are just before the activation time | # Check we are just before the activation time | ||||
assert_equal(node.getblockheader(node.getbestblockhash())['mediantime'], | assert_equal( | ||||
REPLAY_PROTECTION_START_TIME - 1) | node.getblockheader(node.getbestblockhash())['mediantime'], | ||||
ACTIVATION_TIME - 1) | |||||
# We are just before the fork, replay protected txns still are rejected | |||||
# We are just before the fork | |||||
# Pre-fork-only and always-valid txns are valid but post-fork-only txns | |||||
# are rejected | |||||
send_transaction_to_mempool(tx_pre0) | |||||
send_transaction_to_mempool(tx_pre1) | |||||
tx_chain0 = create_chaining_tx(out[4]) | |||||
tx_chain1 = create_chaining_tx(tx_chain0) | |||||
send_transaction_to_mempool(tx_chain0) | |||||
send_transaction_to_mempool(tx_chain1) | |||||
assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | ||||
node.sendrawtransaction, ToHex(replay_txns[1])) | node.sendrawtransaction, ToHex(tx_post0)) | ||||
assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | |||||
block(3) | node.sendrawtransaction, ToHex(tx_post1)) | ||||
update_block(3, replay_txns) | check_mempool_equal([tx_chain0, tx_chain1, tx_pre0, tx_pre1]) | ||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | |||||
# Rewind bad block | |||||
tip(5104) | |||||
# Send some non replay protected txns in the mempool to check | |||||
# they get cleaned at activation. | |||||
txns = create_fund_and_spend_tx(out[2]) | |||||
send_transaction_to_mempool(txns[0]) | |||||
tx_id = send_transaction_to_mempool(txns[1]) | |||||
# Activate the replay protection | # Activate the fork. Mine the first chained txn and a pre-fork-only txn | ||||
block(5556) | block(5556) | ||||
update_block(5556, [tx_chain0, tx_pre0]) | |||||
yield accepted() | yield accepted() | ||||
# Check we just activated the replay protection | # Check we just activated the fork | ||||
assert_equal(node.getblockheader(node.getbestblockhash())['mediantime'], | assert_equal( | ||||
REPLAY_PROTECTION_START_TIME) | node.getblockheader(node.getbestblockhash())['mediantime'], | ||||
ACTIVATION_TIME) | |||||
# Non replay protected transactions are not valid anymore, | |||||
# so they should be removed from the mempool. | # Pre-fork-only txns were evicted from the mempool, while always-valid | ||||
assert(tx_id not in set(node.getrawmempool())) | # txn remain | ||||
# Evicted: tx_pre1 | |||||
# Good old transactions are now invalid. | check_mempool_equal([tx_chain1]) | ||||
send_transaction_to_mempool(txns[0]) | |||||
# Post-fork-only and always-valid txns are accepted, pre-fork-only txn | |||||
# are rejected | |||||
send_transaction_to_mempool(tx_post0) | |||||
send_transaction_to_mempool(tx_post1) | |||||
tx_chain2 = create_chaining_tx(tx_chain1) | |||||
send_transaction_to_mempool(tx_chain2) | |||||
assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | assert_raises_rpc_error(-26, RPC_INVALID_SIGNATURE_ERROR, | ||||
node.sendrawtransaction, ToHex(txns[1])) | node.sendrawtransaction, ToHex(tx_pre1)) | ||||
check_mempool_equal([tx_chain1, tx_chain2, tx_post0, tx_post1]) | |||||
# They also cannot be mined | # Mine the second chained txn and a post-fork-only txn | ||||
block(4) | block(5557) | ||||
update_block(4, txns) | update_block(5557, [tx_chain1, tx_post0]) | ||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | |||||
# Rewind bad block | |||||
tip(5556) | |||||
# The replay protected transaction is now valid | |||||
replay_tx0_id = send_transaction_to_mempool(replay_txns[0]) | |||||
replay_tx1_id = send_transaction_to_mempool(replay_txns[1]) | |||||
# Make sure the transaction are ready to be mined. | |||||
tmpl = node.getblocktemplate() | |||||
found_id0 = False | |||||
found_id1 = False | |||||
for txn in tmpl['transactions']: | |||||
txid = txn['txid'] | |||||
if txid == replay_tx0_id: | |||||
found_id0 = True | |||||
elif txid == replay_tx1_id: | |||||
found_id1 = True | |||||
assert(found_id0 and found_id1) | |||||
# And the mempool is still in good shape. | |||||
assert(replay_tx0_id in set(node.getrawmempool())) | |||||
assert(replay_tx1_id in set(node.getrawmempool())) | |||||
# They also can also be mined | |||||
b5 = block(5) | |||||
update_block(5, replay_txns) | |||||
yield accepted() | yield accepted() | ||||
# Ok, now we check if a reorg work properly accross the activation. | # So the mempool contains the 3rd chained txn and a post-fork-only txn | ||||
check_mempool_equal([tx_chain2, tx_post1]) | |||||
# Now we check if a reorg work properly across the activation. | |||||
postforkblockid = node.getbestblockhash() | postforkblockid = node.getbestblockhash() | ||||
node.invalidateblock(postforkblockid) | node.invalidateblock(postforkblockid) | ||||
assert(replay_tx0_id in set(node.getrawmempool())) | # Transactions mined post-fork returned to the mempool | ||||
assert(replay_tx1_id in set(node.getrawmempool())) | check_mempool_equal([tx_chain1, tx_chain2, tx_post0, tx_post1]) | ||||
# Deactivating replay protection. | # Deactivate the fork | ||||
forkblockid = node.getbestblockhash() | forkblockid = node.getbestblockhash() | ||||
node.invalidateblock(forkblockid) | node.invalidateblock(forkblockid) | ||||
assert(replay_tx0_id not in set(node.getrawmempool())) | # Post-fork-only txns were evicted from the mempool, while txns from | ||||
assert(replay_tx1_id not in set(node.getrawmempool())) | # the fork activation block returned to the mempool | ||||
# Evicted: tx_post0, tx_post1 | |||||
check_mempool_equal([tx_chain0, tx_chain1, tx_chain2, tx_pre0]) | |||||
# Check that we also do it properly on deeper reorg. | # Check that we do it properly on a reorg that deactivates and | ||||
node.reconsiderblock(forkblockid) | # reactivates the fork at the same time | ||||
node.reconsiderblock(postforkblockid) | node.reconsiderblock(postforkblockid) | ||||
node.invalidateblock(forkblockid) | node.reconsiderblock(forkblockid) | ||||
assert(replay_tx0_id not in set(node.getrawmempool())) | # Send post-fork-only txn into the mempool again | ||||
assert(replay_tx1_id not in set(node.getrawmempool())) | send_transaction_to_mempool(tx_post1) | ||||
check_mempool_equal([tx_chain2, tx_post1]) | |||||
# Create a longer competing chain | |||||
tip(5204) | |||||
block(8) | |||||
block(9) | |||||
block(10) | |||||
yield TestInstance([[self.blocks[8], True], [self.blocks[9], True], [self.blocks[10], True]], False) | |||||
# The mempool was dumped and reevaluated post-fork, thus post-fork-only | |||||
# txn survived this reorg! | |||||
# Evicted: tx_pre0 (which had been mined in the activation block in the | |||||
# now shorter chain) | |||||
check_mempool_equal( | |||||
[tx_chain0, tx_chain1, tx_chain2, tx_post0, tx_post1]) | |||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
ReplayProtectionTest().main() | ReplayProtectionTest().main() |