Skip to content

Commit

Permalink
check mandatory soft forks relaying with previous releases
Browse files Browse the repository at this point in the history
  • Loading branch information
achow101 committed Aug 23, 2023
1 parent 1b09cc5 commit d7e6d45
Show file tree
Hide file tree
Showing 2 changed files with 301 additions and 1 deletion.
298 changes: 298 additions & 0 deletions test/functional/backcompat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
# Copyright (c) 2018-2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Backwards compatibility functional test
Test various backwards compatibility scenarios. Requires previous releases binaries,
see test/README.md.
Due to RPC changes introduced in various versions the below tests
won't work for older versions without some patches or workarounds.
Use only the latest patch version of each release, unless a test specifically
needs an older patch version.
"""

import os
import shutil
import pprint
import tempfile
import re

from test_framework.blocktools import create_block, create_coinbase
from test_framework.descriptors import descsum_create
from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework

from test_framework.address import (
script_to_p2sh,
)
from test_framework.script import (
CScript,
OP_1,
OP_CHECKSEQUENCEVERIFY,
OP_CHECKLOCKTIMEVERIFY,
)
from test_framework.messages import(
tx_from_hex,
)
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
find_vout_for_address,
rpc_port,
p2p_port,
)
from test_framework.test_node import TestNode


class BackwardsCompatibilityTest(BitcoinTestFramework):
def add_options(self, parser):
self.add_wallet_options(parser)

def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
# Add new version after each release:
self.wallet_names = [self.default_wallet_name]

def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
self.skip_if_no_previous_releases()

def get_bin_from_version(self, version, bin_name):
if version > 219999:
# Starting at client version 220000 the first two digits represent
# the major version, e.g. v22.0 instead of v0.22.0.
version *= 100
return os.path.join(
self.options.previous_releases_path,
re.sub(
r'\.0$' if version <= 219999 else r'(\.0){1,2}$',
'', # Remove trailing dot for point releases, after 22.0 also remove double trailing dot.
'v{}.{}.{}.{}'.format(
(version % 100000000) // 1000000,
(version % 1000000) // 10000,
(version % 10000) // 100,
(version % 100) // 1,
),
),
'bin',
bin_name,
)

def connect_nodes(self, from_connection, to_connection):
from_num_peers = 1 + len(from_connection.getpeerinfo())
to_num_peers = 1 + len(to_connection.getpeerinfo())
ip_port = "127.0.0.1:" + str(p2p_port(to_connection.index))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
# See comments in net_processing:
# * Must have a version message before anything else
# * Must have a verack message before anything else
self.wait_until(lambda: sum(peer['version'] != 0 for peer in from_connection.getpeerinfo()) == from_num_peers)
self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo()) == from_num_peers)
# The message bytes are counted before processing the message, so make
# sure it was fully processed by waiting for a ping.
self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 32 for peer in from_connection.getpeerinfo()) == from_num_peers)

def get_node_version(self, node):
if node.version_is_at_least(100000):
return node.getnetworkinfo()["version"]
else:
return node.getinfo()["version"]

def run_test(self):
VERSIONS = [
# Bad
90000,
90100,
90200,
90201,
90300,
100000,
100100,
100200,
100300,
110000,
110100,
110200,
120000,
120100,
130000,
130100,
130200,
140000,
140100,
140200,
140300,
150000,
150001,
150100,
150200,
160000,
160100,
160200,
160300,
170000,
170001,
170100,
180000,
180100,
190000,
190001,
190100,
190200,
200000,
200100,
200200,
210000,
210100,
210200,
220000,
230000,
230100,
230200,
240000,
250000,
]

# Activate soft forks for the versions that need it
self.log.info("Mining")
cb_spk = bytes.fromhex(self.nodes[0].validateaddress(self.nodes[0].getnewaddress())["scriptPubKey"])
for _ in range(600):
template = self.nodes[0].getblocktemplate({"rules": ["segwit"]})
coinbase = create_coinbase(height=template["height"], script_pubkey=cb_spk)
block = create_block(version=0x20000002, tmpl=template, coinbase=coinbase)
while True:
submit_res = self.nodes[0].submitblock(block.serialize().hex())
if submit_res != "high-hash":
break
block.nNonce += 1

p2sh_segwit_addr = self.nodes[0].getnewaddress("", "p2sh-segwit")
p2wpkh_addr = self.nodes[0].getnewaddress("", "bech32")
p2tr_addr = self.nodes[0].getnewaddress("", "bech32m")
der_addr = self.nodes[0].getnewaddress("", "legacy")

multi_desc = descsum_create("sh(multi(1,tprv8mGPkMVz5mZuJDnC2NjjAv7E9Zqa5LCgX4zawbZu5nzTtLb5kGhPwycX4H1gtW1f5ZdTKTNtQJ61hk71F2TdcQ93EFDTpUcPBr98QRji615))")
self.nodes[0].importdescriptors([{"desc": multi_desc,"timestamp":"now"}])
multi_addr = self.nodes[0].deriveaddresses(multi_desc)[0]

csv_script = CScript([20, OP_CHECKSEQUENCEVERIFY])
csv_addr = script_to_p2sh(csv_script)

cltv_script = CScript([610, OP_CHECKLOCKTIMEVERIFY])
cltv_addr = script_to_p2sh(cltv_script)

fund_tx = self.nodes[0].send(
[
{p2sh_segwit_addr: 5},
{p2wpkh_addr: 5},
{p2tr_addr: 5},
{multi_addr: 5},
{csv_addr: 5},
{cltv_addr: 5},
{der_addr: 5},
]
)

p2sh_segwit_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], p2sh_segwit_addr)}
p2wpkh_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], p2wpkh_addr)}
p2tr_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], p2tr_addr)}
multi_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], multi_addr)}
csv_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], csv_addr), "sequence": 20}
cltv_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], cltv_addr)}
der_utxo = {"txid": fund_tx["txid"], "vout": find_vout_for_address(self.nodes[0], fund_tx["txid"], der_addr)}

self.generate(self.nodes[0], 1)

bad_txs = []
for t, utxo in [("p2sh-segwit", p2sh_segwit_utxo), ("p2wpkh", p2wpkh_utxo), ("p2tr", p2tr_utxo)]:
child = self.nodes[0].sendall(recipients=[self.nodes[0].getnewaddress("", "legacy")], add_to_wallet=False, inputs=[utxo])
bad_tx = tx_from_hex(child["hex"])
mut_item = bytearray(bad_tx.wit.vtxinwit[0].scriptWitness.stack[0])
mut_item[10] += 1
bad_tx.wit.vtxinwit[0].scriptWitness.stack[0] = bytes(mut_item)

bad_txs.append((t, bad_tx.serialize_with_witness().hex()))
bad_txs.append((t + " witness stripped", bad_tx.serialize_without_witness().hex()))

multi_child = self.nodes[0].sendall(recipients=[self.nodes[0].getnewaddress("", "legacy")], add_to_wallet=False, inputs=[multi_utxo])
bad_multi_tx = tx_from_hex(multi_child["hex"])
mut_item = bytearray(bad_multi_tx.vin[0].scriptSig)
mut_item[0] = OP_1
bad_multi_tx.vin[0].scriptSig = bytes(mut_item)
bad_txs.append(("nulldummy", bad_multi_tx.serialize().hex()))

csv_child = tx_from_hex(self.nodes[0].createrawtransaction([csv_utxo], {self.nodes[0].getnewaddress("", "legacy"): 4.9999}))
csv_child.vin[0].scriptSig = CScript([csv_script])
bad_txs.append(("csv", csv_child.serialize().hex()))

cltv_child = tx_from_hex(self.nodes[0].createrawtransaction([cltv_utxo], {self.nodes[0].getnewaddress("", "legacy"): 4.9999}, 610))
cltv_child.vin[0].scriptSig = CScript([cltv_script])
bad_txs.append(("cltv", cltv_child.serialize().hex()))

der_child = tx_from_hex(self.nodes[0].sendall(recipients=[self.nodes[0].getnewaddress("", "legacy")], add_to_wallet=False, inputs=[der_utxo])["hex"])
der_mut = bytearray(der_child.vin[0].scriptSig)
der_mut[0] += 1
der_mut[2] += 1
der_mut[4] += 1
der_mut.insert(5, 0)
der_child.vin[0].scriptSig = bytes(der_mut)
bad_txs.append(("der", der_child.serialize().hex()))

for t, tx in bad_txs:
assert_raises_rpc_error(-26, "", self.nodes[0].sendrawtransaction, tx)

for ver in VERSIONS:
self.log.info(f"Starting {ver}")
datadir=os.path.join(self.options.tmpdir, f"node_{ver}")
os.mkdir(datadir)
os.mkdir(os.path.join(datadir, "stderr"))
os.mkdir(os.path.join(datadir, "stdout"))
extra_conf = []
if ver >= 130000 and ver < 160000:
extra_conf = ["vbparams=csv:0:9999999999", "vbparams=segwit:0:9999999999", "bip9params=csv:0:9999999999", "bip9params=segwit:0:9999999999"]
node = TestNode(
i=1,
datadir=datadir,
chain=self.chain,
rpchost=None,
timewait=self.rpc_timeout,
timeout_factor=self.options.timeout_factor,
bitcoind=self.get_bin_from_version(ver, "bitcoind"),
bitcoin_cli=self.get_bin_from_version(ver, "bitcoin-cli"),
version=ver,
coverage_dir=self.options.coveragedir,
cwd=self.options.tmpdir,
extra_conf=["rpcuser=user", "rpcpassword=pass"] + extra_conf,
extra_args=["-regtest", f"-rpcport={rpc_port(1)}", f"-port={p2p_port(1)}"],
use_cli=self.options.usecli,
start_perf=self.options.perf,
use_valgrind=self.options.valgrind,
descriptors=self.options.descriptors,
)
node.start()
self.log.info(f" Waiting for {ver} rpc")
node.wait_for_rpc_connection()
assert_equal(self.get_node_version(node), ver)
self.log.info(f" Waiting for {ver} connection")
self.connect_nodes(self.nodes[0], node)
self.sync_blocks([self.nodes[0], node])
self.log.info(f" Testing {ver}")
for t, tx in bad_txs:
try:
node.sendrawtransaction(tx)
#pprint.pprint(self.nodes[0].decoderawtransaction(tx))
print(f"FAILED: {t} was accepted")
except JSONRPCException as e:
print(f"Success: {t} was rejected with '{e}'")

self.log.info(f" Stopping {ver}")
node.stop_node(wait_until_stopped=False)

if __name__ == '__main__':
BackwardsCompatibilityTest().main()
4 changes: 3 additions & 1 deletion test/functional/test_framework/authproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,10 @@ def get_request(self, *args, **argsn):
))
if args and argsn:
params = dict(args=args, **argsn)
else:
elif args or argsn:
params = args or argsn
else:
params = []
return {'version': '1.1',
'method': self._service_name,
'params': params,
Expand Down

0 comments on commit d7e6d45

Please sign in to comment.