Changeset View
Standalone View
test/functional/abc-segwit-recovery-activation.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
#!/usr/bin/env python3 | |||||
# Copyright (c) 2015-2016 The Bitcoin Core developers | |||||
# Copyright (c) 2017-2019 The Bitcoin developers | |||||
# Distributed under the MIT software license, see the accompanying | |||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. | |||||
""" | |||||
This test checks activation of the SCRIPT_ALLOW_SEGWIT_RECOVERY flag | |||||
""" | |||||
from test_framework.test_framework import ComparisonTestFramework | |||||
from test_framework.util import assert_equal, assert_raises_rpc_error | |||||
from test_framework.comptool import TestManager, TestInstance, RejectResult | |||||
from test_framework.blocktools import * | |||||
from test_framework.script import * | |||||
import time | |||||
# far into the future | |||||
GREAT_WALL_START_TIME = 2000000000 | |||||
# Error due to non clean stack | |||||
CLEANSTACK_ERROR = b'non-mandatory-script-verify-flag (Script did not clean its stack)' | |||||
RPC_CLEANSTACK_ERROR = "64: " + \ | |||||
CLEANSTACK_ERROR.decode("utf-8") | |||||
class PreviousSpendableOutput(object): | |||||
def __init__(self, tx=CTransaction(), n=-1): | |||||
self.tx = tx | |||||
self.n = n # the output we're spending | |||||
class SegwitRecoveryActivationTest(ComparisonTestFramework): | |||||
def set_test_params(self): | |||||
self.num_nodes = 2 | |||||
self.setup_clean_chain = True | |||||
self.block_heights = {} | |||||
self.tip = None | |||||
self.blocks = {} | |||||
# Node0 accepts nonstdtxn. It's used to test the activation itself. | |||||
# Node1 doesn't accept nonstdtxn and doesn't have the mininode | |||||
# whitelisted. It's used to test for bans. | |||||
# Nodes aren't connected to each other. | |||||
self.extra_args = [['-whitelist=127.0.0.1', | |||||
"-greatwallactivationtime=%d" % GREAT_WALL_START_TIME, | |||||
"-acceptnonstdtxn", | |||||
"-replayprotectionactivationtime=%d" % (2 * GREAT_WALL_START_TIME)], | |||||
["-greatwallactivationtime=%d" % GREAT_WALL_START_TIME, | |||||
"-acceptnonstdtxn=0", | |||||
deadalnix: Isn't it the value by default ? | |||||
florianUnsubmitted Done Inline ActionsNot in regtest! To avoid any doubt, I decided to explicitly set/unset this arg in both nodes. florian: Not in regtest! To avoid any doubt, I decided to explicitly set/unset this arg in both nodes. | |||||
"-replayprotectionactivationtime=%d" % (2 * GREAT_WALL_START_TIME)]] | |||||
def run_test(self): | |||||
self.test = TestManager(self, self.options.tmpdir) | |||||
self.test.add_all_connections(self.nodes) | |||||
network_thread_start() | |||||
self.test.run() | |||||
def next_block(self, number): | |||||
if self.tip == None: | |||||
base_block_hash = self.genesis_hash | |||||
block_time = int(time.time()) + 1 | |||||
else: | |||||
base_block_hash = self.tip.sha256 | |||||
block_time = self.tip.nTime + 1 | |||||
# First create the coinbase | |||||
height = self.block_heights[base_block_hash] + 1 | |||||
coinbase = create_coinbase(height) | |||||
coinbase.rehash() | |||||
block = create_block(base_block_hash, coinbase, block_time) | |||||
# Do PoW, which is cheap on regnet | |||||
block.solve() | |||||
self.tip = block | |||||
self.block_heights[block.sha256] = height | |||||
assert number not in self.blocks | |||||
self.blocks[number] = block | |||||
return block | |||||
def get_tests(self): | |||||
self.genesis_hash = int(self.nodes[0].getbestblockhash(), 16) | |||||
self.block_heights[self.genesis_hash] = 0 | |||||
spendable_outputs = [] | |||||
# save the current tip so it can be spent by a later block | |||||
def save_spendable_output(): | |||||
spendable_outputs.append(self.tip) | |||||
# get an output that we previously marked as spendable | |||||
def get_spendable_output(): | |||||
return PreviousSpendableOutput(spendable_outputs.pop(0).vtx[0], 0) | |||||
# returns a test case that asserts that the current tip was accepted | |||||
def accepted(): | |||||
return TestInstance([[self.tip, True]]) | |||||
# returns a test case that asserts that the current tip was rejected | |||||
def rejected(reject=None): | |||||
if reject is None: | |||||
return TestInstance([[self.tip, False]]) | |||||
else: | |||||
return TestInstance([[self.tip, reject]]) | |||||
# move the tip back to a previous block | |||||
def tip(number): | |||||
self.tip = self.blocks[number] | |||||
# adds transactions to the block and updates state | |||||
def update_block(block_number, new_transactions): | |||||
[tx.rehash() for tx in new_transactions] | |||||
block = self.blocks[block_number] | |||||
block.vtx.extend(new_transactions) | |||||
old_sha256 = block.sha256 | |||||
make_conform_to_ctor(block) | |||||
block.hashMerkleRoot = block.calc_merkle_root() | |||||
block.solve() | |||||
# Update the internal state just like in next_block | |||||
self.tip = block | |||||
if block.sha256 != old_sha256: | |||||
self.block_heights[ | |||||
block.sha256] = self.block_heights[old_sha256] | |||||
del self.block_heights[old_sha256] | |||||
self.blocks[block_number] = block | |||||
return block | |||||
# shorthand for functions | |||||
block = self.next_block | |||||
node = self.nodes[0] | |||||
node1 = self.nodes[1] | |||||
deadalnixUnsubmitted Done Inline ActionsThese aren't the most descriptive names. There is an actual difference between these nodes. deadalnix: These aren't the most descriptive names. There is an actual difference between these nodes. | |||||
florianUnsubmitted Done Inline ActionsFixed. florian: Fixed. | |||||
# Create a new block | |||||
block(0) | |||||
save_spendable_output() | |||||
yield accepted() | |||||
# Now we need that block to mature so we can spend the coinbase. | |||||
test = TestInstance(sync_every_block=False) | |||||
for i in range(99): | |||||
block(5000 + i) | |||||
test.blocks_and_transactions.append([self.tip, True]) | |||||
save_spendable_output() | |||||
yield test | |||||
# collect spendable outputs now to avoid cluttering the code later on | |||||
out = [] | |||||
for i in range(100): | |||||
out.append(get_spendable_output()) | |||||
# Returns transactions that creates outputs in segwit addresses | |||||
# (txfund) and that recover them later (txspend) | |||||
def create_fund_and_spend_tx(spend): | |||||
deadalnixUnsubmitted Done Inline ActionsThe name doesn't match what the comment say it does. deadalnix: The name doesn't match what the comment say it does. | |||||
# To make sure we'll be able to recover coins sent to segwit addresses, | |||||
# we test using historical recoveries from btc.com: | |||||
# Spending from a P2SH-P2WPKH coin, | |||||
# txhash:a45698363249312f8d3d93676aa714be59b0bd758e62fa054fb1ea6218480691 | |||||
redeem_script0 = bytearray.fromhex( | |||||
'0014fcf9969ce1c98a135ed293719721fb69f0b686cb') | |||||
# Spending from a P2SH-P2WSH coin, | |||||
# txhash:6b536caf727ccd02c395a1d00b752098ec96e8ec46c96bee8582be6b5060fa2f | |||||
redeem_script1 = bytearray.fromhex( | |||||
'0020fc8b08ed636cb23afcb425ff260b3abd03380a2333b54cfa5d51ac52d803baf4') | |||||
redeem_scripts = [redeem_script0, redeem_script1] | |||||
# Fund transaction to segwit addresses | |||||
txfund = CTransaction() | |||||
txfund.vin = [CTxIn(COutPoint(spend.tx.sha256, spend.n))] | |||||
amount = (50 * COIN - 1000) // len(redeem_scripts) | |||||
for redeem_script in redeem_scripts: | |||||
txfund.vout.append( | |||||
CTxOut(amount, CScript([OP_HASH160, hash160(redeem_script), OP_EQUAL]))) | |||||
txfund.rehash() | |||||
# Spend transaction | |||||
txspend = CTransaction() | |||||
for i in range(len(redeem_scripts)): | |||||
txspend.vin.append( | |||||
CTxIn(COutPoint(txfund.sha256, i), CScript([redeem_scripts[i]]))) | |||||
# We want this output to be standard. The only non standard | |||||
# feature in this spending transaction is the scriptSig, which | |||||
# is using a segwit redeem script | |||||
txspend.vout.append( | |||||
CTxOut(txfund.vout[i].nValue - 200, CScript([OP_HASH160, hash160(CScript([OP_TRUE])), OP_EQUAL]))) | |||||
txspend.rehash() | |||||
return txfund, txspend | |||||
def send_transaction_to_mempool(tx): | |||||
tx_id = node.sendrawtransaction(ToHex(tx)) | |||||
assert(tx_id in set(node.getrawmempool())) | |||||
return tx_id | |||||
# We use node1 to test for bans, since it doesn't have us whitelisted. | |||||
# We can't have the mininode sending invalid blocks to node1, otherwise | |||||
# it WILL be disconnected. Due to how the network thread works, it's | |||||
# easier to keep both connections open at all times, but selectively | |||||
# choose which nodes the TestManager is communicating with. We do so by | |||||
# controlling the items in self.test.p2p_connections. | |||||
TestManager_p2p_connections = self.test.p2p_connections | |||||
# Selects which node the TestManager is communicating with. | |||||
# Use node_index=None to select both. | |||||
def TestManager_select_node(node_index=None): | |||||
if node_index is None: | |||||
self.test.p2p_connections = TestManager_p2p_connections | |||||
else: | |||||
self.test.p2p_connections = [TestManager_p2p_connections[ | |||||
node_index]] | |||||
deadalnixUnsubmitted Not Done Inline ActionsIt does sounds like this changes the TestManager's p2p_connections. Therefore TestManager_p2p_connections is a misnomer. Surely you are interested in keeping that value around for another reason than because it is Testmanager's p2p_connections. At this point in the patch, this is very unclear to me why, when picking a proper name could have provided that information instantly. Grepping in the test folder, I can't find any test that actually set that variable, ad generally setting the fields of another object that way raises flags. I'm pretty sure the other object's implementation really doesn't support this, and has no way to know one of its users is doing something sketchy, so this is bound to break in the future. deadalnix: It does sounds like this changes the TestManager's p2p_connections. Therefore… | |||||
florianUnsubmitted Done Inline ActionsYeah, good call. I found an example I could use. I refactored this ban testing code to use only public interfaces. florian: Yeah, good call. I found an example I could use. I refactored this ban testing code to use only… | |||||
# Check that we are not banned when sending a txn that will be rejected. | |||||
# On return, TestManager is configured to communicate with both nodes, | |||||
def check_for_no_ban_on_rejected_tx(tx, reject): | |||||
# Node1 is only connected to us | |||||
assert(len(node1.getpeerinfo()) == 1) | |||||
# Use the mininode to send the test txn to node1 | |||||
TestManager_select_node(1) | |||||
yield TestInstance([[tx, reject]]) | |||||
# TestManager has synced with node1 using a ping message. If we | |||||
# were to be banned, we should be disconnected by now. To be | |||||
# precise, TestManager would timeout and trigger an assert, | |||||
# effectively failing this functional test. | |||||
# We continue, anyway. Check that the connection is still open. | |||||
assert(len(node1.getpeerinfo()) == 1) | |||||
deadalnixUnsubmitted Not Done Inline ActionsYou probably want to check for some peer specifically rather than the peer count. The would ensure that the peer sending the tx is the one you expect and so that the peer that isn't banned is the one you expect. For all you know, this could send the tx via the RPC and not test anything. deadalnix: You probably want to check for some peer specifically rather than the peer count. The would… | |||||
# Back to talking with both nodes | |||||
TestManager_select_node() | |||||
# Create funding and segwit spending transactions | |||||
txfund, txspend = create_fund_and_spend_tx(out[0]) | |||||
# Move the mocktime | |||||
node.setmocktime(GREAT_WALL_START_TIME) | |||||
node1.setmocktime(GREAT_WALL_START_TIME) | |||||
deadalnixUnsubmitted Done Inline ActionsIs that required ? deadalnix: Is that required ? | |||||
florianUnsubmitted Done Inline ActionsYes, otherwise I get "time-too-new, block timestamp too far in the future (code 16)". And node1 (now node_std) needs to upgrade as well to test the banning behavior after the fork. florian: Yes, otherwise I get "time-too-new, block timestamp too far in the future (code 16)". And node1… | |||||
# Create blocks to get closer to activate the fork | |||||
# Mine txfund, as it can't go into node1 mempool because it's | |||||
# nonstandard. | |||||
b = block(5555) | |||||
b.nTime = GREAT_WALL_START_TIME - 1 | |||||
update_block(5555, [txfund]) | |||||
yield accepted() | |||||
for i in range(5): | |||||
block(5100 + i) | |||||
test.blocks_and_transactions.append([self.tip, True]) | |||||
yield test | |||||
# Check we are just before the activation time | |||||
assert_equal( | |||||
node.getblockheader(node.getbestblockhash())['mediantime'], | |||||
GREAT_WALL_START_TIME - 1) | |||||
assert_equal( | |||||
node1.getblockheader(node.getbestblockhash())['mediantime'], | |||||
GREAT_WALL_START_TIME - 1) | |||||
# Before the fork, segwit spending txns are rejected | |||||
assert_raises_rpc_error(-26, RPC_CLEANSTACK_ERROR, | |||||
node.sendrawtransaction, ToHex(txspend)) | |||||
# Select only node0, to avoid being banned by node1 when testing a | |||||
# rejected block | |||||
TestManager_select_node(0) | |||||
# Blocks containing segwit spending txns are rejected as well. | |||||
block(2) | |||||
c = update_block(2, [txspend]) | |||||
yield rejected(RejectResult(16, b'blk-bad-inputs')) | |||||
# Rewind bad block | |||||
tip(5104) | |||||
# Check that non-upgraded nodes that are checking for standardness are | |||||
# not banning nodes sending segwit spending txns. | |||||
yield from check_for_no_ban_on_rejected_tx( | |||||
txspend, RejectResult(64, CLEANSTACK_ERROR)) | |||||
# Activate the fork in both nodes! | |||||
block(5556) | |||||
yield accepted() | |||||
# Check we just activated the fork | |||||
assert_equal( | |||||
node.getblockheader(node.getbestblockhash())['mediantime'], | |||||
GREAT_WALL_START_TIME) | |||||
assert_equal( | |||||
node1.getblockheader(node.getbestblockhash())['mediantime'], | |||||
GREAT_WALL_START_TIME) | |||||
# Segwit spending txn are accepted in the mempool of nodes not checking | |||||
# standardness, but not in nodes that check | |||||
send_transaction_to_mempool(txspend) | |||||
assert_raises_rpc_error(-26, RPC_CLEANSTACK_ERROR, | |||||
node1.sendrawtransaction, ToHex(txspend)) | |||||
# Check that upgraded nodes checking for standardness are not banning | |||||
# nodes sending segwit spending txns | |||||
yield from check_for_no_ban_on_rejected_tx( | |||||
txspend, RejectResult(64, CLEANSTACK_ERROR)) | |||||
# Blocks containing segwit spending txns are now accepted in both nodes | |||||
block(5) | |||||
update_block(5, [txspend]) | |||||
yield accepted() | |||||
# Ok, now we check if a reorg work properly accross the activation. | |||||
postforkblockid = node.getbestblockhash() | |||||
node.invalidateblock(postforkblockid) | |||||
assert(txspend.hash in node.getrawmempool()) | |||||
# Also check that nodes checking standardness don't return a segwit | |||||
# spending txn into the mempool when disconnecting a block | |||||
node1.invalidateblock(postforkblockid) | |||||
assert(node.getbestblockhash() == node1.getbestblockhash()) | |||||
assert(txspend.hash not in node1.getrawmempool()) | |||||
# Deactivate the fork. | |||||
forkblockid = node.getbestblockhash() | |||||
node.invalidateblock(forkblockid) | |||||
# The spending tx has been evicted from the mempool | |||||
assert(len(node.getrawmempool()) == 0) | |||||
if __name__ == '__main__': | |||||
SegwitRecoveryActivationTest().main() |
Isn't it the value by default ?