aboutsummaryrefslogtreecommitdiff
path: root/test/functional
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional')
-rw-r--r--test/functional/data/rpc_psbt.json4
-rwxr-xr-xtest/functional/feature_addrman.py13
-rwxr-xr-xtest/functional/feature_asmap.py9
-rwxr-xr-xtest/functional/feature_assumeutxo.py162
-rwxr-xr-xtest/functional/feature_bip68_sequence.py18
-rwxr-xr-xtest/functional/feature_block.py3
-rwxr-xr-xtest/functional/feature_coinstatsindex.py3
-rwxr-xr-xtest/functional/feature_csv_activation.py12
-rwxr-xr-xtest/functional/feature_framework_miniwallet.py49
-rwxr-xr-xtest/functional/feature_framework_unit_tests.py1
-rwxr-xr-xtest/functional/feature_pruning.py18
-rwxr-xr-xtest/functional/feature_reindex.py20
-rwxr-xr-xtest/functional/feature_settings.py2
-rwxr-xr-xtest/functional/feature_taproot.py16
-rwxr-xr-xtest/functional/interface_bitcoin_cli.py49
-rwxr-xr-xtest/functional/interface_rpc.py1
-rwxr-xr-xtest/functional/mempool_accept.py5
-rwxr-xr-xtest/functional/mempool_limit.py26
-rwxr-xr-xtest/functional/mempool_package_onemore.py21
-rwxr-xr-xtest/functional/mempool_package_rbf.py587
-rwxr-xr-xtest/functional/mempool_truc.py (renamed from test/functional/mempool_accept_v3.py)201
-rwxr-xr-xtest/functional/p2p_addr_relay.py3
-rwxr-xr-xtest/functional/p2p_addrv2_relay.py12
-rwxr-xr-xtest/functional/p2p_disconnect_ban.py12
-rwxr-xr-xtest/functional/p2p_handshake.py4
-rwxr-xr-xtest/functional/p2p_invalid_messages.py10
-rwxr-xr-xtest/functional/p2p_mutated_blocks.py11
-rwxr-xr-xtest/functional/p2p_segwit.py13
-rwxr-xr-xtest/functional/p2p_sendheaders.py48
-rwxr-xr-xtest/functional/p2p_unrequested_blocks.py4
-rwxr-xr-xtest/functional/p2p_v2_earlykeyresponse.py89
-rwxr-xr-xtest/functional/p2p_v2_misbehaving.py176
-rwxr-xr-xtest/functional/rpc_createmultisig.py254
-rwxr-xr-xtest/functional/rpc_dumptxoutset.py2
-rwxr-xr-xtest/functional/rpc_generate.py2
-rwxr-xr-xtest/functional/rpc_net.py27
-rwxr-xr-xtest/functional/rpc_packages.py2
-rwxr-xr-xtest/functional/rpc_psbt.py6
-rwxr-xr-xtest/functional/rpc_rawtransaction.py23
-rwxr-xr-xtest/functional/rpc_signrawtransactionwithkey.py10
-rwxr-xr-xtest/functional/rpc_users.py39
-rw-r--r--test/functional/test_framework/authproxy.py35
-rw-r--r--test/functional/test_framework/key.py6
-rwxr-xr-xtest/functional/test_framework/messages.py16
-rwxr-xr-xtest/functional/test_framework/p2p.py4
-rw-r--r--test/functional/test_framework/script.py33
-rwxr-xr-xtest/functional/test_framework/script_util.py25
-rwxr-xr-xtest/functional/test_framework/test_framework.py43
-rwxr-xr-xtest/functional/test_framework/test_node.py11
-rw-r--r--test/functional/test_framework/util.py7
-rw-r--r--test/functional/test_framework/v2_p2p.py12
-rw-r--r--test/functional/test_framework/wallet.py24
-rwxr-xr-xtest/functional/test_runner.py37
-rwxr-xr-xtest/functional/wallet_balance.py5
-rwxr-xr-xtest/functional/wallet_bumpfee.py25
-rwxr-xr-xtest/functional/wallet_conflicts.py2
-rwxr-xr-xtest/functional/wallet_create_tx.py21
-rwxr-xr-xtest/functional/wallet_fundrawtransaction.py33
-rwxr-xr-xtest/functional/wallet_listsinceblock.py26
-rwxr-xr-xtest/functional/wallet_multisig_descriptor_psbt.py25
-rwxr-xr-xtest/functional/wallet_send.py34
-rwxr-xr-xtest/functional/wallet_sendall.py67
62 files changed, 1892 insertions, 566 deletions
diff --git a/test/functional/data/rpc_psbt.json b/test/functional/data/rpc_psbt.json
index 3127350872..1ccc5e0ba0 100644
--- a/test/functional/data/rpc_psbt.json
+++ b/test/functional/data/rpc_psbt.json
@@ -38,7 +38,9 @@
"cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwlCiXVqo3OczGiewPzzo2C+MswLWbFuk6Hou0YFcmssp6P/cGxBdmSWMrLMaOH5ErileONxnOdxCIXHqWb0m81DywEBAAA=",
"cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwk5iXVqo3OczGiewPzzo2C+MswLWbFuk6Hou0YFcmssp6P/cGxBdmSWMrLMaOH5ErileONxnOdxCIXHqWb0m81DywAA",
"cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJjFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgAIyAssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20qzAAAA=",
- "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJhFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4SMgLLE6xoJI3oBqpqNlnPPAPraCHQnIEUpOho/r3oZbttKswAAA"
+ "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJhFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4SMgLLE6xoJI3oBqpqNlnPPAPraCHQnIEUpOho/r3oZbttKswAAA",
+ "cHNidP8BAHUCAAAAAQCBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA",
+ "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAgD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA"
],
"invalid_with_msg": [
[
diff --git a/test/functional/feature_addrman.py b/test/functional/feature_addrman.py
index 95d33d62ea..2efad70900 100755
--- a/test/functional/feature_addrman.py
+++ b/test/functional/feature_addrman.py
@@ -6,7 +6,6 @@
import os
import re
-import struct
from test_framework.messages import ser_uint256, hash256, MAGIC_BYTES
from test_framework.netutil import ADDRMAN_NEW_BUCKET_COUNT, ADDRMAN_TRIED_BUCKET_COUNT, ADDRMAN_BUCKET_SIZE
@@ -28,15 +27,15 @@ def serialize_addrman(
tried = []
INCOMPATIBILITY_BASE = 32
r = MAGIC_BYTES[net_magic]
- r += struct.pack("B", format)
- r += struct.pack("B", INCOMPATIBILITY_BASE + lowest_compatible)
+ r += format.to_bytes(1, "little")
+ r += (INCOMPATIBILITY_BASE + lowest_compatible).to_bytes(1, "little")
r += ser_uint256(bucket_key)
- r += struct.pack("<i", len_new or len(new))
- r += struct.pack("<i", len_tried or len(tried))
+ r += (len_new or len(new)).to_bytes(4, "little", signed=True)
+ r += (len_tried or len(tried)).to_bytes(4, "little", signed=True)
ADDRMAN_NEW_BUCKET_COUNT = 1 << 10
- r += struct.pack("<i", ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30))
+ r += (ADDRMAN_NEW_BUCKET_COUNT ^ (1 << 30)).to_bytes(4, "little", signed=True)
for _ in range(ADDRMAN_NEW_BUCKET_COUNT):
- r += struct.pack("<i", 0)
+ r += (0).to_bytes(4, "little", signed=True)
checksum = hash256(r)
r += mock_checksum or checksum
return r
diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py
index 024a8fa18c..e469deef49 100755
--- a/test/functional/feature_asmap.py
+++ b/test/functional/feature_asmap.py
@@ -27,6 +27,7 @@ import os
import shutil
from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
DEFAULT_ASMAP_FILENAME = 'ip_asn.map' # defined in src/init.cpp
ASMAP = '../../src/test/data/asmap.raw' # path to unit test skeleton asmap
@@ -118,6 +119,14 @@ class AsmapTest(BitcoinTestFramework):
msg = "ASMap Health Check: 4 clearnet peers are mapped to 3 ASNs with 0 peers being unmapped"
with self.node.assert_debug_log(expected_msgs=[msg]):
self.start_node(0, extra_args=['-asmap'])
+ raw_addrman = self.node.getrawaddrman()
+ asns = []
+ for _, entries in raw_addrman.items():
+ for _, entry in entries.items():
+ asn = entry['mapped_as']
+ if asn not in asns:
+ asns.append(asn)
+ assert_equal(len(asns), 3)
os.remove(self.default_asmap)
def run_test(self):
diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py
index 0d6c92c9fa..688e2866b2 100755
--- a/test/functional/feature_assumeutxo.py
+++ b/test/functional/feature_assumeutxo.py
@@ -21,7 +21,6 @@ Interesting test cases could be loading an assumeutxo snapshot file with:
Interesting starting states could be loading a snapshot when the current chain tip is:
- TODO: An ancestor of snapshot block
-- TODO: Not an ancestor of the snapshot block but has less work
- TODO: The snapshot block
- TODO: A descendant of the snapshot block
- TODO: Not an ancestor or a descendant of the snapshot block and has more work
@@ -33,6 +32,7 @@ from dataclasses import dataclass
from test_framework.messages import tx_from_hex
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
+ assert_approx,
assert_equal,
assert_raises_rpc_error,
)
@@ -51,18 +51,19 @@ class AssumeutxoTest(BitcoinTestFramework):
def set_test_params(self):
"""Use the pregenerated, deterministic chain up to height 199."""
- self.num_nodes = 3
+ self.num_nodes = 4
self.rpc_timeout = 120
self.extra_args = [
[],
["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"],
["-persistmempool=0","-txindex=1", "-blockfilterindex=1", "-coinstatsindex=1"],
+ []
]
def setup_network(self):
"""Start with the nodes disconnected so that one can generate a snapshot
including blocks the other hasn't yet seen."""
- self.add_nodes(3)
+ self.add_nodes(4)
self.start_nodes(extra_args=self.extra_args)
def test_invalid_snapshot_scenarios(self, valid_snapshot_path):
@@ -70,55 +71,91 @@ class AssumeutxoTest(BitcoinTestFramework):
with open(valid_snapshot_path, 'rb') as f:
valid_snapshot_contents = f.read()
bad_snapshot_path = valid_snapshot_path + '.mod'
+ node = self.nodes[1]
def expected_error(log_msg="", rpc_details=""):
- with self.nodes[1].assert_debug_log([log_msg]):
- assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path)
+ with node.assert_debug_log([log_msg]):
+ assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", node.loadtxoutset, bad_snapshot_path)
+
+ self.log.info(" - snapshot file with invalid file magic")
+ parsing_error_code = -22
+ bad_magic = 0xf00f00f000
+ with open(bad_snapshot_path, 'wb') as f:
+ f.write(bad_magic.to_bytes(5, "big") + valid_snapshot_contents[5:])
+ assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: Invalid UTXO set snapshot magic bytes. Please check if this is indeed a snapshot file or if you are using an outdated snapshot format.", node.loadtxoutset, bad_snapshot_path)
+
+ self.log.info(" - snapshot file with unsupported version")
+ for version in [0, 2]:
+ with open(bad_snapshot_path, 'wb') as f:
+ f.write(valid_snapshot_contents[:5] + version.to_bytes(2, "little") + valid_snapshot_contents[7:])
+ assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: Version of snapshot {version} does not match any of the supported versions.", node.loadtxoutset, bad_snapshot_path)
+
+ self.log.info(" - snapshot file with mismatching network magic")
+ invalid_magics = [
+ # magic, name, real
+ [0xf9beb4d9, "main", True],
+ [0x0b110907, "test", True],
+ [0x0a03cf40, "signet", True],
+ [0x00000000, "", False],
+ [0xffffffff, "", False],
+ ]
+ for [magic, name, real] in invalid_magics:
+ with open(bad_snapshot_path, 'wb') as f:
+ f.write(valid_snapshot_contents[:7] + magic.to_bytes(4, 'big') + valid_snapshot_contents[11:])
+ if real:
+ assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: The network of the snapshot ({name}) does not match the network of this node (regtest).", node.loadtxoutset, bad_snapshot_path)
+ else:
+ assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: This snapshot has been created for an unrecognized network. This could be a custom signet, a new testnet or possibly caused by data corruption.", node.loadtxoutset, bad_snapshot_path)
self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters")
prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
bogus_block_hash = "0" * 64 # Represents any unknown block hash
+ # The height is not used for anything critical currently, so we just
+ # confirm the manipulation in the error message
+ bogus_height = 1337
for bad_block_hash in [bogus_block_hash, prev_block_hash]:
with open(bad_snapshot_path, 'wb') as f:
- # block hash of the snapshot base is stored right at the start (first 32 bytes)
- f.write(bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[32:])
- error_details = f", assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})"
- expected_error(rpc_details=error_details)
+ f.write(valid_snapshot_contents[:11] + bogus_height.to_bytes(4, "little") + bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[47:])
+
+ msg = f"Unable to load UTXO snapshot: assumeutxo block hash in snapshot metadata not recognized (hash: {bad_block_hash}, height: {bogus_height}). The following snapshot heights are available: 110, 200, 299."
+ assert_raises_rpc_error(-32603, msg, node.loadtxoutset, bad_snapshot_path)
self.log.info(" - snapshot file with wrong number of coins")
- valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little")
+ valid_num_coins = int.from_bytes(valid_snapshot_contents[47:47 + 8], "little")
for off in [-1, +1]:
with open(bad_snapshot_path, 'wb') as f:
- f.write(valid_snapshot_contents[:32])
+ f.write(valid_snapshot_contents[:47])
f.write((valid_num_coins + off).to_bytes(8, "little"))
- f.write(valid_snapshot_contents[32 + 8:])
+ f.write(valid_snapshot_contents[47 + 8:])
expected_error(log_msg=f"bad snapshot - coins left over after deserializing 298 coins" if off == -1 else f"bad snapshot format or truncated snapshot after deserializing 299 coins")
- self.log.info(" - snapshot file with alternated UTXO data")
+ self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash")
cases = [
# (content, offset, wrong_hash, custom_message)
[b"\xff" * 32, 0, "7d52155c9a9fdc4525b637ef6170568e5dad6fabd0b1fdbb9432010b8453095b", None], # wrong outpoint hash
- [(1).to_bytes(4, "little"), 32, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
- [b"\x81", 36, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
- [b"\x80", 36, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
- [b"\x84\x58", 36, None, "[snapshot] bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
- [b"\xCA\xD2\x8F\x5A", 41, None, "[snapshot] bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
+ [(2).to_bytes(1, "little"), 32, None, "[snapshot] bad snapshot data after deserializing 1 coins"], # wrong txid coins count
+ [b"\xfd\xff\xff", 32, None, "[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left
+ [b"\x01", 33, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
+ [b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
+ [b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
+ [b"\x84\x58", 34, None, "[snapshot] bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
+ [b"\xCA\xD2\x8F\x5A", 39, None, "[snapshot] bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
]
for content, offset, wrong_hash, custom_message in cases:
with open(bad_snapshot_path, "wb") as f:
- f.write(valid_snapshot_contents[:(32 + 8 + offset)])
+ # Prior to offset: Snapshot magic, snapshot version, network magic, height, hash, coins count
+ f.write(valid_snapshot_contents[:(5 + 2 + 4 + 4 + 32 + 8 + offset)])
f.write(content)
- f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):])
+ f.write(valid_snapshot_contents[(5 + 2 + 4 + 4 + 32 + 8 + offset + len(content)):])
log_msg = custom_message if custom_message is not None else f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}"
expected_error(log_msg=log_msg)
def test_headers_not_synced(self, valid_snapshot_path):
for node in self.nodes[1:]:
- assert_raises_rpc_error(-32603, "The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) must appear in the headers chain. Make sure all headers are syncing, and call this RPC again.",
- node.loadtxoutset,
- valid_snapshot_path)
+ msg = "Unable to load UTXO snapshot: The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) must appear in the headers chain. Make sure all headers are syncing, and call loadtxoutset again."
+ assert_raises_rpc_error(-32603, msg, node.loadtxoutset, valid_snapshot_path)
def test_invalid_chainstate_scenarios(self):
self.log.info("Test different scenarios of invalid snapshot chainstate in datadir")
@@ -150,8 +187,8 @@ class AssumeutxoTest(BitcoinTestFramework):
assert tx['txid'] in node.getrawmempool()
# Attempt to load the snapshot on Node 2 and expect it to fail
- with node.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot when mempool not empty"]):
- assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path)
+ msg = "Unable to load UTXO snapshot: Can't activate a snapshot when mempool not empty"
+ assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
self.restart_node(2, extra_args=self.extra_args[2])
@@ -161,6 +198,49 @@ class AssumeutxoTest(BitcoinTestFramework):
path = node.datadir_path / node.chain / "invalid" / "path"
assert_raises_rpc_error(-8, "Couldn't open file {} for reading.".format(path), node.loadtxoutset, path)
+ def test_snapshot_with_less_work(self, dump_output_path):
+ self.log.info("Test bitcoind should fail when snapshot has less accumulated work than this node.")
+ node = self.nodes[0]
+ assert_equal(node.getblockcount(), FINAL_HEIGHT)
+ with node.assert_debug_log(expected_msgs=["[snapshot] activation failed - work does not exceed active chainstate"]):
+ assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path)
+
+ def test_snapshot_block_invalidated(self, dump_output_path):
+ self.log.info("Test snapshot is not loaded when base block is invalid.")
+ node = self.nodes[0]
+ # We are testing the case where the base block is invalidated itself
+ # and also the case where one of its parents is invalidated.
+ for height in [SNAPSHOT_BASE_HEIGHT, SNAPSHOT_BASE_HEIGHT - 1]:
+ block_hash = node.getblockhash(height)
+ node.invalidateblock(block_hash)
+ assert_equal(node.getblockcount(), height - 1)
+ msg = "Unable to load UTXO snapshot: The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) is part of an invalid chain."
+ assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
+ node.reconsiderblock(block_hash)
+
+ def test_snapshot_in_a_divergent_chain(self, dump_output_path):
+ n0 = self.nodes[0]
+ n3 = self.nodes[3]
+ assert_equal(n0.getblockcount(), FINAL_HEIGHT)
+ assert_equal(n3.getblockcount(), START_HEIGHT)
+
+ self.log.info("Check importing a snapshot where current chain-tip is not an ancestor of the snapshot block but has less work")
+ # Generate a divergent chain in n3 up to 298
+ self.generate(n3, nblocks=99, sync_fun=self.no_op)
+ assert_equal(n3.getblockcount(), SNAPSHOT_BASE_HEIGHT - 1)
+
+ # Try importing the snapshot and assert its success
+ loaded = n3.loadtxoutset(dump_output_path)
+ assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
+ normal, snapshot = n3.getchainstates()["chainstates"]
+ assert_equal(normal['blocks'], START_HEIGHT + 99)
+ assert_equal(snapshot['blocks'], SNAPSHOT_BASE_HEIGHT)
+
+ # Now lets sync the nodes and wait for the background validation to finish
+ self.connect_nodes(0, 3)
+ self.sync_blocks(nodes=(n0, n3))
+ self.wait_until(lambda: len(n3.getchainstates()['chainstates']) == 1)
+
def run_test(self):
"""
Bring up two (disconnected) nodes, mine some new blocks on the first,
@@ -172,6 +252,7 @@ class AssumeutxoTest(BitcoinTestFramework):
n0 = self.nodes[0]
n1 = self.nodes[1]
n2 = self.nodes[2]
+ n3 = self.nodes[3]
self.mini_wallet = MiniWallet(n0)
@@ -222,6 +303,7 @@ class AssumeutxoTest(BitcoinTestFramework):
# block.
n1.submitheader(block)
n2.submitheader(block)
+ n3.submitheader(block)
# Ensure everyone is seeing the same headers.
for n in self.nodes:
@@ -242,10 +324,12 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)
+ self.test_snapshot_with_less_work(dump_output['path'])
self.test_invalid_mempool_state(dump_output['path'])
self.test_invalid_snapshot_scenarios(dump_output['path'])
self.test_invalid_chainstate_scenarios()
self.test_invalid_file_path()
+ self.test_snapshot_block_invalidated(dump_output['path'])
self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
loaded = n1.loadtxoutset(dump_output['path'])
@@ -257,21 +341,35 @@ class AssumeutxoTest(BitcoinTestFramework):
the snapshot, and final values after the snapshot is validated."""
for height, block in blocks.items():
tx = n1.getblockheader(block.hash)["nTx"]
- chain_tx = n1.getchaintxstats(nblocks=1, blockhash=block.hash)["txcount"]
+ stats = n1.getchaintxstats(nblocks=1, blockhash=block.hash)
+ chain_tx = stats.get("txcount", None)
+ window_tx_count = stats.get("window_tx_count", None)
+ tx_rate = stats.get("txrate", None)
+ window_interval = stats.get("window_interval")
# Intermediate nTx of the starting block should be set, but nTx of
# later blocks should be 0 before they are downloaded.
+ # The window_tx_count of one block is equal to the blocks tx count.
+ # If the window tx count is unknown, the value is missing.
+ # The tx_rate is calculated from window_tx_count and window_interval
+ # when possible.
if final or height == START_HEIGHT:
assert_equal(tx, block.tx)
+ assert_equal(window_tx_count, tx)
+ if window_interval > 0:
+ assert_approx(tx_rate, window_tx_count / window_interval, vspan=0.1)
+ else:
+ assert_equal(tx_rate, None)
else:
assert_equal(tx, 0)
+ assert_equal(window_tx_count, None)
# Intermediate nChainTx of the starting block and snapshot block
- # should be set, but others should be 0 until they are downloaded.
+ # should be set, but others should be None until they are downloaded.
if final or height in (START_HEIGHT, SNAPSHOT_BASE_HEIGHT):
assert_equal(chain_tx, block.chain_tx)
else:
- assert_equal(chain_tx, 0)
+ assert_equal(chain_tx, None)
check_tx_counts(final=False)
@@ -406,12 +504,12 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(snapshot['validated'], False)
self.log.info("Check that loading the snapshot again will fail because there is already an active snapshot.")
- with n2.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot-based chainstate more than once"]):
- assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", n2.loadtxoutset, dump_output['path'])
+ msg = "Unable to load UTXO snapshot: Can't activate a snapshot-based chainstate more than once"
+ assert_raises_rpc_error(-32603, msg, n2.loadtxoutset, dump_output['path'])
self.connect_nodes(0, 2)
self.wait_until(lambda: n2.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT)
- self.sync_blocks()
+ self.sync_blocks(nodes=(n0, n2))
self.log.info("Ensuring background validation completes")
self.wait_until(lambda: len(n2.getchainstates()['chainstates']) == 1)
@@ -448,6 +546,8 @@ class AssumeutxoTest(BitcoinTestFramework):
self.connect_nodes(0, 2)
self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT)
+ self.test_snapshot_in_a_divergent_chain(dump_output['path'])
+
@dataclass
class Block:
hash: str
diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py
index 8768d4040d..14b92d6733 100755
--- a/test/functional/feature_bip68_sequence.py
+++ b/test/functional/feature_bip68_sequence.py
@@ -78,8 +78,8 @@ class BIP68Test(BitcoinTestFramework):
self.log.info("Activating BIP68 (and 112/113)")
self.activateCSV()
- self.log.info("Verifying nVersion=2 transactions are standard.")
- self.log.info("Note that nVersion=2 transactions are always standard (independent of BIP68 activation status).")
+ self.log.info("Verifying version=2 transactions are standard.")
+ self.log.info("Note that version=2 transactions are always standard (independent of BIP68 activation status).")
self.test_version2_relay()
self.log.info("Passed")
@@ -107,7 +107,7 @@ class BIP68Test(BitcoinTestFramework):
# This transaction will enable sequence-locks, so this transaction should
# fail
tx2 = CTransaction()
- tx2.nVersion = 2
+ tx2.version = 2
sequence_value = sequence_value & 0x7fffffff
tx2.vin = [CTxIn(COutPoint(tx1_id, 0), nSequence=sequence_value)]
tx2.wit.vtxinwit = [CTxInWitness()]
@@ -119,7 +119,7 @@ class BIP68Test(BitcoinTestFramework):
# Setting the version back down to 1 should disable the sequence lock,
# so this should be accepted.
- tx2.nVersion = 1
+ tx2.version = 1
self.wallet.sendrawtransaction(from_node=self.nodes[0], tx_hex=tx2.serialize().hex())
@@ -159,7 +159,7 @@ class BIP68Test(BitcoinTestFramework):
using_sequence_locks = False
tx = CTransaction()
- tx.nVersion = 2
+ tx.version = 2
value = 0
for j in range(num_inputs):
sequence_value = 0xfffffffe # this disables sequence locks
@@ -228,7 +228,7 @@ class BIP68Test(BitcoinTestFramework):
# Anyone-can-spend mempool tx.
# Sequence lock of 0 should pass.
tx2 = CTransaction()
- tx2.nVersion = 2
+ tx2.version = 2
tx2.vin = [CTxIn(COutPoint(tx1.sha256, 0), nSequence=0)]
tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee * COIN), SCRIPT_W0_SH_OP_TRUE)]
self.wallet.sign_tx(tx=tx2)
@@ -246,7 +246,7 @@ class BIP68Test(BitcoinTestFramework):
sequence_value |= SEQUENCE_LOCKTIME_TYPE_FLAG
tx = CTransaction()
- tx.nVersion = 2
+ tx.version = 2
tx.vin = [CTxIn(COutPoint(orig_tx.sha256, 0), nSequence=sequence_value)]
tx.wit.vtxinwit = [CTxInWitness()]
tx.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
@@ -360,7 +360,7 @@ class BIP68Test(BitcoinTestFramework):
# Make an anyone-can-spend transaction
tx2 = CTransaction()
- tx2.nVersion = 1
+ tx2.version = 1
tx2.vin = [CTxIn(COutPoint(tx1.sha256, 0), nSequence=0)]
tx2.vout = [CTxOut(int(tx1.vout[0].nValue - self.relayfee * COIN), SCRIPT_W0_SH_OP_TRUE)]
@@ -376,7 +376,7 @@ class BIP68Test(BitcoinTestFramework):
sequence_value = 100 # 100 block relative locktime
tx3 = CTransaction()
- tx3.nVersion = 2
+ tx3.version = 2
tx3.vin = [CTxIn(COutPoint(tx2.sha256, 0), nSequence=sequence_value)]
tx3.wit.vtxinwit = [CTxInWitness()]
tx3.wit.vtxinwit[0].scriptWitness.stack = [CScript([OP_TRUE])]
diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py
index 8a95975184..932f37a083 100755
--- a/test/functional/feature_block.py
+++ b/test/functional/feature_block.py
@@ -4,7 +4,6 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test block processing."""
import copy
-import struct
import time
from test_framework.blocktools import (
@@ -67,7 +66,7 @@ class CBrokenBlock(CBlock):
def serialize(self, with_witness=False):
r = b""
r += super(CBlock, self).serialize()
- r += struct.pack("<BQ", 255, len(self.vtx))
+ r += (255).to_bytes(1, "little") + len(self.vtx).to_bytes(8, "little")
for tx in self.vtx:
if with_witness:
r += tx.serialize_with_witness()
diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py
index d6c1567e64..691163d053 100755
--- a/test/functional/feature_coinstatsindex.py
+++ b/test/functional/feature_coinstatsindex.py
@@ -242,6 +242,9 @@ class CoinStatsIndexTest(BitcoinTestFramework):
res12 = index_node.gettxoutsetinfo('muhash')
assert_equal(res12, res10)
+ self.log.info("Test obtaining info for a non-existent block hash")
+ assert_raises_rpc_error(-5, "Block not found", index_node.gettxoutsetinfo, hash_type="none", hash_or_height="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", use_index=True)
+
def _test_use_index_option(self):
self.log.info("Test use_index option for nodes running the index")
diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py
index bc1f9e8f2f..2db9682931 100755
--- a/test/functional/feature_csv_activation.py
+++ b/test/functional/feature_csv_activation.py
@@ -110,7 +110,7 @@ class BIP68_112_113Test(BitcoinTestFramework):
def create_bip112special(self, input, txversion):
tx = self.create_self_transfer_from_utxo(input)
- tx.nVersion = txversion
+ tx.version = txversion
self.miniwallet.sign_tx(tx)
tx.vin[0].scriptSig = CScript([-1, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig)))
tx.rehash()
@@ -118,7 +118,7 @@ class BIP68_112_113Test(BitcoinTestFramework):
def create_bip112emptystack(self, input, txversion):
tx = self.create_self_transfer_from_utxo(input)
- tx.nVersion = txversion
+ tx.version = txversion
self.miniwallet.sign_tx(tx)
tx.vin[0].scriptSig = CScript([OP_CHECKSEQUENCEVERIFY] + list(CScript(tx.vin[0].scriptSig)))
tx.rehash()
@@ -136,7 +136,7 @@ class BIP68_112_113Test(BitcoinTestFramework):
for i, (sdf, srhb, stf, srlb) in enumerate(product(*[[True, False]] * 4)):
locktime = relative_locktime(sdf, srhb, stf, srlb)
tx = self.create_self_transfer_from_utxo(bip68inputs[i])
- tx.nVersion = txversion
+ tx.version = txversion
tx.vin[0].nSequence = locktime + locktime_delta
self.miniwallet.sign_tx(tx)
txs.append({'tx': tx, 'sdf': sdf, 'stf': stf})
@@ -154,7 +154,7 @@ class BIP68_112_113Test(BitcoinTestFramework):
tx.vin[0].nSequence = BASE_RELATIVE_LOCKTIME + locktime_delta
else: # vary nSequence instead, OP_CSV is fixed
tx.vin[0].nSequence = locktime + locktime_delta
- tx.nVersion = txversion
+ tx.version = txversion
self.miniwallet.sign_tx(tx)
if varyOP_CSV:
tx.vin[0].scriptSig = CScript([locktime, OP_CHECKSEQUENCEVERIFY, OP_DROP] + list(CScript(tx.vin[0].scriptSig)))
@@ -257,10 +257,10 @@ class BIP68_112_113Test(BitcoinTestFramework):
# BIP113 test transaction will be modified before each use to put in appropriate block time
bip113tx_v1 = self.create_self_transfer_from_utxo(bip113input)
bip113tx_v1.vin[0].nSequence = 0xFFFFFFFE
- bip113tx_v1.nVersion = 1
+ bip113tx_v1.version = 1
bip113tx_v2 = self.create_self_transfer_from_utxo(bip113input)
bip113tx_v2.vin[0].nSequence = 0xFFFFFFFE
- bip113tx_v2.nVersion = 2
+ bip113tx_v2.version = 2
# For BIP68 test all 16 relative sequence locktimes
bip68txs_v1 = self.create_bip68txs(bip68inputs, 1)
diff --git a/test/functional/feature_framework_miniwallet.py b/test/functional/feature_framework_miniwallet.py
new file mode 100755
index 0000000000..f108289018
--- /dev/null
+++ b/test/functional/feature_framework_miniwallet.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# Copyright (c) 2024 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test MiniWallet."""
+from test_framework.blocktools import COINBASE_MATURITY
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_greater_than_or_equal,
+)
+from test_framework.wallet import (
+ MiniWallet,
+ MiniWalletMode,
+)
+
+
+class FeatureFrameworkMiniWalletTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+
+ def test_tx_padding(self):
+ """Verify that MiniWallet's transaction padding (`target_weight` parameter)
+ works accurately enough (i.e. at most 3 WUs higher) with all modes."""
+ for mode_name, wallet in self.wallets:
+ self.log.info(f"Test tx padding with MiniWallet mode {mode_name}...")
+ utxo = wallet.get_utxo(mark_as_spent=False)
+ for target_weight in [1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 4000000,
+ 989, 2001, 4337, 13371, 23219, 49153, 102035, 223419, 3999989]:
+ tx = wallet.create_self_transfer(utxo_to_spend=utxo, target_weight=target_weight)["tx"]
+ self.log.debug(f"-> target weight: {target_weight}, actual weight: {tx.get_weight()}")
+ assert_greater_than_or_equal(tx.get_weight(), target_weight)
+ assert_greater_than_or_equal(target_weight + 3, tx.get_weight())
+
+ def run_test(self):
+ node = self.nodes[0]
+ self.wallets = [
+ ("ADDRESS_OP_TRUE", MiniWallet(node, mode=MiniWalletMode.ADDRESS_OP_TRUE)),
+ ("RAW_OP_TRUE", MiniWallet(node, mode=MiniWalletMode.RAW_OP_TRUE)),
+ ("RAW_P2PK", MiniWallet(node, mode=MiniWalletMode.RAW_P2PK)),
+ ]
+ for _, wallet in self.wallets:
+ self.generate(wallet, 10)
+ self.generate(wallet, COINBASE_MATURITY)
+
+ self.test_tx_padding()
+
+
+if __name__ == '__main__':
+ FeatureFrameworkMiniWalletTest().main()
diff --git a/test/functional/feature_framework_unit_tests.py b/test/functional/feature_framework_unit_tests.py
index f03f084bed..14d83f8a70 100755
--- a/test/functional/feature_framework_unit_tests.py
+++ b/test/functional/feature_framework_unit_tests.py
@@ -27,6 +27,7 @@ TEST_FRAMEWORK_MODULES = [
"crypto.ripemd160",
"crypto.secp256k1",
"script",
+ "script_util",
"segwit_addr",
"wallet_util",
]
diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py
index 4b548ef0f3..5f99b8dee8 100755
--- a/test/functional/feature_pruning.py
+++ b/test/functional/feature_pruning.py
@@ -25,6 +25,7 @@ from test_framework.util import (
assert_equal,
assert_greater_than,
assert_raises_rpc_error,
+ try_rpc,
)
# Rescans start at the earliest block up to 2 hours before a key timestamp, so
@@ -479,8 +480,12 @@ class PruneTest(BitcoinTestFramework):
self.log.info("Test invalid pruning command line options")
self.test_invalid_command_line_options()
+ self.log.info("Test scanblocks can not return pruned data")
self.test_scanblocks_pruned()
+ self.log.info("Test pruneheight reflects the presence of block and undo data")
+ self.test_pruneheight_undo_presence()
+
self.log.info("Done")
def test_scanblocks_pruned(self):
@@ -494,5 +499,18 @@ class PruneTest(BitcoinTestFramework):
assert_raises_rpc_error(-1, "Block not available (pruned data)", node.scanblocks,
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})
+ def test_pruneheight_undo_presence(self):
+ node = self.nodes[2]
+ pruneheight = node.getblockchaininfo()["pruneheight"]
+ fetch_block = node.getblockhash(pruneheight - 1)
+
+ self.connect_nodes(1, 2)
+ peers = node.getpeerinfo()
+ node.getblockfrompeer(fetch_block, peers[0]["id"])
+ self.wait_until(lambda: not try_rpc(-1, "Block not available (pruned data)", node.getblock, fetch_block), timeout=5)
+
+ new_pruneheight = node.getblockchaininfo()["pruneheight"]
+ assert_equal(pruneheight, new_pruneheight)
+
if __name__ == '__main__':
PruneTest().main()
diff --git a/test/functional/feature_reindex.py b/test/functional/feature_reindex.py
index f0f32a61ab..835cd0c5cf 100755
--- a/test/functional/feature_reindex.py
+++ b/test/functional/feature_reindex.py
@@ -73,6 +73,25 @@ class ReindexTest(BitcoinTestFramework):
# All blocks should be accepted and processed.
assert_equal(self.nodes[0].getblockcount(), 12)
+ def continue_reindex_after_shutdown(self):
+ node = self.nodes[0]
+ self.generate(node, 1500)
+
+ # Restart node with reindex and stop reindex as soon as it starts reindexing
+ self.log.info("Restarting node while reindexing..")
+ node.stop_node()
+ with node.busy_wait_for_debug_log([b'initload thread start']):
+ node.start(['-blockfilterindex', '-reindex'])
+ node.wait_for_rpc_connection(wait_for_import=False)
+ node.stop_node()
+
+ # Start node without the reindex flag and verify it does not wipe the indexes data again
+ db_path = node.chain_path / 'indexes' / 'blockfilter' / 'basic' / 'db'
+ with node.assert_debug_log(expected_msgs=[f'Opening LevelDB in {db_path}'], unexpected_msgs=[f'Wiping LevelDB in {db_path}']):
+ node.start(['-blockfilterindex'])
+ node.wait_for_rpc_connection(wait_for_import=False)
+ node.stop_node()
+
def run_test(self):
self.reindex(False)
self.reindex(True)
@@ -80,6 +99,7 @@ class ReindexTest(BitcoinTestFramework):
self.reindex(True)
self.out_of_order()
+ self.continue_reindex_after_shutdown()
if __name__ == '__main__':
diff --git a/test/functional/feature_settings.py b/test/functional/feature_settings.py
index 0214e781de..1cd0aeabd3 100755
--- a/test/functional/feature_settings.py
+++ b/test/functional/feature_settings.py
@@ -25,7 +25,7 @@ class SettingsTest(BitcoinTestFramework):
# Assert default settings file was created
self.stop_node(0)
- default_settings = {"_warning_": "This file is automatically generated and updated by Bitcoin Core. Please do not edit this file while the node is running, as any changes might be ignored or overwritten."}
+ default_settings = {"_warning_": f"This file is automatically generated and updated by {self.config['environment']['PACKAGE_NAME']}. Please do not edit this file while the node is running, as any changes might be ignored or overwritten."}
with settings.open() as fp:
assert_equal(json.load(fp), default_settings)
diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py
index e7d65b4539..1a0844d240 100755
--- a/test/functional/feature_taproot.py
+++ b/test/functional/feature_taproot.py
@@ -1408,10 +1408,10 @@ class TaprootTest(BitcoinTestFramework):
left = done
while left:
- # Construct CTransaction with random nVersion, nLocktime
+ # Construct CTransaction with random version, nLocktime
tx = CTransaction()
- tx.nVersion = random.choice([1, 2, random.randint(-0x80000000, 0x7fffffff)])
- min_sequence = (tx.nVersion != 1 and tx.nVersion != 0) * 0x80000000 # The minimum sequence number to disable relative locktime
+ tx.version = random.choice([1, 2, random.getrandbits(32)])
+ min_sequence = (tx.version != 1 and tx.version != 0) * 0x80000000 # The minimum sequence number to disable relative locktime
if random.choice([True, False]):
tx.nLockTime = random.randrange(LOCKTIME_THRESHOLD, self.lastblocktime - 7200) # all absolute locktimes in the past
else:
@@ -1502,8 +1502,8 @@ class TaprootTest(BitcoinTestFramework):
is_standard_tx = (
fail_input is None # Must be valid to be standard
and (all(utxo.spender.is_standard for utxo in input_utxos)) # All inputs must be standard
- and tx.nVersion >= 1 # The tx version must be standard
- and tx.nVersion <= 2)
+ and tx.version >= 1 # The tx version must be standard
+ and tx.version <= 2)
tx.rehash()
msg = ','.join(utxo.spender.comment + ("*" if n == fail_input else "") for n, utxo in enumerate(input_utxos))
if is_standard_tx:
@@ -1530,7 +1530,7 @@ class TaprootTest(BitcoinTestFramework):
# Deterministically mine coins to OP_TRUE in block 1
assert_equal(self.nodes[0].getblockcount(), 0)
coinbase = CTransaction()
- coinbase.nVersion = 1
+ coinbase.version = 1
coinbase.vin = [CTxIn(COutPoint(0, 0xffffffff), CScript([OP_1, OP_1]), SEQUENCE_FINAL)]
coinbase.vout = [CTxOut(5000000000, CScript([OP_1]))]
coinbase.nLockTime = 0
@@ -1622,7 +1622,7 @@ class TaprootTest(BitcoinTestFramework):
for i, spk in enumerate(old_spks + tap_spks):
val = 42000000 * (i + 7)
tx = CTransaction()
- tx.nVersion = 1
+ tx.version = 1
tx.vin = [CTxIn(COutPoint(lasttxid, i & 1), CScript([]), SEQUENCE_FINAL)]
tx.vout = [CTxOut(val, spk), CTxOut(amount - val, CScript([OP_1]))]
if i & 1:
@@ -1679,7 +1679,7 @@ class TaprootTest(BitcoinTestFramework):
# Construct a deterministic transaction spending all outputs created above.
tx = CTransaction()
- tx.nVersion = 2
+ tx.version = 2
tx.vin = []
inputs = []
input_spks = [tap_spks[0], tap_spks[1], old_spks[0], tap_spks[2], tap_spks[5], old_spks[2], tap_spks[6], tap_spks[3], tap_spks[4]]
diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py
index 83bb5121e5..a6628dcbf3 100755
--- a/test/functional/interface_bitcoin_cli.py
+++ b/test/functional/interface_bitcoin_cli.py
@@ -8,6 +8,7 @@ from decimal import Decimal
import re
from test_framework.blocktools import COINBASE_MATURITY
+from test_framework.netutil import test_ipv6_local
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -15,6 +16,7 @@ from test_framework.util import (
assert_raises_process_error,
assert_raises_rpc_error,
get_auth_cookie,
+ rpc_port,
)
import time
@@ -107,6 +109,53 @@ class TestBitcoinCli(BitcoinTestFramework):
self.log.info("Test connecting to a non-existing server")
assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo)
+ self.log.info("Test handling of invalid ports in rpcconnect")
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:notaport", self.nodes[0].cli("-rpcconnect=127.0.0.1:notaport").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:-1", self.nodes[0].cli("-rpcconnect=127.0.0.1:-1").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:0", self.nodes[0].cli("-rpcconnect=127.0.0.1:0").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: 127.0.0.1:65536", self.nodes[0].cli("-rpcconnect=127.0.0.1:65536").echo)
+
+ self.log.info("Checking for IPv6")
+ have_ipv6 = test_ipv6_local()
+ if not have_ipv6:
+ self.log.info("Skipping IPv6 tests")
+
+ if have_ipv6:
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:notaport", self.nodes[0].cli("-rpcconnect=[::1]:notaport").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:-1", self.nodes[0].cli("-rpcconnect=[::1]:-1").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:0", self.nodes[0].cli("-rpcconnect=[::1]:0").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcconnect: [::1]:65536", self.nodes[0].cli("-rpcconnect=[::1]:65536").echo)
+
+ self.log.info("Test handling of invalid ports in rpcport")
+ assert_raises_process_error(1, "Invalid port provided in -rpcport: notaport", self.nodes[0].cli("-rpcport=notaport").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcport: -1", self.nodes[0].cli("-rpcport=-1").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcport: 0", self.nodes[0].cli("-rpcport=0").echo)
+ assert_raises_process_error(1, "Invalid port provided in -rpcport: 65536", self.nodes[0].cli("-rpcport=65536").echo)
+
+ self.log.info("Test port usage preferences")
+ node_rpc_port = rpc_port(self.nodes[0].index)
+ # Prevent bitcoin-cli from using existing rpcport in conf
+ conf_rpcport = "rpcport=" + str(node_rpc_port)
+ self.nodes[0].replace_in_config([(conf_rpcport, "#" + conf_rpcport)])
+ # prefer rpcport over rpcconnect
+ assert_raises_process_error(1, "Could not connect to the server 127.0.0.1:1", self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}", "-rpcport=1").echo)
+ if have_ipv6:
+ assert_raises_process_error(1, "Could not connect to the server ::1:1", self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}", "-rpcport=1").echo)
+
+ assert_equal(BLOCKS, self.nodes[0].cli("-rpcconnect=127.0.0.1:18999", f'-rpcport={node_rpc_port}').getblockcount())
+ if have_ipv6:
+ assert_equal(BLOCKS, self.nodes[0].cli("-rpcconnect=[::1]:18999", f'-rpcport={node_rpc_port}').getblockcount())
+
+ # prefer rpcconnect port over default
+ assert_equal(BLOCKS, self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}").getblockcount())
+ if have_ipv6:
+ assert_equal(BLOCKS, self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}").getblockcount())
+
+ # prefer rpcport over default
+ assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcport={node_rpc_port}').getblockcount())
+ # Re-enable rpcport in conf if present
+ self.nodes[0].replace_in_config([("#" + conf_rpcport, conf_rpcport)])
+
self.log.info("Test connecting with non-existing RPC cookie file")
assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)
diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py
index b08ca42796..6c1855c400 100755
--- a/test/functional/interface_rpc.py
+++ b/test/functional/interface_rpc.py
@@ -14,7 +14,6 @@ from typing import Optional
import subprocess
-RPC_INVALID_ADDRESS_OR_KEY = -5
RPC_INVALID_PARAMETER = -8
RPC_METHOD_NOT_FOUND = -32601
RPC_INVALID_REQUEST = -32600
diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py
index b00be5f4f0..e1cee46839 100755
--- a/test/functional/mempool_accept.py
+++ b/test/functional/mempool_accept.py
@@ -18,6 +18,7 @@ from test_framework.messages import (
CTxInWitness,
CTxOut,
MAX_BLOCK_WEIGHT,
+ WITNESS_SCALE_FACTOR,
MAX_MONEY,
SEQUENCE_FINAL,
tx_from_hex,
@@ -228,7 +229,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
self.log.info('A really large transaction')
tx = tx_from_hex(raw_tx_reference)
- tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_WEIGHT // 4 / len(tx.vin[0].serialize()))
+ tx.vin = [tx.vin[0]] * math.ceil((MAX_BLOCK_WEIGHT // WITNESS_SCALE_FACTOR) / len(tx.vin[0].serialize()))
self.check_mempool_result(
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bad-txns-oversize'}],
rawtxs=[tx.serialize().hex()],
@@ -287,7 +288,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
self.log.info('Some nonstandard transactions')
tx = tx_from_hex(raw_tx_reference)
- tx.nVersion = 3 # A version currently non-standard
+ tx.version = 4 # A version currently non-standard
self.check_mempool_result(
result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}],
rawtxs=[tx.serialize().hex()],
diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py
index d46924f4ce..49a0a32c45 100755
--- a/test/functional/mempool_limit.py
+++ b/test/functional/mempool_limit.py
@@ -59,7 +59,7 @@ class MempoolLimitTest(BitcoinTestFramework):
mempoolmin_feerate = node.getmempoolinfo()["mempoolminfee"]
tx_A = self.wallet.send_self_transfer(
from_node=node,
- fee=(mempoolmin_feerate / 1000) * (A_weight // 4) + Decimal('0.000001'),
+ fee_rate=mempoolmin_feerate,
target_weight=A_weight,
utxo_to_spend=rbf_utxo,
confirmed_only=True
@@ -77,7 +77,7 @@ class MempoolLimitTest(BitcoinTestFramework):
non_cpfp_carveout_weight = 40001 # EXTRA_DESCENDANT_TX_SIZE_LIMIT + 1
tx_C = self.wallet.create_self_transfer(
target_weight=non_cpfp_carveout_weight,
- fee = (mempoolmin_feerate / 1000) * (non_cpfp_carveout_weight // 4) + Decimal('0.000001'),
+ fee_rate=mempoolmin_feerate,
utxo_to_spend=tx_B["new_utxo"],
confirmed_only=True
)
@@ -109,7 +109,7 @@ class MempoolLimitTest(BitcoinTestFramework):
# happen in the middle of package evaluation, as it can invalidate the coins cache.
mempool_evicted_tx = self.wallet.send_self_transfer(
from_node=node,
- fee=(mempoolmin_feerate / 1000) * (evicted_weight // 4) + Decimal('0.000001'),
+ fee_rate=mempoolmin_feerate,
target_weight=evicted_weight,
confirmed_only=True
)
@@ -135,11 +135,11 @@ class MempoolLimitTest(BitcoinTestFramework):
parent_weight = 100000
num_big_parents = 3
assert_greater_than(parent_weight * num_big_parents, current_info["maxmempool"] - current_info["bytes"])
- parent_fee = (100 * mempoolmin_feerate / 1000) * (parent_weight // 4)
+ parent_feerate = 100 * mempoolmin_feerate
big_parent_txids = []
for i in range(num_big_parents):
- parent = self.wallet.create_self_transfer(fee=parent_fee, target_weight=parent_weight, confirmed_only=True)
+ parent = self.wallet.create_self_transfer(fee_rate=parent_feerate, target_weight=parent_weight, confirmed_only=True)
parent_utxos.append(parent["new_utxo"])
package_hex.append(parent["hex"])
big_parent_txids.append(parent["txid"])
@@ -314,18 +314,20 @@ class MempoolLimitTest(BitcoinTestFramework):
target_weight_each = 200000
assert_greater_than(target_weight_each * 2, node.getmempoolinfo()["maxmempool"] - node.getmempoolinfo()["bytes"])
# Should be a true CPFP: parent's feerate is just below mempool min feerate
- parent_fee = (mempoolmin_feerate / 1000) * (target_weight_each // 4) - Decimal("0.00001")
+ parent_feerate = mempoolmin_feerate - Decimal("0.000001") # 0.1 sats/vbyte below min feerate
# Parent + child is above mempool minimum feerate
- child_fee = (worst_feerate_btcvb) * (target_weight_each // 4) - Decimal("0.00001")
+ child_feerate = (worst_feerate_btcvb * 1000) - Decimal("0.000001") # 0.1 sats/vbyte below worst feerate
# However, when eviction is triggered, these transactions should be at the bottom.
# This assertion assumes parent and child are the same size.
miniwallet.rescan_utxos()
- tx_parent_just_below = miniwallet.create_self_transfer(fee=parent_fee, target_weight=target_weight_each)
- tx_child_just_above = miniwallet.create_self_transfer(utxo_to_spend=tx_parent_just_below["new_utxo"], fee=child_fee, target_weight=target_weight_each)
+ tx_parent_just_below = miniwallet.create_self_transfer(fee_rate=parent_feerate, target_weight=target_weight_each)
+ tx_child_just_above = miniwallet.create_self_transfer(utxo_to_spend=tx_parent_just_below["new_utxo"], fee_rate=child_feerate, target_weight=target_weight_each)
# This package ranks below the lowest descendant package in the mempool
- assert_greater_than(worst_feerate_btcvb, (parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()))
- assert_greater_than(mempoolmin_feerate, (parent_fee) / (tx_parent_just_below["tx"].get_vsize()))
- assert_greater_than((parent_fee + child_fee) / (tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()), mempoolmin_feerate / 1000)
+ package_fee = tx_parent_just_below["fee"] + tx_child_just_above["fee"]
+ package_vsize = tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()
+ assert_greater_than(worst_feerate_btcvb, package_fee / package_vsize)
+ assert_greater_than(mempoolmin_feerate, tx_parent_just_below["fee"] / (tx_parent_just_below["tx"].get_vsize()))
+ assert_greater_than(package_fee / package_vsize, mempoolmin_feerate / 1000)
res = node.submitpackage([tx_parent_just_below["hex"], tx_child_just_above["hex"]])
for wtxid in [tx_parent_just_below["wtxid"], tx_child_just_above["wtxid"]]:
assert_equal(res["tx-results"][wtxid]["error"], "mempool full")
diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py
index 98b397e32b..632425814a 100755
--- a/test/functional/mempool_package_onemore.py
+++ b/test/functional/mempool_package_onemore.py
@@ -40,28 +40,37 @@ class MempoolPackagesTest(BitcoinTestFramework):
for _ in range(DEFAULT_ANCESTOR_LIMIT - 4):
utxo, = self.chain_tx([utxo])
chain.append(utxo)
- second_chain, = self.chain_tx([self.wallet.get_utxo()])
+ second_chain, = self.chain_tx([self.wallet.get_utxo(confirmed_only=True)])
# Check mempool has DEFAULT_ANCESTOR_LIMIT + 1 transactions in it
assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 1)
# Adding one more transaction on to the chain should fail.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_tx, [utxo])
- # ...even if it chains on from some point in the middle of the chain.
+ # ... or if it chains on from some point in the middle of the chain.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[2]])
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[1]])
# ...even if it chains on to two parent transactions with one in the chain.
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0], second_chain])
# ...especially if its > 40k weight
assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0]], num_outputs=350)
+ # ...even if it's submitted with other transactions
+ replaceable_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[chain[0]])
+ txns = [replaceable_tx["tx"], self.wallet.create_self_transfer_multi(utxos_to_spend=replaceable_tx["new_utxos"])["tx"]]
+ txns_hex = [tx.serialize().hex() for tx in txns]
+ assert_equal(self.nodes[0].testmempoolaccept(txns_hex)[0]["reject-reason"], "too-long-mempool-chain")
+ pkg_result = self.nodes[0].submitpackage(txns_hex)
+ assert "too-long-mempool-chain" in pkg_result["tx-results"][txns[0].getwtxid()]["error"]
+ assert_equal(pkg_result["tx-results"][txns[1].getwtxid()]["error"], "bad-txns-inputs-missingorspent")
# But not if it chains directly off the first transaction
- replacable_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[chain[0]])['tx']
+ self.nodes[0].sendrawtransaction(replaceable_tx["hex"])
# and the second chain should work just fine
self.chain_tx([second_chain])
- # Make sure we can RBF the chain which used our carve-out rule
- replacable_tx.vout[0].nValue -= 1000000
- self.nodes[0].sendrawtransaction(replacable_tx.serialize().hex())
+ # Ensure an individual transaction with single direct conflict can RBF the chain which used our carve-out rule
+ replacement_tx = replaceable_tx["tx"]
+ replacement_tx.vout[0].nValue -= 1000000
+ self.nodes[0].sendrawtransaction(replacement_tx.serialize().hex())
# Finally, check that we added two transactions
assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 3)
diff --git a/test/functional/mempool_package_rbf.py b/test/functional/mempool_package_rbf.py
new file mode 100755
index 0000000000..ceb9530394
--- /dev/null
+++ b/test/functional/mempool_package_rbf.py
@@ -0,0 +1,587 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+from decimal import Decimal
+
+from test_framework.messages import (
+ COIN,
+ MAX_BIP125_RBF_SEQUENCE,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mempool_util import fill_mempool
+from test_framework.util import (
+ assert_greater_than_or_equal,
+ assert_equal,
+)
+from test_framework.wallet import (
+ DEFAULT_FEE,
+ MiniWallet,
+)
+
+MAX_REPLACEMENT_CANDIDATES = 100
+
+# Value high enough to cause evictions in each subtest
+# for typical cases
+DEFAULT_CHILD_FEE = DEFAULT_FEE * 4
+
+class PackageRBFTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 2
+ self.setup_clean_chain = True
+ # Required for fill_mempool()
+ self.extra_args = [[
+ "-datacarriersize=100000",
+ "-maxmempool=5",
+ ]] * self.num_nodes
+
+ def assert_mempool_contents(self, expected=None):
+ """Assert that all transactions in expected are in the mempool,
+ and no additional ones exist.
+ """
+ if not expected:
+ expected = []
+ mempool = self.nodes[0].getrawmempool(verbose=False)
+ assert_equal(len(mempool), len(expected))
+ for tx in expected:
+ assert tx.rehash() in mempool
+
+ def create_simple_package(self, parent_coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=False):
+ """Create a 1 parent 1 child package using the coin passed in as the parent's input. The
+ parent has 1 output, used to fund 1 child transaction.
+ All transactions signal BIP125 replaceability, but nSequence changes based on self.ctr. This
+ prevents identical txids between packages when the parents spend the same coin and have the
+ same fee (i.e. 0sat).
+
+ returns tuple (hex serialized txns, CTransaction objects)
+ """
+ self.ctr += 1
+ # Use fee_rate=0 because create_self_transfer will use the default fee_rate value otherwise.
+ # Passing in fee>0 overrides fee_rate, so this still works for non-zero parent_fee.
+ parent_result = self.wallet.create_self_transfer(
+ fee=parent_fee,
+ utxo_to_spend=parent_coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ num_child_outputs = 10 if heavy_child else 1
+ child_result = self.wallet.create_self_transfer_multi(
+ utxos_to_spend=[parent_result["new_utxo"]],
+ num_outputs=num_child_outputs,
+ fee_per_output=int(child_fee * COIN // num_child_outputs),
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+ package_hex = [parent_result["hex"], child_result["hex"]]
+ package_txns = [parent_result["tx"], child_result["tx"]]
+ return package_hex, package_txns
+
+ def run_test(self):
+ # Counter used to count the number of times we constructed packages. Since we're constructing parent transactions with the same
+ # coins (to create conflicts), and perhaps giving them the same fee, we might accidentally just create the same transaction again.
+ # To prevent this, set nSequences to MAX_BIP125_RBF_SEQUENCE - self.ctr.
+ self.ctr = 0
+
+ self.log.info("Generate blocks to create UTXOs")
+ self.wallet = MiniWallet(self.nodes[0])
+
+ # Make more than enough coins for the sum of all tests,
+ # otherwise a wallet rescan is needed later
+ self.generate(self.wallet, 300)
+ self.coins = self.wallet.get_utxos(mark_as_spent=False)
+
+ self.test_package_rbf_basic()
+ self.test_package_rbf_singleton()
+ self.test_package_rbf_additional_fees()
+ self.test_package_rbf_max_conflicts()
+ self.test_too_numerous_ancestors()
+ self.test_package_rbf_with_wrong_pkg_size()
+ self.test_insufficient_feerate()
+ self.test_wrong_conflict_cluster_size_linear()
+ self.test_wrong_conflict_cluster_size_parents_child()
+ self.test_wrong_conflict_cluster_size_parent_children()
+ self.test_0fee_package_rbf()
+ self.test_child_conflicts_parent_mempool_ancestor()
+
+ def test_package_rbf_basic(self):
+ self.log.info("Test that a child can pay to replace its parents' conflicts of cluster size 2")
+ node = self.nodes[0]
+ # Reuse the same coins so that the transactions conflict with one another.
+ parent_coin = self.coins.pop()
+ package_hex1, package_txns1 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_FEE)
+ package_hex2, package_txns2 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Make sure 2nd node gets set up for basic package RBF
+ self.sync_all()
+
+ # Test run rejected because conflicts are not allowed in subpackage evaluation
+ testres = node.testmempoolaccept(package_hex2)
+ assert_equal(testres[0]["reject-reason"], "bip125-replacement-disallowed")
+
+ # But accepted during normal submission
+ submitres = node.submitpackage(package_hex2)
+ assert_equal(set(submitres["replaced-transactions"]), set([tx.rehash() for tx in package_txns1]))
+ self.assert_mempool_contents(expected=package_txns2)
+
+ # Make sure 2nd node gets a basic package RBF over p2p
+ self.sync_all()
+
+ self.generate(node, 1)
+
+ def test_package_rbf_singleton(self):
+ self.log.info("Test child can pay to replace a parent's single conflicted tx")
+ node = self.nodes[0]
+
+ # Make singleton tx to conflict with in next batch
+ singleton_coin = self.coins.pop()
+ singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)
+ node.sendrawtransaction(singleton_tx["hex"])
+ self.assert_mempool_contents(expected=[singleton_tx["tx"]])
+
+ package_hex, package_txns = self.create_simple_package(singleton_coin, DEFAULT_FEE, singleton_tx["fee"] * 2)
+
+ submitres = node.submitpackage(package_hex)
+ assert_equal(submitres["replaced-transactions"], [singleton_tx["tx"].rehash()])
+ self.assert_mempool_contents(expected=package_txns)
+
+ self.generate(node, 1)
+
+ def test_package_rbf_additional_fees(self):
+ self.log.info("Check Package RBF must increase the absolute fee")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=True)
+ assert_greater_than_or_equal(1000, package_txns1[-1].get_vsize())
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ PACKAGE_FEE = DEFAULT_FEE + DEFAULT_CHILD_FEE
+ PACKAGE_FEE_MINUS_ONE = PACKAGE_FEE - Decimal("0.00000001")
+
+ # Package 2 has a higher feerate but lower absolute fee
+ package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"))
+ pkg_results2 = node.submitpackage(package_hex2)
+ assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {package_txns2[1].rehash()}, less fees than conflicting txs; {PACKAGE_FEE_MINUS_ONE} < {PACKAGE_FEE}", pkg_results2["package_msg"])
+ self.assert_mempool_contents(expected=package_txns1)
+
+ self.log.info("Check replacement pays for incremental bandwidth")
+ package_hex3, package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE)
+ pkg_results3 = node.submitpackage(package_hex3)
+ assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {package_txns3[1].rehash()}, not enough additional fees to relay; 0.00 < 0.00000{sum([tx.get_vsize() for tx in package_txns3])}", pkg_results3["package_msg"])
+
+ self.assert_mempool_contents(expected=package_txns1)
+ self.generate(node, 1)
+
+ self.log.info("Check Package RBF must have strict cpfp structure")
+ coin = self.coins.pop()
+ package_hex4, package_txns4 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex4)
+ self.assert_mempool_contents(expected=package_txns4)
+ package_hex5, package_txns5 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"))
+ pkg_results5 = node.submitpackage(package_hex5)
+ assert 'package RBF failed: package feerate is less than parent feerate' in pkg_results5["package_msg"]
+
+ self.assert_mempool_contents(expected=package_txns4)
+ self.generate(node, 1)
+
+ def test_package_rbf_max_conflicts(self):
+ node = self.nodes[0]
+ self.log.info("Check Package RBF cannot replace more than MAX_REPLACEMENT_CANDIDATES transactions")
+ num_coins = 51
+ parent_coins = self.coins[:num_coins]
+ del self.coins[:num_coins]
+
+ # Original transactions: 51 transactions with 1 descendants each -> 102 total transactions
+ size_two_clusters = []
+ for coin in parent_coins:
+ size_two_clusters.append(self.wallet.send_self_transfer_chain(from_node=node, chain_length=2, utxo_to_spend=coin))
+ expected_txns = [txn["tx"] for parent_child_txns in size_two_clusters for txn in parent_child_txns]
+ assert_equal(len(expected_txns), num_coins * 2)
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # parent feeerate needs to be high enough for minrelay
+ # child feerate needs to be large enough to trigger package rbf with a very large parent and
+ # pay for all evicted fees. maxfeerate turned off for all submissions since child feerate
+ # is extremely high
+ parent_fee_per_conflict = 10000
+ child_feerate = 10000 * DEFAULT_FEE
+
+ # Conflict against all transactions by double-spending each parent, causing 102 evictions
+ package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins, fee_per_output=parent_fee_per_conflict)
+ package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
+
+ pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
+ assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (102 > 100)\n", pkg_results["package_msg"])
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Make singleton tx to conflict with in next batch
+ singleton_coin = self.coins.pop()
+ singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)
+ node.sendrawtransaction(singleton_tx["hex"])
+ expected_txns.append(singleton_tx["tx"])
+
+ # Double-spend same set minus last, and double-spend singleton. This hits 101 evictions; should still fail.
+ # N.B. we can't RBF just a child tx in the clusters, as that would make resulting cluster of size 3.
+ double_spending_coins = parent_coins[:-1] + [singleton_coin]
+ package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=double_spending_coins, fee_per_output=parent_fee_per_conflict)
+ package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
+ pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
+ assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].rehash()}; too many potential replacements (101 > 100)\n", pkg_results["package_msg"])
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Finally, evict MAX_REPLACEMENT_CANDIDATES
+ package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins[:-1], fee_per_output=parent_fee_per_conflict)
+ package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
+ pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
+ assert_equal(pkg_results["package_msg"], "success")
+ self.assert_mempool_contents(expected=[singleton_tx["tx"], size_two_clusters[-1][0]["tx"], size_two_clusters[-1][1]["tx"], package_parent["tx"], package_child["tx"]] )
+
+ self.generate(node, 1)
+
+ def test_too_numerous_ancestors(self):
+ self.log.info("Test that package RBF doesn't work with packages larger than 2 due to ancestors")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ package_hex1, package_txns1 = self.create_simple_package(coin, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Double-spends the original package
+ self.ctr += 1
+ parent_result1 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ coin2 = self.coins.pop()
+
+ # Added to make package too large for package RBF;
+ # it will enter mempool individually
+ self.ctr += 1
+ parent_result2 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin2,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Child that spends both, violating cluster size rule due
+ # to in-mempool ancestry
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result1["new_utxo"], parent_result2["new_utxo"]],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ package_hex2 = [parent_result1["hex"], parent_result2["hex"], child_result["hex"]]
+ package_txns2_succeed = [parent_result2["tx"]]
+
+ pkg_result = node.submitpackage(package_hex2)
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: new transaction cannot have mempool ancestors')
+ self.assert_mempool_contents(expected=package_txns1 + package_txns2_succeed)
+ self.generate(node, 1)
+
+ def test_wrong_conflict_cluster_size_linear(self):
+ self.log.info("Test that conflicting with a cluster not sized two is rejected: linear chain")
+ node = self.nodes[0]
+
+ # Coins we will conflict with
+ coin1 = self.coins.pop()
+ coin2 = self.coins.pop()
+ coin3 = self.coins.pop()
+
+ # Three transactions chained; package RBF against any of these
+ # should be rejected
+ self.ctr += 1
+ parent_result = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin1,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[parent_result["new_utxo"], coin2],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ grandchild_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[child_result["new_utxos"][0], coin3],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ expected_txns = [parent_result["tx"], child_result["tx"], grandchild_result["tx"]]
+ for tx in expected_txns:
+ node.sendrawtransaction(tx.serialize().hex())
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Now make conflicting packages for each coin
+ package_hex1, package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+
+ package_result = node.submitpackage(package_hex1)
+ assert_equal(f"package RBF failed: {parent_result['tx'].rehash()} has 2 descendants, max 1 allowed", package_result["package_msg"])
+
+ package_hex2, package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex2)
+ assert_equal(f"package RBF failed: {child_result['tx'].rehash()} has both ancestor and descendant, exceeding cluster limit of 2", package_result["package_msg"])
+
+ package_hex3, package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex3)
+ assert_equal(f"package RBF failed: {grandchild_result['tx'].rehash()} has 2 ancestors, max 1 allowed", package_result["package_msg"])
+
+ # Check that replacements were actually rejected
+ self.assert_mempool_contents(expected=expected_txns)
+ self.generate(node, 1)
+
+ def test_wrong_conflict_cluster_size_parents_child(self):
+ self.log.info("Test that conflicting with a cluster not sized two is rejected: two parents one child")
+ node = self.nodes[0]
+
+ # Coins we will conflict with
+ coin1 = self.coins.pop()
+ coin2 = self.coins.pop()
+ coin3 = self.coins.pop()
+
+ self.ctr += 1
+ parent1_result = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin1,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ parent2_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[coin2],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[parent1_result["new_utxo"], parent2_result["new_utxos"][0], coin3],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ expected_txns = [parent1_result["tx"], parent2_result["tx"], child_result["tx"]]
+ for tx in expected_txns:
+ node.sendrawtransaction(tx.serialize().hex())
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Now make conflicting packages for each coin
+ package_hex1, package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex1)
+ assert_equal(f"package RBF failed: {parent1_result['tx'].rehash()} is not the only parent of child {child_result['tx'].rehash()}", package_result["package_msg"])
+
+ package_hex2, package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex2)
+ assert_equal(f"package RBF failed: {parent2_result['tx'].rehash()} is not the only parent of child {child_result['tx'].rehash()}", package_result["package_msg"])
+
+ package_hex3, package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex3)
+ assert_equal(f"package RBF failed: {child_result['tx'].rehash()} has 2 ancestors, max 1 allowed", package_result["package_msg"])
+
+ # Check that replacements were actually rejected
+ self.assert_mempool_contents(expected=expected_txns)
+ self.generate(node, 1)
+
+ def test_wrong_conflict_cluster_size_parent_children(self):
+ self.log.info("Test that conflicting with a cluster not sized two is rejected: one parent two children")
+ node = self.nodes[0]
+
+ # Coins we will conflict with
+ coin1 = self.coins.pop()
+ coin2 = self.coins.pop()
+ coin3 = self.coins.pop()
+
+ self.ctr += 1
+ parent_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ num_outputs=2,
+ utxos_to_spend=[coin1],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ child1_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[parent_result["new_utxos"][0], coin2],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ self.ctr += 1
+ child2_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_FEE * COIN),
+ utxos_to_spend=[parent_result["new_utxos"][1], coin3],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Submit them to mempool
+ expected_txns = [parent_result["tx"], child1_result["tx"], child2_result["tx"]]
+ for tx in expected_txns:
+ node.sendrawtransaction(tx.serialize().hex())
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Now make conflicting packages for each coin
+ package_hex1, package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex1)
+ assert_equal(f"package RBF failed: {parent_result['tx'].rehash()} has 2 descendants, max 1 allowed", package_result["package_msg"])
+
+ package_hex2, package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex2)
+ assert_equal(f"package RBF failed: {child1_result['tx'].rehash()} is not the only child of parent {parent_result['tx'].rehash()}", package_result["package_msg"])
+
+ package_hex3, package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_result = node.submitpackage(package_hex3)
+ assert_equal(f"package RBF failed: {child2_result['tx'].rehash()} is not the only child of parent {parent_result['tx'].rehash()}", package_result["package_msg"])
+
+ # Check that replacements were actually rejected
+ self.assert_mempool_contents(expected=expected_txns)
+ self.generate(node, 1)
+
+ def test_package_rbf_with_wrong_pkg_size(self):
+ self.log.info("Test that package RBF doesn't work with packages larger than 2 due to pkg size")
+ node = self.nodes[0]
+ coin1 = self.coins.pop()
+ coin2 = self.coins.pop()
+
+ # Two packages to require multiple direct conflicts, easier to set up illicit pkg size
+ package_hex1, package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_hex2, package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+
+ node.submitpackage(package_hex1)
+ node.submitpackage(package_hex2)
+
+ self.assert_mempool_contents(expected=package_txns1 + package_txns2)
+ assert_equal(len(node.getrawmempool()), 4)
+
+ # Double-spends the first package
+ self.ctr += 1
+ parent_result1 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin1,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Double-spends the second package
+ self.ctr += 1
+ parent_result2 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin2,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Child that spends both, violating cluster size rule due
+ # to pkg size
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result1["new_utxo"], parent_result2["new_utxo"]],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ package_hex3 = [parent_result1["hex"], parent_result2["hex"], child_result["hex"]]
+
+ pkg_result = node.submitpackage(package_hex3)
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: package must be 1-parent-1-child')
+ self.assert_mempool_contents(expected=package_txns1 + package_txns2)
+ self.generate(node, 1)
+
+ def test_insufficient_feerate(self):
+ self.log.info("Check Package RBF must beat feerate of direct conflict")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ # Non-cpfp structure
+ package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Package 2 feerate is below the feerate of directly conflicted parent, so it fails even though
+ # total fees are higher than the original package
+ package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"), child_fee=DEFAULT_CHILD_FEE)
+ pkg_results2 = node.submitpackage(package_hex2)
+ assert_equal(pkg_results2["package_msg"], 'package RBF failed: insufficient feerate: does not improve feerate diagram')
+ self.assert_mempool_contents(expected=package_txns1)
+ self.generate(node, 1)
+
+ def test_0fee_package_rbf(self):
+ self.log.info("Test package RBF: TRUC 0-fee parent + high-fee child replaces parent's conflicts")
+ node = self.nodes[0]
+ # Reuse the same coins so that the transactions conflict with one another.
+ self.wallet.rescan_utxos()
+ parent_coin = self.wallet.get_utxo(confirmed_only=True)
+
+ # package1 pays default fee on both transactions
+ parent1 = self.wallet.create_self_transfer(utxo_to_spend=parent_coin, version=3)
+ child1 = self.wallet.create_self_transfer(utxo_to_spend=parent1["new_utxo"], version=3)
+ package_hex1 = [parent1["hex"], child1["hex"]]
+ fees_package1 = parent1["fee"] + child1["fee"]
+ submitres1 = node.submitpackage(package_hex1)
+ assert_equal(submitres1["package_msg"], "success")
+ self.assert_mempool_contents([parent1["tx"], child1["tx"]])
+
+ # package2 has a 0-fee parent (conflicting with package1) and very high fee child
+ parent2 = self.wallet.create_self_transfer(utxo_to_spend=parent_coin, fee=0, fee_rate=0, version=3)
+ child2 = self.wallet.create_self_transfer(utxo_to_spend=parent2["new_utxo"], fee=fees_package1*10, version=3)
+ package_hex2 = [parent2["hex"], child2["hex"]]
+
+ submitres2 = node.submitpackage(package_hex2)
+ assert_equal(submitres2["package_msg"], "success")
+ assert_equal(set(submitres2["replaced-transactions"]), set([parent1["txid"], child1["txid"]]))
+ self.assert_mempool_contents([parent2["tx"], child2["tx"]])
+
+ self.generate(node, 1)
+
+ def test_child_conflicts_parent_mempool_ancestor(self):
+ fill_mempool(self, self.nodes[0])
+ # Reset coins since we filled the mempool with current coins
+ self.coins = self.wallet.get_utxos(mark_as_spent=False, confirmed_only=True)
+
+ self.log.info("Test that package RBF doesn't have issues with mempool<->package conflicts via inconsistency")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ self.ctr += 1
+ grandparent_result = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ node.sendrawtransaction(grandparent_result["hex"])
+
+ # Now make package of two descendants that looks
+ # like a cpfp where the parent can't get in on its own
+ self.ctr += 1
+ parent_result = self.wallet.create_self_transfer(
+ fee_rate=Decimal('0.00001000'),
+ utxo_to_spend=grandparent_result["new_utxo"],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+ # Last tx double-spends grandparent's coin,
+ # which is not inside the current package
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result["new_utxo"], coin],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ pkg_result = node.submitpackage([parent_result["hex"], child_result["hex"]])
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: new transaction cannot have mempool ancestors')
+ mempool_info = node.getrawmempool()
+ assert grandparent_result["txid"] in mempool_info
+ assert parent_result["txid"] not in mempool_info
+ assert child_result["txid"] not in mempool_info
+
+if __name__ == "__main__":
+ PackageRBFTest().main()
diff --git a/test/functional/mempool_accept_v3.py b/test/functional/mempool_truc.py
index 8285b82c19..e1f3d77201 100755
--- a/test/functional/mempool_accept_v3.py
+++ b/test/functional/mempool_truc.py
@@ -6,6 +6,7 @@ from decimal import Decimal
from test_framework.messages import (
MAX_BIP125_RBF_SEQUENCE,
+ WITNESS_SCALE_FACTOR,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -21,6 +22,7 @@ from test_framework.wallet import (
)
MAX_REPLACEMENT_CANDIDATES = 100
+TRUC_MAX_VSIZE = 10000
def cleanup(extra_args=None):
def decorator(func):
@@ -37,10 +39,10 @@ def cleanup(extra_args=None):
return wrapper
return decorator
-class MempoolAcceptV3(BitcoinTestFramework):
+class MempoolTRUC(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
- self.extra_args = [["-acceptnonstdtxn=1"]]
+ self.extra_args = [[]]
self.setup_clean_chain = True
def check_mempool(self, txids):
@@ -49,10 +51,24 @@ class MempoolAcceptV3(BitcoinTestFramework):
assert_equal(len(txids), len(mempool_contents))
assert all([txid in txids for txid in mempool_contents])
- @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
- def test_v3_acceptance(self):
+ @cleanup(extra_args=["-datacarriersize=20000"])
+ def test_truc_max_vsize(self):
node = self.nodes[0]
- self.log.info("Test a child of a v3 transaction cannot be more than 1000vB")
+ self.log.info("Test TRUC-specific maximum transaction vsize")
+ tx_v3_heavy = self.wallet.create_self_transfer(target_weight=(TRUC_MAX_VSIZE + 1) * WITNESS_SCALE_FACTOR, version=3)
+ assert_greater_than_or_equal(tx_v3_heavy["tx"].get_vsize(), TRUC_MAX_VSIZE)
+ expected_error_heavy = f"TRUC-violation, version=3 tx {tx_v3_heavy['txid']} (wtxid={tx_v3_heavy['wtxid']}) is too big"
+ assert_raises_rpc_error(-26, expected_error_heavy, node.sendrawtransaction, tx_v3_heavy["hex"])
+ self.check_mempool([])
+
+ # Ensure we are hitting the TRUC-specific limit and not something else
+ tx_v2_heavy = self.wallet.send_self_transfer(from_node=node, target_weight=(TRUC_MAX_VSIZE + 1) * WITNESS_SCALE_FACTOR, version=2)
+ self.check_mempool([tx_v2_heavy["txid"]])
+
+ @cleanup(extra_args=["-datacarriersize=1000"])
+ def test_truc_acceptance(self):
+ node = self.nodes[0]
+ self.log.info("Test a child of a TRUC transaction cannot be more than 1000vB")
tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3)
self.check_mempool([tx_v3_parent_normal["txid"]])
tx_v3_child_heavy = self.wallet.create_self_transfer(
@@ -61,13 +77,13 @@ class MempoolAcceptV3(BitcoinTestFramework):
version=3
)
assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), 1000)
- expected_error_child_heavy = f"v3-rule-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big"
+ expected_error_child_heavy = f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big"
assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"])
self.check_mempool([tx_v3_parent_normal["txid"]])
# tx has no descendants
assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1)
- self.log.info("Test that, during replacements, only the new transaction counts for v3 descendant limit")
+ self.log.info("Test that, during replacements, only the new transaction counts for TRUC descendant limit")
tx_v3_child_almost_heavy = self.wallet.send_self_transfer(
from_node=node,
fee_rate=DEFAULT_FEE,
@@ -89,10 +105,10 @@ class MempoolAcceptV3(BitcoinTestFramework):
self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]])
assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
- def test_v3_replacement(self):
+ @cleanup(extra_args=None)
+ def test_truc_replacement(self):
node = self.nodes[0]
- self.log.info("Test v3 transactions may be replaced by v3 transactions")
+ self.log.info("Test TRUC transactions may be replaced by TRUC transactions")
utxo_v3_bip125 = self.wallet.get_utxo()
tx_v3_bip125 = self.wallet.send_self_transfer(
from_node=node,
@@ -111,7 +127,7 @@ class MempoolAcceptV3(BitcoinTestFramework):
)
self.check_mempool([tx_v3_bip125_rbf["txid"]])
- self.log.info("Test v3 transactions may be replaced by V2 transactions")
+ self.log.info("Test TRUC transactions may be replaced by non-TRUC (BIP125) transactions")
tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer(
from_node=node,
fee_rate=DEFAULT_FEE * 3,
@@ -120,7 +136,7 @@ class MempoolAcceptV3(BitcoinTestFramework):
)
self.check_mempool([tx_v3_bip125_rbf_v2["txid"]])
- self.log.info("Test that replacements cannot cause violation of inherited v3")
+ self.log.info("Test that replacements cannot cause violation of inherited TRUC")
utxo_v3_parent = self.wallet.get_utxo()
tx_v3_parent = self.wallet.send_self_transfer(
from_node=node,
@@ -141,15 +157,15 @@ class MempoolAcceptV3(BitcoinTestFramework):
utxo_to_spend=tx_v3_parent["new_utxo"],
version=2
)
- expected_error_v2_v3 = f"v3-rule-violation, non-v3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})"
+ expected_error_v2_v3 = f"TRUC-violation, non-version=3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})"
assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"])
self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
- def test_v3_bip125(self):
+ @cleanup(extra_args=None)
+ def test_truc_bip125(self):
node = self.nodes[0]
- self.log.info("Test v3 transactions that don't signal BIP125 are replaceable")
+ self.log.info("Test TRUC transactions that don't signal BIP125 are replaceable")
assert_equal(node.getmempoolinfo()["fullrbf"], False)
utxo_v3_no_bip125 = self.wallet.get_utxo()
tx_v3_no_bip125 = self.wallet.send_self_transfer(
@@ -170,10 +186,10 @@ class MempoolAcceptV3(BitcoinTestFramework):
)
self.check_mempool([tx_v3_no_bip125_rbf["txid"]])
- @cleanup(extra_args=["-datacarriersize=40000", "-acceptnonstdtxn=1"])
- def test_v3_reorg(self):
+ @cleanup(extra_args=["-datacarriersize=40000"])
+ def test_truc_reorg(self):
node = self.nodes[0]
- self.log.info("Test that, during a reorg, v3 rules are not enforced")
+ self.log.info("Test that, during a reorg, TRUC rules are not enforced")
tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2)
tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3)
tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3)
@@ -192,36 +208,62 @@ class MempoolAcceptV3(BitcoinTestFramework):
node.reconsiderblock(block[0])
- @cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"])
+ @cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000"])
def test_nondefault_package_limits(self):
"""
- Max standard tx size + v3 rules imply the ancestor/descendant rules (at their default
+ Max standard tx size + TRUC rules imply the ancestor/descendant rules (at their default
values), but those checks must not be skipped. Ensure both sets of checks are done by
changing the ancestor/descendant limit configurations.
"""
node = self.nodes[0]
- self.log.info("Test that a decreased limitdescendantsize also applies to v3 child")
- tx_v3_parent_large1 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3)
- tx_v3_child_large1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large1["new_utxo"], version=3)
- # Child is within v3 limits, but parent's descendant limit is exceeded
- assert_greater_than(1000, tx_v3_child_large1["tx"].get_vsize())
+ self.log.info("Test that a decreased limitdescendantsize also applies to TRUC child")
+ parent_target_weight = 9990 * WITNESS_SCALE_FACTOR
+ child_target_weight = 500 * WITNESS_SCALE_FACTOR
+ tx_v3_parent_large1 = self.wallet.send_self_transfer(
+ from_node=node,
+ target_weight=parent_target_weight,
+ version=3
+ )
+ tx_v3_child_large1 = self.wallet.create_self_transfer(
+ utxo_to_spend=tx_v3_parent_large1["new_utxo"],
+ target_weight=child_target_weight,
+ version=3
+ )
+
+ # Parent and child are within v3 limits, but parent's 10kvB descendant limit is exceeded
+ assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large1["tx"].get_vsize())
+ assert_greater_than_or_equal(1000, tx_v3_child_large1["tx"].get_vsize())
+ assert_greater_than(tx_v3_parent_large1["tx"].get_vsize() + tx_v3_child_large1["tx"].get_vsize(), 10000)
+
assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds descendant size limit for tx {tx_v3_parent_large1['txid']}", node.sendrawtransaction, tx_v3_child_large1["hex"])
self.check_mempool([tx_v3_parent_large1["txid"]])
assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1)
self.generate(node, 1)
self.log.info("Test that a decreased limitancestorsize also applies to v3 parent")
- self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000", "-acceptnonstdtxn=1"])
- tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3)
- tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], version=3)
- # Child is within v3 limits
+ self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000"])
+ tx_v3_parent_large2 = self.wallet.send_self_transfer(
+ from_node=node,
+ target_weight=parent_target_weight,
+ version=3
+ )
+ tx_v3_child_large2 = self.wallet.create_self_transfer(
+ utxo_to_spend=tx_v3_parent_large2["new_utxo"],
+ target_weight=child_target_weight,
+ version=3
+ )
+
+ # Parent and child are within TRUC limits
+ assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large2["tx"].get_vsize())
assert_greater_than_or_equal(1000, tx_v3_child_large2["tx"].get_vsize())
+ assert_greater_than(tx_v3_parent_large2["tx"].get_vsize() + tx_v3_child_large2["tx"].get_vsize(), 10000)
+
assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds ancestor size limit", node.sendrawtransaction, tx_v3_child_large2["hex"])
self.check_mempool([tx_v3_parent_large2["txid"]])
- @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
- def test_v3_ancestors_package(self):
- self.log.info("Test that v3 ancestor limits are checked within the package")
+ @cleanup(extra_args=["-datacarriersize=1000"])
+ def test_truc_ancestors_package(self):
+ self.log.info("Test that TRUC ancestor limits are checked within the package")
node = self.nodes[0]
tx_v3_parent_normal = self.wallet.create_self_transfer(
fee_rate=0,
@@ -247,34 +289,34 @@ class MempoolAcceptV3(BitcoinTestFramework):
self.check_mempool([])
result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]])
- assert_equal(result['package_msg'], f"v3-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors")
+ assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors")
self.check_mempool([])
self.check_mempool([])
result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]])
# tx_v3_child_heavy is heavy based on weight, not sigops.
- assert_equal(result['package_msg'], f"v3-violation, v3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes")
+ assert_equal(result['package_msg'], f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes")
self.check_mempool([])
tx_v3_parent = self.wallet.create_self_transfer(version=3)
tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3)
tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3)
result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]])
- assert all([txresult["package-error"] == f"v3-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors" for txresult in result])
+ assert all([txresult["package-error"] == f"TRUC-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors" for txresult in result])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
- def test_v3_ancestors_package_and_mempool(self):
+ @cleanup(extra_args=None)
+ def test_truc_ancestors_package_and_mempool(self):
"""
- A v3 transaction in a package cannot have 2 v3 parents.
+ A TRUC transaction in a package cannot have 2 TRUC parents.
Test that if we have a transaction graph A -> B -> C, where A, B, C are
- all v3 transactions, that we cannot use submitpackage to get the
+ all TRUC transactions, that we cannot use submitpackage to get the
transactions all into the mempool.
Verify, in particular, that if A is already in the mempool, then
submitpackage(B, C) will fail.
"""
node = self.nodes[0]
- self.log.info("Test that v3 ancestor limits include transactions within the package and all in-mempool ancestors")
+ self.log.info("Test that TRUC ancestor limits include transactions within the package and all in-mempool ancestors")
# This is our transaction "A":
tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3)
@@ -289,17 +331,17 @@ class MempoolAcceptV3(BitcoinTestFramework):
# submitpackage(B, C) should fail
result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]])
- assert_equal(result['package_msg'], f"v3-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors")
+ assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors")
self.check_mempool([tx_in_mempool["txid"]])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
+ @cleanup(extra_args=None)
def test_sibling_eviction_package(self):
"""
When a transaction has a mempool sibling, it may be eligible for sibling eviction.
However, this option is only available in single transaction acceptance. It doesn't work in
a multi-testmempoolaccept (where RBF is disabled) or when doing package CPFP.
"""
- self.log.info("Test v3 sibling eviction in submitpackage and multi-testmempoolaccept")
+ self.log.info("Test TRUC sibling eviction in submitpackage and multi-testmempoolaccept")
node = self.nodes[0]
# Add a parent + child to mempool
tx_mempool_parent = self.wallet.send_self_transfer_multi(
@@ -342,17 +384,17 @@ class MempoolAcceptV3(BitcoinTestFramework):
# Fails with another non-related transaction via testmempoolaccept
tx_unrelated = self.wallet.create_self_transfer(version=3)
result_test_unrelated = node.testmempoolaccept([tx_sibling_1["hex"], tx_unrelated["hex"]])
- assert_equal(result_test_unrelated[0]["reject-reason"], "v3-rule-violation")
+ assert_equal(result_test_unrelated[0]["reject-reason"], "TRUC-violation")
# Fails in a package via testmempoolaccept
result_test_1p1c = node.testmempoolaccept([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
- assert_equal(result_test_1p1c[0]["reject-reason"], "v3-rule-violation")
+ assert_equal(result_test_1p1c[0]["reject-reason"], "TRUC-violation")
# Allowed when tx is submitted in a package and evaluated individually.
# Note that the child failed since it would be the 3rd generation.
result_package_indiv = node.submitpackage([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
self.check_mempool([tx_mempool_parent["txid"], tx_sibling_1["txid"]])
- expected_error_gen3 = f"v3-rule-violation, tx {tx_has_mempool_uncle['txid']} (wtxid={tx_has_mempool_uncle['wtxid']}) would have too many ancestors"
+ expected_error_gen3 = f"TRUC-violation, tx {tx_has_mempool_uncle['txid']} (wtxid={tx_has_mempool_uncle['wtxid']}) would have too many ancestors"
assert_equal(result_package_indiv["tx-results"][tx_has_mempool_uncle['wtxid']]['error'], expected_error_gen3)
@@ -360,17 +402,17 @@ class MempoolAcceptV3(BitcoinTestFramework):
node.submitpackage([tx_mempool_parent["hex"], tx_sibling_2["hex"]])
self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])
- # Child cannot pay for sibling eviction for parent, as it violates v3 topology limits
+ # Child cannot pay for sibling eviction for parent, as it violates TRUC topology limits
result_package_cpfp = node.submitpackage([tx_sibling_3["hex"], tx_bumps_parent_with_sibling["hex"]])
self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])
- expected_error_cpfp = f"v3-rule-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit"
+ expected_error_cpfp = f"TRUC-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit"
assert_equal(result_package_cpfp["tx-results"][tx_sibling_3['wtxid']]['error'], expected_error_cpfp)
- @cleanup(extra_args=["-datacarriersize=1000", "-acceptnonstdtxn=1"])
- def test_v3_package_inheritance(self):
- self.log.info("Test that v3 inheritance is checked within package")
+ @cleanup(extra_args=["-datacarriersize=1000"])
+ def test_truc_package_inheritance(self):
+ self.log.info("Test that TRUC inheritance is checked within package")
node = self.nodes[0]
tx_v3_parent = self.wallet.create_self_transfer(
fee_rate=0,
@@ -384,14 +426,14 @@ class MempoolAcceptV3(BitcoinTestFramework):
)
self.check_mempool([])
result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]])
- assert_equal(result['package_msg'], f"v3-violation, non-v3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from v3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})")
+ assert_equal(result['package_msg'], f"TRUC-violation, non-version=3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})")
self.check_mempool([])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
- def test_v3_in_testmempoolaccept(self):
+ @cleanup(extra_args=None)
+ def test_truc_in_testmempoolaccept(self):
node = self.nodes[0]
- self.log.info("Test that v3 inheritance is accurately assessed in testmempoolaccept")
+ self.log.info("Test that TRUC inheritance is accurately assessed in testmempoolaccept")
tx_v2 = self.wallet.create_self_transfer(version=2)
tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2)
tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3)
@@ -405,11 +447,11 @@ class MempoolAcceptV3(BitcoinTestFramework):
assert all([result["allowed"] for result in test_accept_v2_and_v3])
test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]])
- expected_error_v3_from_v2 = f"v3-violation, v3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-v3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})"
+ expected_error_v3_from_v2 = f"TRUC-violation, version=3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-version=3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})"
assert all([result["package-error"] == expected_error_v3_from_v2 for result in test_accept_v3_from_v2])
test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]])
- expected_error_v2_from_v3 = f"v3-violation, non-v3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from v3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})"
+ expected_error_v2_from_v3 = f"TRUC-violation, non-version=3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from version=3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})"
assert all([result["package-error"] == expected_error_v2_from_v3 for result in test_accept_v2_from_v3])
test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]])
@@ -421,26 +463,26 @@ class MempoolAcceptV3(BitcoinTestFramework):
tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3)
tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3)
test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
- expected_error_2children = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
+ expected_error_2children = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
assert all([result["package-error"] == expected_error_2children for result in test_accept_2children])
- # Extra v3 transaction does not get incorrectly marked as extra descendant
+ # Extra TRUC transaction does not get incorrectly marked as extra descendant
test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]])
assert all([result["allowed"] for result in test_accept_1child_with_exra])
- # Extra v3 transaction does not make us ignore the extra descendant
+ # Extra TRUC transaction does not make us ignore the extra descendant
test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]])
- expected_error_extra = f"v3-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
+ expected_error_extra = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_exra])
# Same result if the parent is already in mempool
node.sendrawtransaction(tx_v3_parent["hex"])
test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_in_mempool_parent])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
+ @cleanup(extra_args=None)
def test_reorg_2child_rbf(self):
node = self.nodes[0]
- self.log.info("Test that children of a v3 transaction can be replaced individually, even if there are multiple due to reorg")
+ self.log.info("Test that children of a TRUC transaction can be replaced individually, even if there are multiple due to reorg")
ancestor_tx = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
self.check_mempool([ancestor_tx["txid"]])
@@ -468,9 +510,9 @@ class MempoolAcceptV3(BitcoinTestFramework):
self.check_mempool([ancestor_tx["txid"], child_1_conflict["txid"], child_2["txid"]])
assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3)
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
- def test_v3_sibling_eviction(self):
- self.log.info("Test sibling eviction for v3")
+ @cleanup(extra_args=None)
+ def test_truc_sibling_eviction(self):
+ self.log.info("Test sibling eviction for TRUC")
node = self.nodes[0]
tx_v3_parent = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
# This is the sibling to replace
@@ -541,7 +583,7 @@ class MempoolAcceptV3(BitcoinTestFramework):
node.sendrawtransaction(tx_v3_child_3["hex"])
self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_3["txid"]])
- @cleanup(extra_args=["-acceptnonstdtxn=1"])
+ @cleanup(extra_args=None)
def test_reorg_sibling_eviction_1p2c(self):
node = self.nodes[0]
self.log.info("Test that sibling eviction is not allowed when multiple siblings exist")
@@ -567,7 +609,7 @@ class MempoolAcceptV3(BitcoinTestFramework):
utxo_to_spend=tx_with_multi_children["new_utxos"][2],
fee_rate=DEFAULT_FEE*50
)
- expected_error_2siblings = f"v3-rule-violation, tx {tx_with_multi_children['txid']} (wtxid={tx_with_multi_children['wtxid']}) would exceed descendant count limit"
+ expected_error_2siblings = f"TRUC-violation, tx {tx_with_multi_children['txid']} (wtxid={tx_with_multi_children['wtxid']}) would exceed descendant count limit"
assert_raises_rpc_error(-26, expected_error_2siblings, node.sendrawtransaction, tx_with_sibling3["hex"])
# However, an RBF (with conflicting inputs) is possible even if the resulting cluster size exceeds 2
@@ -585,20 +627,21 @@ class MempoolAcceptV3(BitcoinTestFramework):
node = self.nodes[0]
self.wallet = MiniWallet(node)
self.generate(self.wallet, 120)
- self.test_v3_acceptance()
- self.test_v3_replacement()
- self.test_v3_bip125()
- self.test_v3_reorg()
+ self.test_truc_max_vsize()
+ self.test_truc_acceptance()
+ self.test_truc_replacement()
+ self.test_truc_bip125()
+ self.test_truc_reorg()
self.test_nondefault_package_limits()
- self.test_v3_ancestors_package()
- self.test_v3_ancestors_package_and_mempool()
+ self.test_truc_ancestors_package()
+ self.test_truc_ancestors_package_and_mempool()
self.test_sibling_eviction_package()
- self.test_v3_package_inheritance()
- self.test_v3_in_testmempoolaccept()
+ self.test_truc_package_inheritance()
+ self.test_truc_in_testmempoolaccept()
self.test_reorg_2child_rbf()
- self.test_v3_sibling_eviction()
+ self.test_truc_sibling_eviction()
self.test_reorg_sibling_eviction_1p2c()
if __name__ == "__main__":
- MempoolAcceptV3().main()
+ MempoolTRUC().main()
diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py
index b23ec1028b..d10e47e036 100755
--- a/test/functional/p2p_addr_relay.py
+++ b/test/functional/p2p_addr_relay.py
@@ -142,7 +142,8 @@ class AddrTest(BitcoinTestFramework):
msg = self.setup_addr_msg(1010)
with self.nodes[0].assert_debug_log(['addr message size = 1010']):
- addr_source.send_and_ping(msg)
+ addr_source.send_message(msg)
+ addr_source.wait_for_disconnect()
self.nodes[0].disconnect_p2ps()
diff --git a/test/functional/p2p_addrv2_relay.py b/test/functional/p2p_addrv2_relay.py
index ea114e7d70..4ec8e0bc04 100755
--- a/test/functional/p2p_addrv2_relay.py
+++ b/test/functional/p2p_addrv2_relay.py
@@ -86,11 +86,6 @@ class AddrTest(BitcoinTestFramework):
addr_source = self.nodes[0].add_p2p_connection(P2PInterface())
msg = msg_addrv2()
- self.log.info('Send too-large addrv2 message')
- msg.addrs = ADDRS * 101
- with self.nodes[0].assert_debug_log(['addrv2 message size = 1010']):
- addr_source.send_and_ping(msg)
-
self.log.info('Check that addrv2 message content is relayed and added to addrman')
addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
msg.addrs = ADDRS
@@ -106,6 +101,13 @@ class AddrTest(BitcoinTestFramework):
assert addr_receiver.addrv2_received_and_checked
assert_equal(len(self.nodes[0].getnodeaddresses(count=0, network="i2p")), 0)
+ self.log.info('Send too-large addrv2 message')
+ msg.addrs = ADDRS * 101
+ with self.nodes[0].assert_debug_log(['addrv2 message size = 1010']):
+ addr_source.send_message(msg)
+ addr_source.wait_for_disconnect()
+
+
if __name__ == '__main__':
AddrTest().main()
diff --git a/test/functional/p2p_disconnect_ban.py b/test/functional/p2p_disconnect_ban.py
index 678b006886..e47f9c732b 100755
--- a/test/functional/p2p_disconnect_ban.py
+++ b/test/functional/p2p_disconnect_ban.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# Copyright (c) 2014-2022 The Bitcoin Core developers
+# Copyright (c) 2014-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test node disconnect and ban behavior"""
@@ -18,7 +18,7 @@ class DisconnectBanTest(BitcoinTestFramework):
self.supports_cli = False
def run_test(self):
- self.log.info("Connect nodes both way")
+ self.log.info("Connect nodes both ways")
# By default, the test framework sets up an addnode connection from
# node 1 --> node0. By connecting node0 --> node 1, we're left with
# the two nodes being connected both ways.
@@ -84,7 +84,7 @@ class DisconnectBanTest(BitcoinTestFramework):
assert_equal("192.168.0.1/32", listBeforeShutdown[2]['address'])
self.log.info("setban: test banning with absolute timestamp")
- self.nodes[1].setban("192.168.0.2", "add", old_time + 120, True)
+ self.nodes[1].setban("192.168.0.2", "add", old_time + 120, absolute=True)
# Move time forward by 3 seconds so the fourth ban has expired
self.nodes[1].setmocktime(old_time + 3)
@@ -102,7 +102,9 @@ class DisconnectBanTest(BitcoinTestFramework):
assert_equal(ban["ban_duration"], 120)
assert_equal(ban["time_remaining"], 117)
- self.restart_node(1)
+ # Keep mocktime, to avoid ban expiry when restart takes longer than
+ # time_remaining
+ self.restart_node(1, extra_args=[f"-mocktime={old_time+4}"])
listAfterShutdown = self.nodes[1].listbanned()
assert_equal("127.0.0.0/24", listAfterShutdown[0]['address'])
@@ -113,7 +115,7 @@ class DisconnectBanTest(BitcoinTestFramework):
# Clear ban lists
self.nodes[1].clearbanned()
- self.log.info("Connect nodes both way")
+ self.log.info("Connect nodes both ways")
self.connect_nodes(0, 1)
self.connect_nodes(1, 0)
diff --git a/test/functional/p2p_handshake.py b/test/functional/p2p_handshake.py
index dd19fe9333..21959ae522 100755
--- a/test/functional/p2p_handshake.py
+++ b/test/functional/p2p_handshake.py
@@ -88,6 +88,10 @@ class P2PHandshakeTest(BitcoinTestFramework):
with node.assert_debug_log([f"feeler connection completed"]):
self.add_outbound_connection(node, "feeler", NODE_NONE, wait_for_disconnect=True)
+ # TODO: re-add test introduced in commit 5d2fb14bafe4e80c0a482d99e5ebde07c477f000
+ # ("test: p2p: check that connecting to ourself leads to disconnect") once
+ # the race condition causing issue #30368 is fixed
+
if __name__ == '__main__':
P2PHandshakeTest().main()
diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py
index 40a69936bc..8e459ba676 100755
--- a/test/functional/p2p_invalid_messages.py
+++ b/test/functional/p2p_invalid_messages.py
@@ -5,7 +5,6 @@
"""Test node responses to invalid network messages."""
import random
-import struct
import time
from test_framework.messages import (
@@ -233,7 +232,7 @@ class InvalidMessagesTest(BitcoinTestFramework):
'208d')) # port
def test_addrv2_unrecognized_network(self):
- now_hex = struct.pack('<I', int(time.time())).hex()
+ now_hex = int(time.time()).to_bytes(4, "little").hex()
self.test_addrv2('unrecognized network',
[
'received: addrv2 (25 bytes)',
@@ -261,7 +260,9 @@ class InvalidMessagesTest(BitcoinTestFramework):
msg_type = msg.msgtype.decode('ascii')
self.log.info("Test {} message of size {} is logged as misbehaving".format(msg_type, size))
with self.nodes[0].assert_debug_log(['Misbehaving', '{} message size = {}'.format(msg_type, size)]):
- self.nodes[0].add_p2p_connection(P2PInterface()).send_and_ping(msg)
+ conn = self.nodes[0].add_p2p_connection(P2PInterface())
+ conn.send_message(msg)
+ conn.wait_for_disconnect()
self.nodes[0].disconnect_p2ps()
def test_oversized_inv_msg(self):
@@ -322,7 +323,8 @@ class InvalidMessagesTest(BitcoinTestFramework):
# delete arbitrary block header somewhere in the middle to break link
del block_headers[random.randrange(1, len(block_headers)-1)]
with self.nodes[0].assert_debug_log(expected_msgs=MISBEHAVING_NONCONTINUOUS_HEADERS_MSGS):
- peer.send_and_ping(msg_headers(block_headers))
+ peer.send_message(msg_headers(block_headers))
+ peer.wait_for_disconnect()
self.nodes[0].disconnect_p2ps()
def test_resource_exhaustion(self):
diff --git a/test/functional/p2p_mutated_blocks.py b/test/functional/p2p_mutated_blocks.py
index 737edaf5bf..708b19b1e5 100755
--- a/test/functional/p2p_mutated_blocks.py
+++ b/test/functional/p2p_mutated_blocks.py
@@ -55,7 +55,7 @@ class MutatedBlocksTest(BitcoinTestFramework):
# Create mutated version of the block by changing the transaction
# version on the self-transfer.
mutated_block = copy.deepcopy(block)
- mutated_block.vtx[1].nVersion = 4
+ mutated_block.vtx[1].version = 4
# Announce the new block via a compact block through the honest relayer
cmpctblock = HeaderAndShortIDs()
@@ -104,11 +104,10 @@ class MutatedBlocksTest(BitcoinTestFramework):
block_missing_prev.hashPrevBlock = 123
block_missing_prev.solve()
- # Attacker gets a DoS score of 10, not immediately disconnected, so we do it 10 times to get to 100
- for _ in range(10):
- assert_equal(len(self.nodes[0].getpeerinfo()), 2)
- with self.nodes[0].assert_debug_log(expected_msgs=["AcceptBlock FAILED (prev-blk-not-found)"]):
- attacker.send_message(msg_block(block_missing_prev))
+ # Check that non-connecting block causes disconnect
+ assert_equal(len(self.nodes[0].getpeerinfo()), 2)
+ with self.nodes[0].assert_debug_log(expected_msgs=["AcceptBlock FAILED (prev-blk-not-found)"]):
+ attacker.send_message(msg_block(block_missing_prev))
attacker.wait_for_disconnect(timeout=5)
diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py
index 45bbd7f1c3..d20cf41a72 100755
--- a/test/functional/p2p_segwit.py
+++ b/test/functional/p2p_segwit.py
@@ -5,7 +5,6 @@
"""Test segwit transactions and blocks on P2P network."""
from decimal import Decimal
import random
-import struct
import time
from test_framework.blocktools import (
@@ -1165,16 +1164,16 @@ class SegWitTest(BitcoinTestFramework):
if not self.wit.is_null():
flags |= 1
r = b""
- r += struct.pack("<i", self.nVersion)
+ r += self.version.to_bytes(4, "little")
if flags:
dummy = []
r += ser_vector(dummy)
- r += struct.pack("<B", flags)
+ r += flags.to_bytes(1, "little")
r += ser_vector(self.vin)
r += ser_vector(self.vout)
if flags & 1:
r += self.wit.serialize()
- r += struct.pack("<I", self.nLockTime)
+ r += self.nLockTime.to_bytes(4, "little")
return r
tx2 = BrokenCTransaction()
@@ -1976,11 +1975,11 @@ class SegWitTest(BitcoinTestFramework):
def serialize_with_bogus_witness(tx):
flags = 3
r = b""
- r += struct.pack("<i", tx.nVersion)
+ r += tx.version.to_bytes(4, "little")
if flags:
dummy = []
r += ser_vector(dummy)
- r += struct.pack("<B", flags)
+ r += flags.to_bytes(1, "little")
r += ser_vector(tx.vin)
r += ser_vector(tx.vout)
if flags & 1:
@@ -1990,7 +1989,7 @@ class SegWitTest(BitcoinTestFramework):
for _ in range(len(tx.wit.vtxinwit), len(tx.vin)):
tx.wit.vtxinwit.append(CTxInWitness())
r += tx.wit.serialize()
- r += struct.pack("<I", tx.nLockTime)
+ r += tx.nLockTime.to_bytes(4, "little")
return r
class msg_bogus_tx(msg_tx):
diff --git a/test/functional/p2p_sendheaders.py b/test/functional/p2p_sendheaders.py
index 27a3aa8fb9..5c463267a1 100755
--- a/test/functional/p2p_sendheaders.py
+++ b/test/functional/p2p_sendheaders.py
@@ -71,19 +71,13 @@ f. Announce 1 more header that builds on that fork.
Expect: no response.
Part 5: Test handling of headers that don't connect.
-a. Repeat 10 times:
+a. Repeat 100 times:
1. Announce a header that doesn't connect.
Expect: getheaders message
2. Send headers chain.
Expect: getdata for the missing blocks, tip update.
-b. Then send 9 more headers that don't connect.
+b. Then send 99 more headers that don't connect.
Expect: getheaders message each time.
-c. Announce a header that does connect.
- Expect: no response.
-d. Announce 49 headers that don't connect.
- Expect: getheaders message each time.
-e. Announce one more that doesn't connect.
- Expect: disconnect.
"""
from test_framework.blocktools import create_block, create_coinbase
from test_framework.messages import CInv
@@ -526,7 +520,8 @@ class SendHeadersTest(BitcoinTestFramework):
# First we test that receipt of an unconnecting header doesn't prevent
# chain sync.
expected_hash = tip
- for i in range(10):
+ NUM_HEADERS = 100
+ for i in range(NUM_HEADERS):
self.log.debug("Part 5.{}: starting...".format(i))
test_node.last_message.pop("getdata", None)
blocks = []
@@ -550,41 +545,24 @@ class SendHeadersTest(BitcoinTestFramework):
blocks = []
# Now we test that if we repeatedly don't send connecting headers, we
# don't go into an infinite loop trying to get them to connect.
- MAX_NUM_UNCONNECTING_HEADERS_MSGS = 10
- for _ in range(MAX_NUM_UNCONNECTING_HEADERS_MSGS + 1):
+ for _ in range(NUM_HEADERS + 1):
blocks.append(create_block(tip, create_coinbase(height), block_time))
blocks[-1].solve()
tip = blocks[-1].sha256
block_time += 1
height += 1
- for i in range(1, MAX_NUM_UNCONNECTING_HEADERS_MSGS):
- # Send a header that doesn't connect, check that we get a getheaders.
+ for i in range(1, NUM_HEADERS):
+ with p2p_lock:
+ test_node.last_message.pop("getheaders", None)
+ # Send an empty header as a failed response to the received getheaders
+ # (from the previous iteration). Otherwise, the new headers will be
+ # treated as a response instead of as an announcement.
+ test_node.send_header_for_blocks([])
+ # Send the actual unconnecting header, which should trigger a new getheaders.
test_node.send_header_for_blocks([blocks[i]])
test_node.wait_for_getheaders(block_hash=expected_hash)
- # Next header will connect, should re-set our count:
- test_node.send_header_for_blocks([blocks[0]])
- expected_hash = blocks[0].sha256
-
- # Remove the first two entries (blocks[1] would connect):
- blocks = blocks[2:]
-
- # Now try to see how many unconnecting headers we can send
- # before we get disconnected. Should be 5*MAX_NUM_UNCONNECTING_HEADERS_MSGS
- for i in range(5 * MAX_NUM_UNCONNECTING_HEADERS_MSGS - 1):
- # Send a header that doesn't connect, check that we get a getheaders.
- test_node.send_header_for_blocks([blocks[i % len(blocks)]])
- test_node.wait_for_getheaders(block_hash=expected_hash)
-
- # Eventually this stops working.
- test_node.send_header_for_blocks([blocks[-1]])
-
- # Should get disconnected
- test_node.wait_for_disconnect()
-
- self.log.info("Part 5: success!")
-
# Finally, check that the inv node never received a getdata request,
# throughout the test
assert "getdata" not in inv_node.last_message
diff --git a/test/functional/p2p_unrequested_blocks.py b/test/functional/p2p_unrequested_blocks.py
index f368434895..776eaf5255 100755
--- a/test/functional/p2p_unrequested_blocks.py
+++ b/test/functional/p2p_unrequested_blocks.py
@@ -170,9 +170,11 @@ class AcceptBlockTest(BitcoinTestFramework):
tip = next_block
# Now send the block at height 5 and check that it wasn't accepted (missing header)
- test_node.send_and_ping(msg_block(all_blocks[1]))
+ test_node.send_message(msg_block(all_blocks[1]))
+ test_node.wait_for_disconnect()
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblock, all_blocks[1].hash)
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblockheader, all_blocks[1].hash)
+ test_node = self.nodes[0].add_p2p_connection(P2PInterface())
# The block at height 5 should be accepted if we provide the missing header, though
headers_message = msg_headers()
diff --git a/test/functional/p2p_v2_earlykeyresponse.py b/test/functional/p2p_v2_earlykeyresponse.py
deleted file mode 100755
index 32d2e1148a..0000000000
--- a/test/functional/p2p_v2_earlykeyresponse.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) 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.
-
-import random
-
-from test_framework.test_framework import BitcoinTestFramework
-from test_framework.crypto.ellswift import ellswift_create
-from test_framework.p2p import P2PInterface
-from test_framework.v2_p2p import EncryptedP2PState
-
-
-class TestEncryptedP2PState(EncryptedP2PState):
- """ Modify v2 P2P protocol functions for testing that "The responder waits until one byte is received which does
- not match the 16 bytes consisting of the network magic followed by "version\x00\x00\x00\x00\x00"." (see BIP 324)
-
- - if `send_net_magic` is True, send first 4 bytes of ellswift (match network magic) else send remaining 60 bytes
- - `can_data_be_received` is a variable used to assert if data is received on recvbuf.
- - v2 TestNode shouldn't respond back if we send V1_PREFIX and data shouldn't be received on recvbuf.
- This state is represented using `can_data_be_received` = False.
- - v2 TestNode responds back when mismatch from V1_PREFIX happens and data can be received on recvbuf.
- This state is represented using `can_data_be_received` = True.
- """
-
- def __init__(self):
- super().__init__(initiating=True, net='regtest')
- self.send_net_magic = True
- self.can_data_be_received = False
-
- def initiate_v2_handshake(self, garbage_len=random.randrange(4096)):
- """Initiator begins the v2 handshake by sending its ellswift bytes and garbage.
- Here, the 64 bytes ellswift is assumed to have it's 4 bytes match network magic bytes. It is sent in 2 phases:
- 1. when `send_network_magic` = True, send first 4 bytes of ellswift (matches network magic bytes)
- 2. when `send_network_magic` = False, send remaining 60 bytes of ellswift
- """
- if self.send_net_magic:
- self.privkey_ours, self.ellswift_ours = ellswift_create()
- self.sent_garbage = random.randbytes(garbage_len)
- self.send_net_magic = False
- return b"\xfa\xbf\xb5\xda"
- else:
- self.can_data_be_received = True
- return self.ellswift_ours[4:] + self.sent_garbage
-
-
-class PeerEarlyKey(P2PInterface):
- """Custom implementation of P2PInterface which uses modified v2 P2P protocol functions for testing purposes."""
- def __init__(self):
- super().__init__()
- self.v2_state = None
- self.connection_opened = False
-
- def connection_made(self, transport):
- """64 bytes ellswift is sent in 2 parts during `initial_v2_handshake()`"""
- self.v2_state = TestEncryptedP2PState()
- super().connection_made(transport)
-
- def data_received(self, t):
- # check that data can be received on recvbuf only when mismatch from V1_PREFIX happens (send_net_magic = False)
- assert self.v2_state.can_data_be_received and not self.v2_state.send_net_magic
-
- def on_open(self):
- self.connection_opened = True
-
-class P2PEarlyKey(BitcoinTestFramework):
- def set_test_params(self):
- self.num_nodes = 1
- self.extra_args = [["-v2transport=1", "-peertimeout=3"]]
-
- def run_test(self):
- self.log.info('Sending ellswift bytes in parts to ensure that response from responder is received only when')
- self.log.info('ellswift bytes have a mismatch from the 16 bytes(network magic followed by "version\\x00\\x00\\x00\\x00\\x00")')
- node0 = self.nodes[0]
- self.log.info('Sending first 4 bytes of ellswift which match network magic')
- self.log.info('If a response is received, assertion failure would happen in our custom data_received() function')
- # send happens in `initiate_v2_handshake()` in `connection_made()`
- peer1 = node0.add_p2p_connection(PeerEarlyKey(), wait_for_verack=False, send_version=False, supports_v2_p2p=True, wait_for_v2_handshake=False)
- self.wait_until(lambda: peer1.connection_opened)
- self.log.info('Sending remaining ellswift and garbage which are different from V1_PREFIX. Since a response is')
- self.log.info('expected now, our custom data_received() function wouldn\'t result in assertion failure')
- ellswift_and_garbage_data = peer1.v2_state.initiate_v2_handshake()
- peer1.send_raw_message(ellswift_and_garbage_data)
- peer1.wait_for_disconnect(timeout=5)
- self.log.info('successful disconnection when MITM happens in the key exchange phase')
-
-
-if __name__ == '__main__':
- P2PEarlyKey().main()
diff --git a/test/functional/p2p_v2_misbehaving.py b/test/functional/p2p_v2_misbehaving.py
new file mode 100755
index 0000000000..e45a63b3b0
--- /dev/null
+++ b/test/functional/p2p_v2_misbehaving.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# Copyright (c) 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.
+
+import random
+from enum import Enum
+
+from test_framework.messages import MAGIC_BYTES
+from test_framework.p2p import P2PInterface
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import random_bitflip
+from test_framework.v2_p2p import (
+ EncryptedP2PState,
+ MAX_GARBAGE_LEN,
+)
+
+
+class TestType(Enum):
+ """ Scenarios to be tested:
+
+ 1. EARLY_KEY_RESPONSE - The responder needs to wait until one byte is received which does not match the 16 bytes
+ consisting of network magic followed by "version\x00\x00\x00\x00\x00" before sending out its ellswift + garbage bytes
+ 2. EXCESS_GARBAGE - Disconnection happens when > MAX_GARBAGE_LEN bytes garbage is sent
+ 3. WRONG_GARBAGE_TERMINATOR - Disconnection happens when incorrect garbage terminator is sent
+ 4. WRONG_GARBAGE - Disconnection happens when garbage bytes that is sent is different from what the peer receives
+ 5. SEND_NO_AAD - Disconnection happens when AAD of first encrypted packet after the garbage terminator is not filled
+ 6. SEND_NON_EMPTY_VERSION_PACKET - non-empty version packet is simply ignored
+ """
+ EARLY_KEY_RESPONSE = 0
+ EXCESS_GARBAGE = 1
+ WRONG_GARBAGE_TERMINATOR = 2
+ WRONG_GARBAGE = 3
+ SEND_NO_AAD = 4
+ SEND_NON_EMPTY_VERSION_PACKET = 5
+
+
+class EarlyKeyResponseState(EncryptedP2PState):
+ """ Modify v2 P2P protocol functions for testing EARLY_KEY_RESPONSE scenario"""
+ def __init__(self, initiating, net):
+ super().__init__(initiating=initiating, net=net)
+ self.can_data_be_received = False # variable used to assert if data is received on recvbuf.
+
+ def initiate_v2_handshake(self):
+ """Send ellswift and garbage bytes in 2 parts when TestType = (EARLY_KEY_RESPONSE)"""
+ self.generate_keypair_and_garbage()
+ return b""
+
+
+class ExcessGarbageState(EncryptedP2PState):
+ """Generate > MAX_GARBAGE_LEN garbage bytes"""
+ def generate_keypair_and_garbage(self):
+ garbage_len = MAX_GARBAGE_LEN + random.randrange(1, MAX_GARBAGE_LEN + 1)
+ return super().generate_keypair_and_garbage(garbage_len)
+
+
+class WrongGarbageTerminatorState(EncryptedP2PState):
+ """Add option for sending wrong garbage terminator"""
+ def generate_keypair_and_garbage(self):
+ garbage_len = random.randrange(MAX_GARBAGE_LEN//2)
+ return super().generate_keypair_and_garbage(garbage_len)
+
+ def complete_handshake(self, response):
+ length, handshake_bytes = super().complete_handshake(response)
+ # first 16 bytes returned by complete_handshake() is the garbage terminator
+ wrong_garbage_terminator = random_bitflip(handshake_bytes[:16])
+ return length, wrong_garbage_terminator + handshake_bytes[16:]
+
+
+class WrongGarbageState(EncryptedP2PState):
+ """Generate tampered garbage bytes"""
+ def generate_keypair_and_garbage(self):
+ garbage_len = random.randrange(1, MAX_GARBAGE_LEN)
+ ellswift_garbage_bytes = super().generate_keypair_and_garbage(garbage_len)
+ # assume that garbage bytes sent to TestNode were tampered with
+ return ellswift_garbage_bytes[:64] + random_bitflip(ellswift_garbage_bytes[64:])
+
+
+class NoAADState(EncryptedP2PState):
+ """Add option for not filling first encrypted packet after garbage terminator with AAD"""
+ def generate_keypair_and_garbage(self):
+ garbage_len = random.randrange(1, MAX_GARBAGE_LEN)
+ return super().generate_keypair_and_garbage(garbage_len)
+
+ def complete_handshake(self, response):
+ self.sent_garbage = b'' # do not authenticate the garbage which is sent
+ return super().complete_handshake(response)
+
+
+class NonEmptyVersionPacketState(EncryptedP2PState):
+ """"Add option for sending non-empty transport version packet."""
+ def complete_handshake(self, response):
+ self.transport_version = random.randbytes(5)
+ return super().complete_handshake(response)
+
+
+class MisbehavingV2Peer(P2PInterface):
+ """Custom implementation of P2PInterface which uses modified v2 P2P protocol functions for testing purposes."""
+ def __init__(self, test_type):
+ super().__init__()
+ self.test_type = test_type
+
+ def connection_made(self, transport):
+ if self.test_type == TestType.EARLY_KEY_RESPONSE:
+ self.v2_state = EarlyKeyResponseState(initiating=True, net='regtest')
+ elif self.test_type == TestType.EXCESS_GARBAGE:
+ self.v2_state = ExcessGarbageState(initiating=True, net='regtest')
+ elif self.test_type == TestType.WRONG_GARBAGE_TERMINATOR:
+ self.v2_state = WrongGarbageTerminatorState(initiating=True, net='regtest')
+ elif self.test_type == TestType.WRONG_GARBAGE:
+ self.v2_state = WrongGarbageState(initiating=True, net='regtest')
+ elif self.test_type == TestType.SEND_NO_AAD:
+ self.v2_state = NoAADState(initiating=True, net='regtest')
+ elif TestType.SEND_NON_EMPTY_VERSION_PACKET:
+ self.v2_state = NonEmptyVersionPacketState(initiating=True, net='regtest')
+ super().connection_made(transport)
+
+ def data_received(self, t):
+ if self.test_type == TestType.EARLY_KEY_RESPONSE:
+ # check that data can be received on recvbuf only when mismatch from V1_PREFIX happens
+ assert self.v2_state.can_data_be_received
+ else:
+ super().data_received(t)
+
+
+class EncryptedP2PMisbehaving(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.extra_args = [["-v2transport=1", "-peertimeout=3"]]
+
+ def run_test(self):
+ self.test_earlykeyresponse()
+ self.test_v2disconnection()
+
+ def test_earlykeyresponse(self):
+ self.log.info('Sending ellswift bytes in parts to ensure that response from responder is received only when')
+ self.log.info('ellswift bytes have a mismatch from the 16 bytes(network magic followed by "version\\x00\\x00\\x00\\x00\\x00")')
+ node0 = self.nodes[0]
+ self.log.info('Sending first 4 bytes of ellswift which match network magic')
+ self.log.info('If a response is received, assertion failure would happen in our custom data_received() function')
+ peer1 = node0.add_p2p_connection(MisbehavingV2Peer(TestType.EARLY_KEY_RESPONSE), wait_for_verack=False, send_version=False, supports_v2_p2p=True, wait_for_v2_handshake=False)
+ peer1.send_raw_message(MAGIC_BYTES['regtest'])
+ self.log.info('Sending remaining ellswift and garbage which are different from V1_PREFIX. Since a response is')
+ self.log.info('expected now, our custom data_received() function wouldn\'t result in assertion failure')
+ peer1.v2_state.can_data_be_received = True
+ peer1.send_raw_message(peer1.v2_state.ellswift_ours[4:] + peer1.v2_state.sent_garbage)
+ with node0.assert_debug_log(['V2 handshake timeout peer=0']):
+ peer1.wait_for_disconnect(timeout=5)
+ self.log.info('successful disconnection since modified ellswift was sent as response')
+
+ def test_v2disconnection(self):
+ # test v2 disconnection scenarios
+ node0 = self.nodes[0]
+ expected_debug_message = [
+ [], # EARLY_KEY_RESPONSE
+ ["V2 transport error: missing garbage terminator, peer=1"], # EXCESS_GARBAGE
+ ["V2 handshake timeout peer=2"], # WRONG_GARBAGE_TERMINATOR
+ ["V2 transport error: packet decryption failure"], # WRONG_GARBAGE
+ ["V2 transport error: packet decryption failure"], # SEND_NO_AAD
+ [], # SEND_NON_EMPTY_VERSION_PACKET
+ ]
+ for test_type in TestType:
+ if test_type == TestType.EARLY_KEY_RESPONSE:
+ continue
+ elif test_type == TestType.SEND_NON_EMPTY_VERSION_PACKET:
+ node0.add_p2p_connection(MisbehavingV2Peer(test_type), wait_for_verack=True, send_version=True, supports_v2_p2p=True)
+ self.log.info(f"No disconnection for {test_type.name}")
+ else:
+ with node0.assert_debug_log(expected_debug_message[test_type.value], timeout=5):
+ peer = node0.add_p2p_connection(MisbehavingV2Peer(test_type), wait_for_verack=False, send_version=False, supports_v2_p2p=True, expect_success=False)
+ peer.wait_for_disconnect()
+ self.log.info(f"Expected disconnection for {test_type.name}")
+
+
+if __name__ == '__main__':
+ EncryptedP2PMisbehaving().main()
diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py
index 65d7b4c422..37656341d2 100755
--- a/test/functional/rpc_createmultisig.py
+++ b/test/functional/rpc_createmultisig.py
@@ -9,10 +9,10 @@ import json
import os
from test_framework.address import address_to_scriptpubkey
-from test_framework.blocktools import COINBASE_MATURITY
-from test_framework.authproxy import JSONRPCException
from test_framework.descriptors import descsum_create, drop_origins
from test_framework.key import ECPubKey
+from test_framework.messages import COIN
+from test_framework.script_util import keys_to_multisig_script
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_raises_rpc_error,
@@ -32,92 +32,54 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
self.setup_clean_chain = True
self.num_nodes = 3
self.supports_cli = False
+ self.enable_wallet_if_possible()
- def get_keys(self):
+ def create_keys(self, num_keys):
self.pub = []
self.priv = []
- node0, node1, node2 = self.nodes
- for _ in range(self.nkeys):
+ for _ in range(num_keys):
privkey, pubkey = generate_keypair(wif=True)
self.pub.append(pubkey.hex())
self.priv.append(privkey)
- if self.is_bdb_compiled():
- self.final = node2.getnewaddress()
- else:
- self.final = getnewdestination('bech32')[2]
+
+ def create_wallet(self, node, wallet_name):
+ node.createwallet(wallet_name=wallet_name, disable_private_keys=True)
+ return node.get_wallet_rpc(wallet_name)
def run_test(self):
node0, node1, node2 = self.nodes
self.wallet = MiniWallet(test_node=node0)
- if self.is_bdb_compiled():
- self.import_deterministic_coinbase_privkeys()
+ if self.is_wallet_compiled():
self.check_addmultisigaddress_errors()
self.log.info('Generating blocks ...')
self.generate(self.wallet, 149)
- self.moved = 0
- for self.nkeys in [3, 5]:
- for self.nsigs in [2, 3]:
- for self.output_type in ["bech32", "p2sh-segwit", "legacy"]:
- self.get_keys()
- self.do_multisig()
- if self.is_bdb_compiled():
- self.checkbalances()
-
- # Test mixed compressed and uncompressed pubkeys
- self.log.info('Mixed compressed and uncompressed multisigs are not allowed')
- pk0, pk1, pk2 = [getnewdestination('bech32')[0].hex() for _ in range(3)]
-
- # decompress pk2
- pk_obj = ECPubKey()
- pk_obj.set(bytes.fromhex(pk2))
- pk_obj.compressed = False
- pk2 = pk_obj.get_bytes().hex()
-
- if self.is_bdb_compiled():
- node0.createwallet(wallet_name='wmulti0', disable_private_keys=True)
- wmulti0 = node0.get_wallet_rpc('wmulti0')
-
- # Check all permutations of keys because order matters apparently
- for keys in itertools.permutations([pk0, pk1, pk2]):
- # Results should be the same as this legacy one
- legacy_addr = node0.createmultisig(2, keys, 'legacy')['address']
-
- if self.is_bdb_compiled():
- result = wmulti0.addmultisigaddress(2, keys, '', 'legacy')
- assert_equal(legacy_addr, result['address'])
- assert 'warnings' not in result
-
- # Generate addresses with the segwit types. These should all make legacy addresses
- err_msg = ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]
-
- for addr_type in ['bech32', 'p2sh-segwit']:
- result = self.nodes[0].createmultisig(nrequired=2, keys=keys, address_type=addr_type)
- assert_equal(legacy_addr, result['address'])
- assert_equal(result['warnings'], err_msg)
-
- if self.is_bdb_compiled():
- result = wmulti0.addmultisigaddress(nrequired=2, keys=keys, address_type=addr_type)
- assert_equal(legacy_addr, result['address'])
- assert_equal(result['warnings'], err_msg)
-
- self.log.info('Testing sortedmulti descriptors with BIP 67 test vectors')
- with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f:
- vectors = json.load(f)
+ wallet_multi = self.create_wallet(node1, 'wmulti') if self._requires_wallet else None
+ self.create_keys(21) # max number of allowed keys + 1
+ m_of_n = [(2, 3), (3, 3), (2, 5), (3, 5), (10, 15), (15, 15)]
+ for (sigs, keys) in m_of_n:
+ for output_type in ["bech32", "p2sh-segwit", "legacy"]:
+ self.do_multisig(keys, sigs, output_type, wallet_multi)
- for t in vectors:
- key_str = ','.join(t['keys'])
- desc = descsum_create('sh(sortedmulti(2,{}))'.format(key_str))
- assert_equal(self.nodes[0].deriveaddresses(desc)[0], t['address'])
- sorted_key_str = ','.join(t['sorted_keys'])
- sorted_key_desc = descsum_create('sh(multi(2,{}))'.format(sorted_key_str))
- assert_equal(self.nodes[0].deriveaddresses(sorted_key_desc)[0], t['address'])
+ self.test_multisig_script_limit(wallet_multi)
+ self.test_mixing_uncompressed_and_compressed_keys(node0, wallet_multi)
+ self.test_sortedmulti_descriptors_bip67()
# Check that bech32m is currently not allowed
assert_raises_rpc_error(-5, "createmultisig cannot create bech32m multisig addresses", self.nodes[0].createmultisig, 2, self.pub, "bech32m")
+ self.log.info('Check correct encoding of multisig script for all n (1..20)')
+ for nkeys in range(1, 20+1):
+ keys = [self.pub[0]]*nkeys
+ expected_ms_script = keys_to_multisig_script(keys, k=nkeys) # simply use n-of-n
+ # note that the 'legacy' address type fails for n values larger than 15
+ # due to exceeding the P2SH size limit (520 bytes), so we use 'bech32' instead
+ # (for the purpose of this encoding test, we don't care about the resulting address)
+ res = self.nodes[0].createmultisig(nrequired=nkeys, keys=keys, address_type='bech32')
+ assert_equal(res['redeemScript'], expected_ms_script.hex())
+
def check_addmultisigaddress_errors(self):
if self.options.descriptors:
return
@@ -133,117 +95,165 @@ class RpcCreateMultiSigTest(BitcoinTestFramework):
pubs = [self.nodes[1].getaddressinfo(addr)["pubkey"] for addr in addresses]
assert_raises_rpc_error(-5, "Bech32m multisig addresses cannot be created with legacy wallets", self.nodes[0].addmultisigaddress, 2, pubs, "", "bech32m")
- def checkbalances(self):
- node0, node1, node2 = self.nodes
- self.generate(node0, COINBASE_MATURITY)
+ def test_multisig_script_limit(self, wallet_multi):
+ node1 = self.nodes[1]
+ pubkeys = self.pub[0:20]
- bal0 = node0.getbalance()
- bal1 = node1.getbalance()
- bal2 = node2.getbalance()
- balw = self.wallet.get_balance()
+ self.log.info('Test legacy redeem script max size limit')
+ assert_raises_rpc_error(-8, "redeemScript exceeds size limit: 684 > 520", node1.createmultisig, 16, pubkeys, 'legacy')
- height = node0.getblockchaininfo()["blocks"]
- assert 150 < height < 350
- total = 149 * 50 + (height - 149 - 100) * 25
- assert bal1 == 0
- assert bal2 == self.moved
- assert_equal(bal0 + bal1 + bal2 + balw, total)
+ self.log.info('Test valid 16-20 multisig p2sh-legacy and bech32 (no wallet)')
+ self.do_multisig(nkeys=20, nsigs=16, output_type="p2sh-segwit", wallet_multi=None)
+ self.do_multisig(nkeys=20, nsigs=16, output_type="bech32", wallet_multi=None)
- def do_multisig(self):
- node0, node1, node2 = self.nodes
+ self.log.info('Test invalid 16-21 multisig p2sh-legacy and bech32 (no wallet)')
+ assert_raises_rpc_error(-8, "Number of keys involved in the multisignature address creation > 20", node1.createmultisig, 16, self.pub, 'p2sh-segwit')
+ assert_raises_rpc_error(-8, "Number of keys involved in the multisignature address creation > 20", node1.createmultisig, 16, self.pub, 'bech32')
- if self.is_bdb_compiled():
- if 'wmulti' not in node1.listwallets():
- try:
- node1.loadwallet('wmulti')
- except JSONRPCException as e:
- path = self.nodes[1].wallets_path / "wmulti"
- if e.error['code'] == -18 and "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path) in e.error['message']:
- node1.createwallet(wallet_name='wmulti', disable_private_keys=True)
- else:
- raise
- wmulti = node1.get_wallet_rpc('wmulti')
+ # Check legacy wallet related command
+ self.log.info('Test legacy redeem script max size limit (with wallet)')
+ if wallet_multi is not None and not self.options.descriptors:
+ assert_raises_rpc_error(-8, "redeemScript exceeds size limit: 684 > 520", wallet_multi.addmultisigaddress, 16, pubkeys, '', 'legacy')
+
+ self.log.info('Test legacy wallet unsupported operation. 16-20 multisig p2sh-legacy and bech32 generation')
+ # Due an internal limitation on legacy wallets, the redeem script limit also applies to p2sh-segwit and bech32 (even when the scripts are valid)
+ # We take this as a "good thing" to tell users to upgrade to descriptors.
+ assert_raises_rpc_error(-4, "Unsupported multisig script size for legacy wallet. Upgrade to descriptors to overcome this limitation for p2sh-segwit or bech32 scripts", wallet_multi.addmultisigaddress, 16, pubkeys, '', 'p2sh-segwit')
+ assert_raises_rpc_error(-4, "Unsupported multisig script size for legacy wallet. Upgrade to descriptors to overcome this limitation for p2sh-segwit or bech32 scripts", wallet_multi.addmultisigaddress, 16, pubkeys, '', 'bech32')
+
+ def do_multisig(self, nkeys, nsigs, output_type, wallet_multi):
+ node0, node1, node2 = self.nodes
+ pub_keys = self.pub[0: nkeys]
+ priv_keys = self.priv[0: nkeys]
# Construct the expected descriptor
- desc = 'multi({},{})'.format(self.nsigs, ','.join(self.pub))
- if self.output_type == 'legacy':
+ desc = 'multi({},{})'.format(nsigs, ','.join(pub_keys))
+ if output_type == 'legacy':
desc = 'sh({})'.format(desc)
- elif self.output_type == 'p2sh-segwit':
+ elif output_type == 'p2sh-segwit':
desc = 'sh(wsh({}))'.format(desc)
- elif self.output_type == 'bech32':
+ elif output_type == 'bech32':
desc = 'wsh({})'.format(desc)
desc = descsum_create(desc)
- msig = node2.createmultisig(self.nsigs, self.pub, self.output_type)
+ msig = node2.createmultisig(nsigs, pub_keys, output_type)
assert 'warnings' not in msig
madd = msig["address"]
mredeem = msig["redeemScript"]
assert_equal(desc, msig['descriptor'])
- if self.output_type == 'bech32':
+ if output_type == 'bech32':
assert madd[0:4] == "bcrt" # actually a bech32 address
- if self.is_bdb_compiled():
+ if wallet_multi is not None:
# compare against addmultisigaddress
- msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None, self.output_type)
+ msigw = wallet_multi.addmultisigaddress(nsigs, pub_keys, None, output_type)
maddw = msigw["address"]
mredeemw = msigw["redeemScript"]
assert_equal(desc, drop_origins(msigw['descriptor']))
# addmultisigiaddress and createmultisig work the same
assert maddw == madd
assert mredeemw == mredeem
- wmulti.unloadwallet()
spk = address_to_scriptpubkey(madd)
- txid = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=spk, amount=1300)["txid"]
- tx = node0.getrawtransaction(txid, True)
- vout = [v["n"] for v in tx["vout"] if madd == v["scriptPubKey"]["address"]]
- assert len(vout) == 1
- vout = vout[0]
- scriptPubKey = tx["vout"][vout]["scriptPubKey"]["hex"]
- value = tx["vout"][vout]["value"]
- prevtxs = [{"txid": txid, "vout": vout, "scriptPubKey": scriptPubKey, "redeemScript": mredeem, "amount": value}]
+ value = decimal.Decimal("0.00004000")
+ tx = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=spk, amount=int(value * COIN))
+ prevtxs = [{"txid": tx["txid"], "vout": tx["sent_vout"], "scriptPubKey": spk.hex(), "redeemScript": mredeem, "amount": value}]
self.generate(node0, 1)
- outval = value - decimal.Decimal("0.00001000")
- rawtx = node2.createrawtransaction([{"txid": txid, "vout": vout}], [{self.final: outval}])
+ outval = value - decimal.Decimal("0.00002000") # deduce fee (must be higher than the min relay fee)
+ # send coins to node2 when wallet is enabled
+ node2_balance = node2.getbalances()['mine']['trusted'] if self.is_wallet_compiled() else 0
+ out_addr = node2.getnewaddress() if self.is_wallet_compiled() else getnewdestination('bech32')[2]
+ rawtx = node2.createrawtransaction([{"txid": tx["txid"], "vout": tx["sent_vout"]}], [{out_addr: outval}])
prevtx_err = dict(prevtxs[0])
del prevtx_err["redeemScript"]
- assert_raises_rpc_error(-8, "Missing redeemScript/witnessScript", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ assert_raises_rpc_error(-8, "Missing redeemScript/witnessScript", node2.signrawtransactionwithkey, rawtx, priv_keys[0:nsigs-1], [prevtx_err])
# if witnessScript specified, all ok
prevtx_err["witnessScript"] = prevtxs[0]["redeemScript"]
- node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ node2.signrawtransactionwithkey(rawtx, priv_keys[0:nsigs-1], [prevtx_err])
# both specified, also ok
prevtx_err["redeemScript"] = prevtxs[0]["redeemScript"]
- node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ node2.signrawtransactionwithkey(rawtx, priv_keys[0:nsigs-1], [prevtx_err])
# redeemScript mismatch to witnessScript
prevtx_err["redeemScript"] = "6a" # OP_RETURN
- assert_raises_rpc_error(-8, "redeemScript does not correspond to witnessScript", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ assert_raises_rpc_error(-8, "redeemScript does not correspond to witnessScript", node2.signrawtransactionwithkey, rawtx, priv_keys[0:nsigs-1], [prevtx_err])
# redeemScript does not match scriptPubKey
del prevtx_err["witnessScript"]
- assert_raises_rpc_error(-8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ assert_raises_rpc_error(-8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, priv_keys[0:nsigs-1], [prevtx_err])
# witnessScript does not match scriptPubKey
prevtx_err["witnessScript"] = prevtx_err["redeemScript"]
del prevtx_err["redeemScript"]
- assert_raises_rpc_error(-8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, self.priv[0:self.nsigs-1], [prevtx_err])
+ assert_raises_rpc_error(-8, "redeemScript/witnessScript does not match scriptPubKey", node2.signrawtransactionwithkey, rawtx, priv_keys[0:nsigs-1], [prevtx_err])
- rawtx2 = node2.signrawtransactionwithkey(rawtx, self.priv[0:self.nsigs - 1], prevtxs)
- rawtx3 = node2.signrawtransactionwithkey(rawtx2["hex"], [self.priv[-1]], prevtxs)
+ rawtx2 = node2.signrawtransactionwithkey(rawtx, priv_keys[0:nsigs - 1], prevtxs)
+ rawtx3 = node2.signrawtransactionwithkey(rawtx2["hex"], [priv_keys[-1]], prevtxs)
+ assert rawtx3['complete']
- self.moved += outval
tx = node0.sendrawtransaction(rawtx3["hex"], 0)
blk = self.generate(node0, 1)[0]
assert tx in node0.getblock(blk)["tx"]
+ # When the wallet is enabled, assert node2 sees the incoming amount
+ if self.is_wallet_compiled():
+ assert_equal(node2.getbalances()['mine']['trusted'], node2_balance + outval)
+
txinfo = node0.getrawtransaction(tx, True, blk)
- self.log.info("n/m=%d/%d %s size=%d vsize=%d weight=%d" % (self.nsigs, self.nkeys, self.output_type, txinfo["size"], txinfo["vsize"], txinfo["weight"]))
+ self.log.info("n/m=%d/%d %s size=%d vsize=%d weight=%d" % (nsigs, nkeys, output_type, txinfo["size"], txinfo["vsize"], txinfo["weight"]))
+
+ def test_mixing_uncompressed_and_compressed_keys(self, node, wallet_multi):
+ self.log.info('Mixed compressed and uncompressed multisigs are not allowed')
+ pk0, pk1, pk2 = [getnewdestination('bech32')[0].hex() for _ in range(3)]
+
+ # decompress pk2
+ pk_obj = ECPubKey()
+ pk_obj.set(bytes.fromhex(pk2))
+ pk_obj.compressed = False
+ pk2 = pk_obj.get_bytes().hex()
+
+ # Check all permutations of keys because order matters apparently
+ for keys in itertools.permutations([pk0, pk1, pk2]):
+ # Results should be the same as this legacy one
+ legacy_addr = node.createmultisig(2, keys, 'legacy')['address']
+
+ if wallet_multi is not None:
+ # 'addmultisigaddress' should return the same address
+ result = wallet_multi.addmultisigaddress(2, keys, '', 'legacy')
+ assert_equal(legacy_addr, result['address'])
+ assert 'warnings' not in result
+
+ # Generate addresses with the segwit types. These should all make legacy addresses
+ err_msg = ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]
+
+ for addr_type in ['bech32', 'p2sh-segwit']:
+ result = self.nodes[0].createmultisig(nrequired=2, keys=keys, address_type=addr_type)
+ assert_equal(legacy_addr, result['address'])
+ assert_equal(result['warnings'], err_msg)
+
+ if wallet_multi is not None:
+ result = wallet_multi.addmultisigaddress(nrequired=2, keys=keys, address_type=addr_type)
+ assert_equal(legacy_addr, result['address'])
+ assert_equal(result['warnings'], err_msg)
+
+ def test_sortedmulti_descriptors_bip67(self):
+ self.log.info('Testing sortedmulti descriptors with BIP 67 test vectors')
+ with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f:
+ vectors = json.load(f)
+
+ for t in vectors:
+ key_str = ','.join(t['keys'])
+ desc = descsum_create('sh(sortedmulti(2,{}))'.format(key_str))
+ assert_equal(self.nodes[0].deriveaddresses(desc)[0], t['address'])
+ sorted_key_str = ','.join(t['sorted_keys'])
+ sorted_key_desc = descsum_create('sh(multi(2,{}))'.format(sorted_key_str))
+ assert_equal(self.nodes[0].deriveaddresses(sorted_key_desc)[0], t['address'])
if __name__ == '__main__':
diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py
index 1ea6cf52d1..c92c8da357 100755
--- a/test/functional/rpc_dumptxoutset.py
+++ b/test/functional/rpc_dumptxoutset.py
@@ -43,7 +43,7 @@ class DumptxoutsetTest(BitcoinTestFramework):
# UTXO snapshot hash should be deterministic based on mocked time.
assert_equal(
sha256sum_file(str(expected_path)).hex(),
- 'b1bacb602eacf5fbc9a7c2ef6eeb0d229c04e98bdf0c2ea5929012cd0eae3830')
+ '2f775f82811150d310527b5ff773f81fb0fb517e941c543c1f7c4d38fd2717b3')
assert_equal(
out['txoutset_hash'], 'a0b7baa3bf5ccbd3279728f230d7ca0c44a76e9923fca8f32dbfd08d65ea496a')
diff --git a/test/functional/rpc_generate.py b/test/functional/rpc_generate.py
index 20f62079fd..3e250925e7 100755
--- a/test/functional/rpc_generate.py
+++ b/test/functional/rpc_generate.py
@@ -87,7 +87,7 @@ class RPCGenerateTest(BitcoinTestFramework):
txid1 = miniwallet.send_self_transfer(from_node=node)['txid']
utxo1 = miniwallet.get_utxo(txid=txid1)
rawtx2 = miniwallet.create_self_transfer(utxo_to_spend=utxo1)['hex']
- assert_raises_rpc_error(-25, 'TestBlockValidity failed: bad-txns-inputs-missingorspent', self.generateblock, node, address, [rawtx2, txid1])
+ assert_raises_rpc_error(-25, 'testBlockValidity failed: bad-txns-inputs-missingorspent', self.generateblock, node, address, [rawtx2, txid1])
self.log.info('Fail to generate block with txid not in mempool')
missing_txid = '0000000000000000000000000000000000000000000000000000000000000000'
diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py
index 2701d2471d..37e2c1fb71 100755
--- a/test/functional/rpc_net.py
+++ b/test/functional/rpc_net.py
@@ -237,28 +237,35 @@ class NetTest(BitcoinTestFramework):
def test_addnode_getaddednodeinfo(self):
self.log.info("Test addnode and getaddednodeinfo")
assert_equal(self.nodes[0].getaddednodeinfo(), [])
- # add a node (node2) to node0
+ self.log.info("Add a node (node2) to node0")
ip_port = "127.0.0.1:{}".format(p2p_port(2))
self.nodes[0].addnode(node=ip_port, command='add')
- # try to add an equivalent ip
- # (note that OpenBSD doesn't support the IPv4 shorthand notation with omitted zero-bytes)
+ self.log.info("Try to add an equivalent ip and check it fails")
+ self.log.debug("(note that OpenBSD doesn't support the IPv4 shorthand notation with omitted zero-bytes)")
if platform.system() != "OpenBSD":
ip_port2 = "127.1:{}".format(p2p_port(2))
assert_raises_rpc_error(-23, "Node already added", self.nodes[0].addnode, node=ip_port2, command='add')
- # check that the node has indeed been added
+ self.log.info("Check that the node has indeed been added")
added_nodes = self.nodes[0].getaddednodeinfo()
assert_equal(len(added_nodes), 1)
assert_equal(added_nodes[0]['addednode'], ip_port)
- # check that node cannot be added again
+ self.log.info("Check that filtering by node works")
+ self.nodes[0].addnode(node="11.22.33.44", command='add')
+ first_added_node = self.nodes[0].getaddednodeinfo(node=ip_port)
+ assert_equal(added_nodes, first_added_node)
+ assert_equal(len(self.nodes[0].getaddednodeinfo()), 2)
+ self.log.info("Check that node cannot be added again")
assert_raises_rpc_error(-23, "Node already added", self.nodes[0].addnode, node=ip_port, command='add')
- # check that node can be removed
+ self.log.info("Check that node can be removed")
self.nodes[0].addnode(node=ip_port, command='remove')
- assert_equal(self.nodes[0].getaddednodeinfo(), [])
- # check that an invalid command returns an error
+ added_nodes = self.nodes[0].getaddednodeinfo()
+ assert_equal(len(added_nodes), 1)
+ assert_equal(added_nodes[0]['addednode'], "11.22.33.44")
+ self.log.info("Check that an invalid command returns an error")
assert_raises_rpc_error(-1, 'addnode "node" "command"', self.nodes[0].addnode, node=ip_port, command='abc')
- # check that trying to remove the node again returns an error
+ self.log.info("Check that trying to remove the node again returns an error")
assert_raises_rpc_error(-24, "Node could not be removed", self.nodes[0].addnode, node=ip_port, command='remove')
- # check that a non-existent node returns an error
+ self.log.info("Check that a non-existent node returns an error")
assert_raises_rpc_error(-24, "Node has not been added", self.nodes[0].getaddednodeinfo, '1.1.1.1')
def test_service_flags(self):
diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py
index 113424c0a6..1acd586d2c 100755
--- a/test/functional/rpc_packages.py
+++ b/test/functional/rpc_packages.py
@@ -394,7 +394,7 @@ class RPCPackagesTest(BitcoinTestFramework):
peer = node.add_p2p_connection(P2PTxInvStore())
txs = self.wallet.create_self_transfer_chain(chain_length=2)
bad_child = tx_from_hex(txs[1]["hex"])
- bad_child.nVersion = -1
+ bad_child.version = 0xffffffff
hex_partial_acceptance = [txs[0]["hex"], bad_child.serialize().hex()]
res = node.submitpackage(hex_partial_acceptance)
assert_equal(res["package_msg"], "transaction failed")
diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
index 6ee7e56886..a56960adff 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -700,11 +700,9 @@ class PSBTTest(BitcoinTestFramework):
assert_equal(analysis['next'], 'creator')
assert_equal(analysis['error'], 'PSBT is not valid. Output amount invalid')
- analysis = self.nodes[0].analyzepsbt('cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
- assert_equal(analysis['next'], 'creator')
- assert_equal(analysis['error'], 'PSBT is not valid. Input 0 specifies invalid prevout')
+ assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].analyzepsbt, "cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==")
- assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
+ assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].walletprocesspsbt, "cHNidP8BAJoCAAAAAkvEW8NnDtdNtDpsmze+Ht2LH35IJcKv00jKAlUs21RrAwAAAAD/////S8Rbw2cO1020OmybN74e3Ysffkglwq/TSMoCVSzbVGsBAAAAAP7///8CwLYClQAAAAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQD5ApUAAAAAFgAUKNw0x8HRctAgmvoevm4u1SbN7XIAAAAAAAEAnQIAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP7///9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/v///wIA+QKVAAAAABl2qRT9zXUVA8Ls5iVqynLHe5/vSe1XyYisQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAAAAAQEfQM0ClQAAAAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==")
self.log.info("Test that we can fund psbts with external inputs specified")
diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py
index 3978c80dde..f974a05f7b 100755
--- a/test/functional/rpc_rawtransaction.py
+++ b/test/functional/rpc_rawtransaction.py
@@ -463,20 +463,34 @@ class RawTransactionsTest(BitcoinTestFramework):
self.log.info("Test transaction version numbers")
# Test the minimum transaction version number that fits in a signed 32-bit integer.
- # As transaction version is unsigned, this should convert to its unsigned equivalent.
+ # As transaction version is serialized unsigned, this should convert to its unsigned equivalent.
tx = CTransaction()
- tx.nVersion = -0x80000000
+ tx.version = 0x80000000
rawtx = tx.serialize().hex()
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx['version'], 0x80000000)
# Test the maximum transaction version number that fits in a signed 32-bit integer.
tx = CTransaction()
- tx.nVersion = 0x7fffffff
+ tx.version = 0x7fffffff
rawtx = tx.serialize().hex()
decrawtx = self.nodes[0].decoderawtransaction(rawtx)
assert_equal(decrawtx['version'], 0x7fffffff)
+ # Test the minimum transaction version number that fits in an unsigned 32-bit integer.
+ tx = CTransaction()
+ tx.version = 0
+ rawtx = tx.serialize().hex()
+ decrawtx = self.nodes[0].decoderawtransaction(rawtx)
+ assert_equal(decrawtx['version'], 0)
+
+ # Test the maximum transaction version number that fits in an unsigned 32-bit integer.
+ tx = CTransaction()
+ tx.version = 0xffffffff
+ rawtx = tx.serialize().hex()
+ decrawtx = self.nodes[0].decoderawtransaction(rawtx)
+ assert_equal(decrawtx['version'], 0xffffffff)
+
def raw_multisig_transaction_legacy_tests(self):
self.log.info("Test raw multisig transactions (legacy)")
# The traditional multisig workflow does not work with descriptor wallets so these are legacy only.
@@ -585,6 +599,8 @@ class RawTransactionsTest(BitcoinTestFramework):
rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs)
self.log.debug(rawTxPartialSigned2)
assert_equal(rawTxPartialSigned2['complete'], False) # node2 only has one key, can't comp. sign the tx
+ assert_raises_rpc_error(-22, "TX decode failed", self.nodes[0].combinerawtransaction, [rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex'] + "00"])
+ assert_raises_rpc_error(-22, "Missing transactions", self.nodes[0].combinerawtransaction, [])
rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
self.log.debug(rawTxComb)
self.nodes[2].sendrawtransaction(rawTxComb)
@@ -592,6 +608,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.sync_all()
self.generate(self.nodes[0], 1)
assert_equal(self.nodes[0].getbalance(), bal + Decimal('50.00000000') + Decimal('2.19000000')) # block reward + tx
+ assert_raises_rpc_error(-25, "Input not found or already spent", self.nodes[0].combinerawtransaction, [rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
if __name__ == '__main__':
diff --git a/test/functional/rpc_signrawtransactionwithkey.py b/test/functional/rpc_signrawtransactionwithkey.py
index 268584331e..f4fec13495 100755
--- a/test/functional/rpc_signrawtransactionwithkey.py
+++ b/test/functional/rpc_signrawtransactionwithkey.py
@@ -126,10 +126,20 @@ class SignRawTransactionWithKeyTest(BitcoinTestFramework):
privkeys = [self.nodes[0].get_deterministic_priv_key().key]
assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[0].signrawtransactionwithkey, tx, privkeys, sighashtype="all")
+ def invalid_private_key_and_tx(self):
+ self.log.info("Test signing transaction with an invalid private key")
+ tx = self.nodes[0].createrawtransaction(INPUTS, OUTPUTS)
+ privkeys = ["123"]
+ assert_raises_rpc_error(-5, "Invalid private key", self.nodes[0].signrawtransactionwithkey, tx, privkeys)
+ self.log.info("Test signing transaction with an invalid tx hex")
+ privkeys = [self.nodes[0].get_deterministic_priv_key().key]
+ assert_raises_rpc_error(-22, "TX decode failed. Make sure the tx has at least one input.", self.nodes[0].signrawtransactionwithkey, tx + "00", privkeys)
+
def run_test(self):
self.successful_signing_test()
self.witness_script_test()
self.invalid_sighashtype_test()
+ self.invalid_private_key_and_tx()
if __name__ == '__main__':
diff --git a/test/functional/rpc_users.py b/test/functional/rpc_users.py
index 66cdd7cf9a..153493fbab 100755
--- a/test/functional/rpc_users.py
+++ b/test/functional/rpc_users.py
@@ -11,12 +11,15 @@ from test_framework.util import (
)
import http.client
+import os
+import platform
import urllib.parse
import subprocess
from random import SystemRandom
import string
import configparser
import sys
+from typing import Optional
def call_with_auth(node, user, password):
@@ -84,6 +87,40 @@ class HTTPBasicsTest(BitcoinTestFramework):
self.log.info('Wrong...')
assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
+ def test_rpccookieperms(self):
+ p = {"owner": 0o600, "group": 0o640, "all": 0o644}
+
+ if platform.system() == 'Windows':
+ self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
+ return
+
+ self.log.info('Check cookie file permissions can be set using -rpccookieperms')
+
+ cookie_file_path = self.nodes[1].chain_path / '.cookie'
+ PERM_BITS_UMASK = 0o777
+
+ def test_perm(perm: Optional[str]):
+ if not perm:
+ perm = 'owner'
+ self.restart_node(1)
+ else:
+ self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
+
+ file_stat = os.stat(cookie_file_path)
+ actual_perms = file_stat.st_mode & PERM_BITS_UMASK
+ expected_perms = p[perm]
+ assert_equal(expected_perms, actual_perms)
+
+ # Remove any leftover rpc{user|password} config options from previous tests
+ self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
+
+ self.log.info('Check default cookie permission')
+ test_perm(None)
+
+ self.log.info('Check custom cookie permissions')
+ for perm in ["owner", "group", "all"]:
+ test_perm(perm)
+
def run_test(self):
self.conf_setup()
self.log.info('Check correctness of the rpcauth config option')
@@ -115,6 +152,8 @@ class HTTPBasicsTest(BitcoinTestFramework):
(self.nodes[0].chain_path / ".cookie.tmp").mkdir()
self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
+ self.test_rpccookieperms()
+
if __name__ == '__main__':
HTTPBasicsTest().main()
diff --git a/test/functional/test_framework/authproxy.py b/test/functional/test_framework/authproxy.py
index 7edf9f3679..a357ae4d34 100644
--- a/test/functional/test_framework/authproxy.py
+++ b/test/functional/test_framework/authproxy.py
@@ -26,7 +26,7 @@ ServiceProxy class:
- HTTP connections persist for the life of the AuthServiceProxy object
(if server supports HTTP/1.1)
-- sends protocol 'version', per JSON-RPC 1.1
+- sends "jsonrpc":"2.0", per JSON-RPC 2.0
- sends proper, incrementing 'id'
- sends Basic HTTP authentication headers
- parses all JSON numbers that look like floats as Decimal
@@ -117,7 +117,7 @@ class AuthServiceProxy():
params = dict(args=args, **argsn)
else:
params = args or argsn
- return {'version': '1.1',
+ return {'jsonrpc': '2.0',
'method': self._service_name,
'params': params,
'id': AuthServiceProxy.__id_count}
@@ -125,15 +125,28 @@ class AuthServiceProxy():
def __call__(self, *args, **argsn):
postdata = json.dumps(self.get_request(*args, **argsn), default=serialization_fallback, ensure_ascii=self.ensure_ascii)
response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
- if response['error'] is not None:
- raise JSONRPCException(response['error'], status)
- elif 'result' not in response:
- raise JSONRPCException({
- 'code': -343, 'message': 'missing JSON-RPC result'}, status)
- elif status != HTTPStatus.OK:
- raise JSONRPCException({
- 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
+ # For backwards compatibility tests, accept JSON RPC 1.1 responses
+ if 'jsonrpc' not in response:
+ if response['error'] is not None:
+ raise JSONRPCException(response['error'], status)
+ elif 'result' not in response:
+ raise JSONRPCException({
+ 'code': -343, 'message': 'missing JSON-RPC result'}, status)
+ elif status != HTTPStatus.OK:
+ raise JSONRPCException({
+ 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
+ else:
+ return response['result']
else:
+ assert response['jsonrpc'] == '2.0'
+ if status != HTTPStatus.OK:
+ raise JSONRPCException({
+ 'code': -342, 'message': 'non-200 HTTP status code'}, status)
+ if 'error' in response:
+ raise JSONRPCException(response['error'], status)
+ elif 'result' not in response:
+ raise JSONRPCException({
+ 'code': -343, 'message': 'missing JSON-RPC 2.0 result and error'}, status)
return response['result']
def batch(self, rpc_call_list):
@@ -142,7 +155,7 @@ class AuthServiceProxy():
response, status = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if status != HTTPStatus.OK:
raise JSONRPCException({
- 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status)
+ 'code': -342, 'message': 'non-200 HTTP status code'}, status)
return response
def _get_response(self):
diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py
index 06252f8996..939c7cbef6 100644
--- a/test/functional/test_framework/key.py
+++ b/test/functional/test_framework/key.py
@@ -14,6 +14,7 @@ import random
import unittest
from test_framework.crypto import secp256k1
+from test_framework.util import random_bitflip
# Point with no known discrete log.
H_POINT = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
@@ -292,11 +293,6 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False):
class TestFrameworkKey(unittest.TestCase):
def test_ecdsa_and_schnorr(self):
"""Test the Python ECDSA and Schnorr implementations."""
- def random_bitflip(sig):
- sig = list(sig)
- sig[random.randrange(len(sig))] ^= (1 << (random.randrange(8)))
- return bytes(sig)
-
byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, ORDER - 1, ORDER, 2**256 - 1]]
keys = {}
for privkey_bytes in byte_arrays: # build array of key/pubkey pairs
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index 4e496a9275..005f7546a8 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -560,12 +560,12 @@ class CTxWitness:
class CTransaction:
- __slots__ = ("hash", "nLockTime", "nVersion", "sha256", "vin", "vout",
+ __slots__ = ("hash", "nLockTime", "version", "sha256", "vin", "vout",
"wit")
def __init__(self, tx=None):
if tx is None:
- self.nVersion = 2
+ self.version = 2
self.vin = []
self.vout = []
self.wit = CTxWitness()
@@ -573,7 +573,7 @@ class CTransaction:
self.sha256 = None
self.hash = None
else:
- self.nVersion = tx.nVersion
+ self.version = tx.version
self.vin = copy.deepcopy(tx.vin)
self.vout = copy.deepcopy(tx.vout)
self.nLockTime = tx.nLockTime
@@ -582,7 +582,7 @@ class CTransaction:
self.wit = copy.deepcopy(tx.wit)
def deserialize(self, f):
- self.nVersion = int.from_bytes(f.read(4), "little", signed=True)
+ self.version = int.from_bytes(f.read(4), "little")
self.vin = deser_vector(f, CTxIn)
flags = 0
if len(self.vin) == 0:
@@ -605,7 +605,7 @@ class CTransaction:
def serialize_without_witness(self):
r = b""
- r += self.nVersion.to_bytes(4, "little", signed=True)
+ r += self.version.to_bytes(4, "little")
r += ser_vector(self.vin)
r += ser_vector(self.vout)
r += self.nLockTime.to_bytes(4, "little")
@@ -617,7 +617,7 @@ class CTransaction:
if not self.wit.is_null():
flags |= 1
r = b""
- r += self.nVersion.to_bytes(4, "little", signed=True)
+ r += self.version.to_bytes(4, "little")
if flags:
dummy = []
r += ser_vector(dummy)
@@ -677,8 +677,8 @@ class CTransaction:
return math.ceil(self.get_weight() / WITNESS_SCALE_FACTOR)
def __repr__(self):
- return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \
- % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime)
+ return "CTransaction(version=%i vin=%s vout=%s wit=%s nLockTime=%i)" \
+ % (self.version, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime)
class CBlockHeader:
diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
index 00bd1e4017..4f1265eb54 100755
--- a/test/functional/test_framework/p2p.py
+++ b/test/functional/test_framework/p2p.py
@@ -223,6 +223,7 @@ class P2PConnection(asyncio.Protocol):
# send the initial handshake immediately
if self.supports_v2_p2p and self.v2_state.initiating and not self.v2_state.tried_v2_handshake:
send_handshake_bytes = self.v2_state.initiate_v2_handshake()
+ logger.debug(f"sending {len(self.v2_state.sent_garbage)} bytes of garbage data")
self.send_raw_message(send_handshake_bytes)
# for v1 outbound connections, send version message immediately after opening
# (for v2 outbound connections, send it after the initial v2 handshake)
@@ -262,6 +263,7 @@ class P2PConnection(asyncio.Protocol):
self.v2_state = None
return
elif send_handshake_bytes:
+ logger.debug(f"sending {len(self.v2_state.sent_garbage)} bytes of garbage data")
self.send_raw_message(send_handshake_bytes)
elif send_handshake_bytes == b"":
return # only after send_handshake_bytes are sent can `complete_handshake()` be done
@@ -411,7 +413,7 @@ class P2PConnection(asyncio.Protocol):
tmsg = self.magic_bytes
tmsg += msgtype
tmsg += b"\x00" * (12 - len(msgtype))
- tmsg += struct.pack("<I", len(data))
+ tmsg += len(data).to_bytes(4, "little")
th = sha256(data)
h = sha256(th)
tmsg += h[:4]
diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py
index 7b19d31e17..97d62f957b 100644
--- a/test/functional/test_framework/script.py
+++ b/test/functional/test_framework/script.py
@@ -8,7 +8,6 @@ This file is modified from python-bitcoinlib.
"""
from collections import namedtuple
-import struct
import unittest
from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey
@@ -58,9 +57,9 @@ class CScriptOp(int):
elif len(d) <= 0xff:
return b'\x4c' + bytes([len(d)]) + d # OP_PUSHDATA1
elif len(d) <= 0xffff:
- return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
+ return b'\x4d' + len(d).to_bytes(2, "little") + d # OP_PUSHDATA2
elif len(d) <= 0xffffffff:
- return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
+ return b'\x4e' + len(d).to_bytes(4, "little") + d # OP_PUSHDATA4
else:
raise ValueError("Data too long to encode in a PUSHDATA op")
@@ -670,7 +669,7 @@ def LegacySignatureMsg(script, txTo, inIdx, hashtype):
txtmp.vin.append(tmp)
s = txtmp.serialize_without_witness()
- s += struct.pack(b"<I", hashtype)
+ s += hashtype.to_bytes(4, "little")
return (s, None)
@@ -726,7 +725,7 @@ def SegwitV0SignatureMsg(script, txTo, inIdx, hashtype, amount):
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_sequence = bytes()
for i in txTo.vin:
- serialize_sequence += struct.pack("<I", i.nSequence)
+ serialize_sequence += i.nSequence.to_bytes(4, "little")
hashSequence = uint256_from_str(hash256(serialize_sequence))
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
@@ -739,16 +738,16 @@ def SegwitV0SignatureMsg(script, txTo, inIdx, hashtype, amount):
hashOutputs = uint256_from_str(hash256(serialize_outputs))
ss = bytes()
- ss += struct.pack("<i", txTo.nVersion)
+ ss += txTo.version.to_bytes(4, "little")
ss += ser_uint256(hashPrevouts)
ss += ser_uint256(hashSequence)
ss += txTo.vin[inIdx].prevout.serialize()
ss += ser_string(script)
- ss += struct.pack("<q", amount)
- ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
+ ss += amount.to_bytes(8, "little", signed=True)
+ ss += txTo.vin[inIdx].nSequence.to_bytes(4, "little")
ss += ser_uint256(hashOutputs)
ss += txTo.nLockTime.to_bytes(4, "little")
- ss += struct.pack("<I", hashtype)
+ ss += hashtype.to_bytes(4, "little")
return ss
def SegwitV0SignatureHash(*args, **kwargs):
@@ -800,13 +799,13 @@ def BIP341_sha_prevouts(txTo):
return sha256(b"".join(i.prevout.serialize() for i in txTo.vin))
def BIP341_sha_amounts(spent_utxos):
- return sha256(b"".join(struct.pack("<q", u.nValue) for u in spent_utxos))
+ return sha256(b"".join(u.nValue.to_bytes(8, "little", signed=True) for u in spent_utxos))
def BIP341_sha_scriptpubkeys(spent_utxos):
return sha256(b"".join(ser_string(u.scriptPubKey) for u in spent_utxos))
def BIP341_sha_sequences(txTo):
- return sha256(b"".join(struct.pack("<I", i.nSequence) for i in txTo.vin))
+ return sha256(b"".join(i.nSequence.to_bytes(4, "little") for i in txTo.vin))
def BIP341_sha_outputs(txTo):
return sha256(b"".join(o.serialize() for o in txTo.vout))
@@ -818,8 +817,8 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index = 0, scriptpat
in_type = hash_type & SIGHASH_ANYONECANPAY
spk = spent_utxos[input_index].scriptPubKey
ss = bytes([0, hash_type]) # epoch, hash_type
- ss += struct.pack("<i", txTo.nVersion)
- ss += struct.pack("<I", txTo.nLockTime)
+ ss += txTo.version.to_bytes(4, "little")
+ ss += txTo.nLockTime.to_bytes(4, "little")
if in_type != SIGHASH_ANYONECANPAY:
ss += BIP341_sha_prevouts(txTo)
ss += BIP341_sha_amounts(spent_utxos)
@@ -835,11 +834,11 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index = 0, scriptpat
ss += bytes([spend_type])
if in_type == SIGHASH_ANYONECANPAY:
ss += txTo.vin[input_index].prevout.serialize()
- ss += struct.pack("<q", spent_utxos[input_index].nValue)
+ ss += spent_utxos[input_index].nValue.to_bytes(8, "little", signed=True)
ss += ser_string(spk)
- ss += struct.pack("<I", txTo.vin[input_index].nSequence)
+ ss += txTo.vin[input_index].nSequence.to_bytes(4, "little")
else:
- ss += struct.pack("<I", input_index)
+ ss += input_index.to_bytes(4, "little")
if (spend_type & 1):
ss += sha256(ser_string(annex))
if out_type == SIGHASH_SINGLE:
@@ -850,7 +849,7 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index = 0, scriptpat
if (scriptpath):
ss += TaggedHash("TapLeaf", bytes([leaf_ver]) + ser_string(script))
ss += bytes([0])
- ss += struct.pack("<i", codeseparator_pos)
+ ss += codeseparator_pos.to_bytes(4, "little", signed=True)
assert len(ss) == 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37
return ss
diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py
index 62894cc0f4..855f3b8cf5 100755
--- a/test/functional/test_framework/script_util.py
+++ b/test/functional/test_framework/script_util.py
@@ -3,10 +3,13 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Useful Script constants and utils."""
+import unittest
+
from test_framework.script import (
CScript,
- CScriptOp,
OP_0,
+ OP_15,
+ OP_16,
OP_CHECKMULTISIG,
OP_CHECKSIG,
OP_DUP,
@@ -49,10 +52,8 @@ def keys_to_multisig_script(keys, *, k=None):
if k is None: # n-of-n multisig by default
k = n
assert k <= n
- op_k = CScriptOp.encode_op_n(k)
- op_n = CScriptOp.encode_op_n(n)
checked_keys = [check_key(key) for key in keys]
- return CScript([op_k] + checked_keys + [op_n, OP_CHECKMULTISIG])
+ return CScript([k] + checked_keys + [n, OP_CHECKMULTISIG])
def keyhash_to_p2pkh_script(hash):
@@ -125,3 +126,19 @@ def check_script(script):
if isinstance(script, bytes) or isinstance(script, CScript):
return script
assert False
+
+
+class TestFrameworkScriptUtil(unittest.TestCase):
+ def test_multisig(self):
+ fake_pubkey = bytes([0]*33)
+ # check correct encoding of P2MS script with n,k <= 16
+ normal_ms_script = keys_to_multisig_script([fake_pubkey]*16, k=15)
+ self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1)
+ self.assertTrue(normal_ms_script.startswith(bytes([OP_15])))
+ self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG])))
+
+ # check correct encoding of P2MS script with n,k > 16
+ max_ms_script = keys_to_multisig_script([fake_pubkey]*20, k=19)
+ self.assertEqual(len(max_ms_script), 2 + 20*34 + 2 + 1)
+ self.assertTrue(max_ms_script.startswith(bytes([1, 19]))) # using OP_PUSH1
+ self.assertTrue(max_ms_script.endswith(bytes([1, 20, OP_CHECKMULTISIG])))
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index a2f767cc98..9e44a11143 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# Copyright (c) 2014-2022 The Bitcoin Core developers
+# Copyright (c) 2014-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Base class for RPC testing."""
@@ -444,6 +444,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True)
n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=True)
+ # Only enables wallet support when the module is available
+ def enable_wallet_if_possible(self):
+ self._requires_wallet = self.is_wallet_compiled()
+
def run_test(self):
"""Tests must override this method to define test logic"""
raise NotImplementedError
@@ -610,8 +614,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
"""
from_connection = self.nodes[a]
to_connection = self.nodes[b]
- 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(b))
if peer_advertises_v2 is None:
@@ -627,19 +629,28 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
if not wait_for_connect:
return
- # 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['version'] != 0 for peer in to_connection.getpeerinfo()) == to_num_peers)
- self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in from_connection.getpeerinfo()) == from_num_peers)
- self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in to_connection.getpeerinfo()) == to_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) >= 29 for peer in from_connection.getpeerinfo()) == from_num_peers)
- self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 for peer in to_connection.getpeerinfo()) == to_num_peers)
+ # Use subversion as peer id. Test nodes have their node number appended to the user agent string
+ from_connection_subver = from_connection.getnetworkinfo()['subversion']
+ to_connection_subver = to_connection.getnetworkinfo()['subversion']
+
+ def find_conn(node, peer_subversion, inbound):
+ return next(filter(lambda peer: peer['subver'] == peer_subversion and peer['inbound'] == inbound, node.getpeerinfo()), None)
+
+ self.wait_until(lambda: find_conn(from_connection, to_connection_subver, inbound=False) is not None)
+ self.wait_until(lambda: find_conn(to_connection, from_connection_subver, inbound=True) is not None)
+
+ def check_bytesrecv(peer, msg_type, min_bytes_recv):
+ assert peer is not None, "Error: peer disconnected"
+ return peer['bytesrecv_per_msg'].pop(msg_type, 0) >= min_bytes_recv
+
+ # Poll until version handshake (fSuccessfullyConnected) is complete to
+ # avoid race conditions, because some message types are blocked from
+ # being sent or received before fSuccessfullyConnected.
+ #
+ # As the flag fSuccessfullyConnected is not exposed, check it by
+ # waiting for a pong, which can only happen after the flag was set.
+ self.wait_until(lambda: check_bytesrecv(find_conn(from_connection, to_connection_subver, inbound=False), 'pong', 29))
+ self.wait_until(lambda: check_bytesrecv(find_conn(to_connection, from_connection_subver, inbound=True), 'pong', 29))
def disconnect_nodes(self, a, b):
def disconnect_nodes_helper(node_a, node_b):
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
index 4ba92a7b1f..0f0083191d 100755
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -106,7 +106,7 @@ class TestNode():
"-debugexclude=libevent",
"-debugexclude=leveldb",
"-debugexclude=rand",
- "-uacomment=testnode%d" % i,
+ "-uacomment=testnode%d" % i, # required for subversion uniqueness across peers
]
if self.descriptors is None:
self.args.append("-disablewallet")
@@ -241,7 +241,7 @@ class TestNode():
if self.start_perf:
self._start_perf()
- def wait_for_rpc_connection(self):
+ def wait_for_rpc_connection(self, *, wait_for_import=True):
"""Sets up an RPC connection to the bitcoind process. Returns False if unable to connect."""
# Poll at a rate of four times per second
poll_per_s = 4
@@ -263,7 +263,7 @@ class TestNode():
)
rpc.getblockcount()
# If the call to getblockcount() succeeds then the RPC connection is up
- if self.version_is_at_least(190000):
+ if self.version_is_at_least(190000) and wait_for_import:
# getmempoolinfo.loaded is available since commit
# bb8ae2c (version 0.19.0)
self.wait_until(lambda: rpc.getmempoolinfo()['loaded'])
@@ -666,7 +666,7 @@ class TestNode():
assert_msg += "with expected error " + expected_msg
self._raise_assertion_error(assert_msg)
- def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, **kwargs):
+ def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, expect_success=True, **kwargs):
"""Add an inbound p2p connection to the node.
This method adds the p2p connection to the self.p2ps list and also
@@ -686,7 +686,6 @@ class TestNode():
if supports_v2_p2p is None:
supports_v2_p2p = self.use_v2transport
-
p2p_conn.p2p_connected_to_node = True
if self.use_v2transport:
kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2
@@ -694,6 +693,8 @@ class TestNode():
p2p_conn.peer_connect(**kwargs, send_version=send_version, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p)()
self.p2ps.append(p2p_conn)
+ if not expect_success:
+ return p2p_conn
p2p_conn.wait_until(lambda: p2p_conn.is_connected, check_connected=False)
if supports_v2_p2p and wait_for_v2_handshake:
p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake)
diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py
index c5b69a3954..f3d080fdde 100644
--- a/test/functional/test_framework/util.py
+++ b/test/functional/test_framework/util.py
@@ -14,6 +14,7 @@ import logging
import os
import pathlib
import platform
+import random
import re
import time
@@ -247,6 +248,12 @@ def ceildiv(a, b):
return -(-a // b)
+def random_bitflip(data):
+ data = list(data)
+ data[random.randrange(len(data))] ^= (1 << (random.randrange(8)))
+ return bytes(data)
+
+
def get_fee(tx_size, feerate_btc_kvb):
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
diff --git a/test/functional/test_framework/v2_p2p.py b/test/functional/test_framework/v2_p2p.py
index 8f79623bd8..87600c36de 100644
--- a/test/functional/test_framework/v2_p2p.py
+++ b/test/functional/test_framework/v2_p2p.py
@@ -4,7 +4,6 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Class for v2 P2P protocol (see BIP 324)"""
-import logging
import random
from .crypto.bip324_cipher import FSChaCha20Poly1305
@@ -14,14 +13,12 @@ from .crypto.hkdf import hkdf_sha256
from .key import TaggedHash
from .messages import MAGIC_BYTES
-logger = logging.getLogger("TestFramework.v2_p2p")
CHACHA20POLY1305_EXPANSION = 16
HEADER_LEN = 1
IGNORE_BIT_POS = 7
LENGTH_FIELD_LEN = 3
MAX_GARBAGE_LEN = 4095
-TRANSPORT_VERSION = b''
SHORTID = {
1: b"addr",
@@ -95,6 +92,7 @@ class EncryptedP2PState:
# has been decrypted. set to -1 if decryption hasn't been done yet.
self.contents_len = -1
self.found_garbage_terminator = False
+ self.transport_version = b''
@staticmethod
def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating):
@@ -111,12 +109,12 @@ class EncryptedP2PState:
# Responding, place their public key encoding first.
return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32)
- def generate_keypair_and_garbage(self):
+ def generate_keypair_and_garbage(self, garbage_len=None):
"""Generates ellswift keypair and 4095 bytes garbage at max"""
self.privkey_ours, self.ellswift_ours = ellswift_create()
- garbage_len = random.randrange(MAX_GARBAGE_LEN + 1)
+ if garbage_len is None:
+ garbage_len = random.randrange(MAX_GARBAGE_LEN + 1)
self.sent_garbage = random.randbytes(garbage_len)
- logger.debug(f"sending {garbage_len} bytes of garbage data")
return self.ellswift_ours + self.sent_garbage
def initiate_v2_handshake(self):
@@ -172,7 +170,7 @@ class EncryptedP2PState:
msg_to_send += self.v2_enc_packet(decoy_content_len * b'\x00', aad=aad, ignore=True)
aad = b''
# Send version packet.
- msg_to_send += self.v2_enc_packet(TRANSPORT_VERSION, aad=aad)
+ msg_to_send += self.v2_enc_packet(self.transport_version, aad=aad)
return 64 - len(self.received_prefix), msg_to_send
def authenticate_handshake(self, response):
diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py
index 4433cbcc55..cb0d291361 100644
--- a/test/functional/test_framework/wallet.py
+++ b/test/functional/test_framework/wallet.py
@@ -7,6 +7,7 @@
from copy import deepcopy
from decimal import Decimal
from enum import Enum
+import math
from typing import (
Any,
Optional,
@@ -33,10 +34,13 @@ from test_framework.messages import (
CTxInWitness,
CTxOut,
hash256,
+ ser_compact_size,
+ WITNESS_SCALE_FACTOR,
)
from test_framework.script import (
CScript,
LEAF_VERSION_TAPSCRIPT,
+ OP_1,
OP_NOP,
OP_RETURN,
OP_TRUE,
@@ -52,6 +56,7 @@ from test_framework.script_util import (
from test_framework.util import (
assert_equal,
assert_greater_than_or_equal,
+ get_fee,
)
from test_framework.wallet_util import generate_keypair
@@ -119,13 +124,16 @@ class MiniWallet:
"""Pad a transaction with extra outputs until it reaches a target weight (or higher).
returns the tx
"""
- tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'a'])))
+ tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN])))
+ # determine number of needed padding bytes by converting weight difference to vbytes
dummy_vbytes = (target_weight - tx.get_weight() + 3) // 4
- tx.vout[-1].scriptPubKey = CScript([OP_RETURN, b'a' * dummy_vbytes])
- # Lower bound should always be off by at most 3
+ # compensate for the increase of the compact-size encoded script length
+ # (note that the length encoding of the unpadded output script needs one byte)
+ dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1
+ tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes)
+ # Actual weight should be at most 3 higher than target weight
assert_greater_than_or_equal(tx.get_weight(), target_weight)
- # Higher bound should always be off by at most 3 + 12 weight (for encoding the length)
- assert_greater_than_or_equal(target_weight + 15, tx.get_weight())
+ assert_greater_than_or_equal(target_weight + 3, tx.get_weight())
def get_balance(self):
return sum(u['value'] for u in self._utxos)
@@ -321,7 +329,7 @@ class MiniWallet:
tx = CTransaction()
tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)]
tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)]
- tx.nVersion = version
+ tx.version = version
tx.nLockTime = locktime
self.sign_tx(tx)
@@ -367,6 +375,10 @@ class MiniWallet:
vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other)
else:
assert False
+ if target_weight and not fee: # respect fee_rate if target weight is passed
+ # the actual weight might be off by 3 WUs, so calculate based on that (see self._bulk_tx)
+ max_actual_weight = target_weight + 3
+ fee = get_fee(math.ceil(max_actual_weight / WITNESS_SCALE_FACTOR), fee_rate)
send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000))
# create tx
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 725b116281..67693259d3 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -15,8 +15,10 @@ For a description of arguments recognized by test scripts, see
import argparse
from collections import deque
import configparser
+import csv
import datetime
import os
+import pathlib
import platform
import time
import shutil
@@ -262,9 +264,9 @@ BASE_SCRIPTS = [
'p2p_invalid_tx.py --v2transport',
'p2p_v2_transport.py',
'p2p_v2_encrypted.py',
- 'p2p_v2_earlykeyresponse.py',
+ 'p2p_v2_misbehaving.py',
'example_test.py',
- 'mempool_accept_v3.py',
+ 'mempool_truc.py',
'wallet_txn_doublespend.py --legacy-wallet',
'wallet_multisig_descriptor_psbt.py --descriptors',
'wallet_txn_doublespend.py --descriptors',
@@ -280,6 +282,7 @@ BASE_SCRIPTS = [
'mempool_packages.py',
'mempool_package_onemore.py',
'mempool_package_limits.py',
+ 'mempool_package_rbf.py',
'feature_versionbits_warning.py',
'rpc_preciousblock.py',
'wallet_importprunedfunds.py --legacy-wallet',
@@ -361,6 +364,7 @@ BASE_SCRIPTS = [
'feature_addrman.py',
'feature_asmap.py',
'feature_fastprune.py',
+ 'feature_framework_miniwallet.py',
'mempool_unbroadcast.py',
'mempool_compatibility.py',
'mempool_accept_wtxid.py',
@@ -438,6 +442,7 @@ def main():
parser.add_argument('--filter', help='filter scripts to run by regular expression')
parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true",
help="Leave bitcoinds and test.* datadir on exit or error")
+ parser.add_argument('--resultsfile', '-r', help='store test results (as CSV) to the provided file')
args, unknown_args = parser.parse_known_args()
@@ -470,6 +475,13 @@ def main():
logging.debug("Temporary test directory at %s" % tmpdir)
+ results_filepath = None
+ if args.resultsfile:
+ results_filepath = pathlib.Path(args.resultsfile)
+ # Stop early if the parent directory doesn't exist
+ assert results_filepath.parent.exists(), "Results file parent directory does not exist"
+ logging.debug("Test results will be written to " + str(results_filepath))
+
enable_bitcoind = config["components"].getboolean("ENABLE_BITCOIND")
if not enable_bitcoind:
@@ -556,9 +568,10 @@ def main():
combined_logs_len=args.combinedlogslen,
failfast=args.failfast,
use_term_control=args.ansi,
+ results_filepath=results_filepath,
)
-def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage=False, args=None, combined_logs_len=0, failfast=False, use_term_control):
+def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage=False, args=None, combined_logs_len=0, failfast=False, use_term_control, results_filepath=None):
args = args or []
# Warn if bitcoind is already running
@@ -650,11 +663,14 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage=
break
if "[Errno 28] No space left on device" in stdout:
- sys.exit(f"Early exiting after test failure due to insuffient free space in {tmpdir}\n"
+ sys.exit(f"Early exiting after test failure due to insufficient free space in {tmpdir}\n"
f"Test execution data left in {tmpdir}.\n"
f"Additional storage is needed to execute testing.")
- print_results(test_results, max_len_name, (int(time.time() - start_time)))
+ runtime = int(time.time() - start_time)
+ print_results(test_results, max_len_name, runtime)
+ if results_filepath:
+ write_results(test_results, results_filepath, runtime)
if coverage:
coverage_passed = coverage.report_rpc_coverage()
@@ -701,6 +717,17 @@ def print_results(test_results, max_len_name, runtime):
results += "Runtime: %s s\n" % (runtime)
print(results)
+
+def write_results(test_results, filepath, total_runtime):
+ with open(filepath, mode="w", encoding="utf8") as results_file:
+ results_writer = csv.writer(results_file)
+ results_writer.writerow(['test', 'status', 'duration(seconds)'])
+ all_passed = True
+ for test_result in test_results:
+ all_passed = all_passed and test_result.was_successful
+ results_writer.writerow([test_result.name, test_result.status, str(test_result.time)])
+ results_writer.writerow(['ALL', ("Passed" if all_passed else "Failed"), str(total_runtime)])
+
class TestHandler:
"""
Trigger the test scripts passed in via the list.
diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py
index c322ae52c1..2c85773bf3 100755
--- a/test/functional/wallet_balance.py
+++ b/test/functional/wallet_balance.py
@@ -4,7 +4,6 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the wallet balance RPC methods."""
from decimal import Decimal
-import struct
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY
from test_framework.blocktools import COINBASE_MATURITY
@@ -266,8 +265,8 @@ class WalletTest(BitcoinTestFramework):
tx_orig = self.nodes[0].gettransaction(txid)['hex']
# Increase fee by 1 coin
tx_replace = tx_orig.replace(
- struct.pack("<q", 99 * 10**8).hex(),
- struct.pack("<q", 98 * 10**8).hex(),
+ (99 * 10**8).to_bytes(8, "little", signed=True).hex(),
+ (98 * 10**8).to_bytes(8, "little", signed=True).hex(),
)
tx_replace = self.nodes[0].signrawtransactionwithwallet(tx_replace)['hex']
# Total balance is given by the sum of outputs of the tx
diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py
index 5b7db55f45..6d45adc823 100755
--- a/test/functional/wallet_bumpfee.py
+++ b/test/functional/wallet_bumpfee.py
@@ -117,6 +117,7 @@ class BumpFeeTest(BitcoinTestFramework):
# Context independent tests
test_feerate_checks_replaced_outputs(self, rbf_node, peer_node)
+ test_bumpfee_with_feerate_ignores_walletincrementalrelayfee(self, rbf_node, peer_node)
def test_invalid_parameters(self, rbf_node, peer_node, dest_address):
self.log.info('Test invalid parameters')
@@ -816,7 +817,7 @@ def test_feerate_checks_replaced_outputs(self, rbf_node, peer_node):
# Since the bumped tx will replace all of the outputs with a single output, we can estimate that its size will 31 * (len(outputs) - 1) bytes smaller
tx_size = tx_details["decoded"]["vsize"]
est_bumped_size = tx_size - (len(tx_details["decoded"]["vout"]) - 1) * 31
- inc_fee_rate = max(rbf_node.getmempoolinfo()["incrementalrelayfee"], Decimal(0.00005000)) # Wallet has a fixed incremental relay fee of 5 sat/vb
+ inc_fee_rate = rbf_node.getmempoolinfo()["incrementalrelayfee"]
# RPC gives us fee as negative
min_fee = (-tx_details["fee"] + get_fee(est_bumped_size, inc_fee_rate)) * Decimal(1e8)
min_fee_rate = (min_fee / est_bumped_size).quantize(Decimal("1.000"))
@@ -830,5 +831,27 @@ def test_feerate_checks_replaced_outputs(self, rbf_node, peer_node):
self.clear_mempool()
+def test_bumpfee_with_feerate_ignores_walletincrementalrelayfee(self, rbf_node, peer_node):
+ self.log.info('Test that bumpfee with fee_rate ignores walletincrementalrelayfee')
+ # Make sure there is enough balance
+ peer_node.sendtoaddress(rbf_node.getnewaddress(), 2)
+ self.generate(peer_node, 1)
+
+ dest_address = peer_node.getnewaddress(address_type="bech32")
+ tx = rbf_node.send(outputs=[{dest_address: 1}], fee_rate=2)
+
+ # Ensure you can not fee bump with a fee_rate below or equal to the original fee_rate
+ assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx["txid"], {"fee_rate": 1})
+ assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx["txid"], {"fee_rate": 2})
+
+ # Ensure you can not fee bump if the fee_rate is more than original fee_rate but the total fee from new fee_rate is
+ # less than (original fee + incrementalrelayfee)
+ assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx["txid"], {"fee_rate": 2.8})
+
+ # You can fee bump as long as the new fee set from fee_rate is atleast (original fee + incrementalrelayfee)
+ rbf_node.bumpfee(tx["txid"], {"fee_rate": 3})
+ self.clear_mempool()
+
+
if __name__ == "__main__":
BumpFeeTest().main()
diff --git a/test/functional/wallet_conflicts.py b/test/functional/wallet_conflicts.py
index e5739a6a59..25a95aa954 100755
--- a/test/functional/wallet_conflicts.py
+++ b/test/functional/wallet_conflicts.py
@@ -9,7 +9,6 @@ Test that wallet correctly tracks transactions that have been conflicted by bloc
from decimal import Decimal
-from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -37,7 +36,6 @@ class TxConflicts(BitcoinTestFramework):
"""
self.test_block_conflicts()
- self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 7, self.nodes[2].getnewaddress())
self.test_mempool_conflict()
self.test_mempool_and_block_conflicts()
self.test_descendants_with_mempool_conflicts()
diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py
index 4e31b48ec0..41ddb2bc69 100755
--- a/test/functional/wallet_create_tx.py
+++ b/test/functional/wallet_create_tx.py
@@ -3,6 +3,9 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+from test_framework.messages import (
+ tx_from_hex,
+)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -33,6 +36,7 @@ class CreateTxWalletTest(BitcoinTestFramework):
self.test_anti_fee_sniping()
self.test_tx_size_too_large()
self.test_create_too_long_mempool_chain()
+ self.test_version3()
def test_anti_fee_sniping(self):
self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled')
@@ -106,6 +110,23 @@ class CreateTxWalletTest(BitcoinTestFramework):
test_wallet.unloadwallet()
+ def test_version3(self):
+ self.log.info('Check wallet does not create transactions with version=3 yet')
+ wallet_rpc = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+
+ self.nodes[0].createwallet("version3")
+ wallet_v3 = self.nodes[0].get_wallet_rpc("version3")
+
+ tx_data = wallet_rpc.send(outputs=[{wallet_v3.getnewaddress(): 25}], options={"change_position": 0})
+ wallet_tx_data = wallet_rpc.gettransaction(tx_data["txid"])
+ tx_current_version = tx_from_hex(wallet_tx_data["hex"])
+
+ # While version=3 transactions are standard, the CURRENT_VERSION is 2.
+ # This test can be removed if CURRENT_VERSION is changed, and replaced with tests that the
+ # wallet handles TRUC rules properly.
+ assert_equal(tx_current_version.version, 2)
+ wallet_v3.unloadwallet()
+
if __name__ == '__main__':
CreateTxWalletTest().main()
diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
index 71c883f166..3c1b2deb1d 100755
--- a/test/functional/wallet_fundrawtransaction.py
+++ b/test/functional/wallet_fundrawtransaction.py
@@ -114,6 +114,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.test_add_inputs_default_value()
self.test_preset_inputs_selection()
self.test_weight_calculation()
+ self.test_weight_limits()
self.test_change_position()
self.test_simple()
self.test_simple_two_coins()
@@ -1312,6 +1313,38 @@ class RawTransactionsTest(BitcoinTestFramework):
self.nodes[2].unloadwallet("test_weight_calculation")
+ def test_weight_limits(self):
+ self.log.info("Test weight limits")
+
+ self.nodes[2].createwallet("test_weight_limits")
+ wallet = self.nodes[2].get_wallet_rpc("test_weight_limits")
+
+ outputs = []
+ for _ in range(1472):
+ outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1})
+ txid = self.nodes[0].send(outputs=outputs)["txid"]
+ self.generate(self.nodes[0], 1)
+
+ # 272 WU per input (273 when high-s); picking 1471 inputs will exceed the max standard tx weight.
+ rawtx = wallet.createrawtransaction([], [{wallet.getnewaddress(): 0.1 * 1471}])
+
+ # 1) Try to fund transaction only using the preset inputs
+ input_weights = []
+ for i in range(1471):
+ input_weights.append({"txid": txid, "vout": i, "weight": 273})
+ assert_raises_rpc_error(-4, "Transaction too large", wallet.fundrawtransaction, hexstring=rawtx, input_weights=input_weights)
+
+ # 2) Let the wallet fund the transaction
+ assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
+ wallet.fundrawtransaction, hexstring=rawtx)
+
+ # 3) Pre-select some inputs and let the wallet fill-up the remaining amount
+ inputs = input_weights[0:1000]
+ assert_raises_rpc_error(-4, "The combination of the pre-selected inputs and the wallet automatic inputs selection exceeds the transaction maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
+ wallet.fundrawtransaction, hexstring=rawtx, input_weights=inputs)
+
+ self.nodes[2].unloadwallet("test_weight_limits")
+
def test_include_unsafe(self):
self.log.info("Test fundrawtxn with unsafe inputs")
diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py
index fd586d546e..15214539a9 100755
--- a/test/functional/wallet_listsinceblock.py
+++ b/test/functional/wallet_listsinceblock.py
@@ -40,6 +40,7 @@ class ListSinceBlockTest(BitcoinTestFramework):
self.test_no_blockhash()
self.test_invalid_blockhash()
self.test_reorg()
+ self.test_cant_read_block()
self.test_double_spend()
self.test_double_send()
self.double_spends_filtered()
@@ -167,6 +168,31 @@ class ListSinceBlockTest(BitcoinTestFramework):
found = next(tx for tx in transactions if tx['txid'] == senttx)
assert_equal(found['blockheight'], self.nodes[0].getblockheader(nodes2_first_blockhash)['height'])
+ def test_cant_read_block(self):
+ self.log.info('Test the RPC error "Can\'t read block from disk"')
+
+ # Split network into two
+ self.split_network()
+
+ # generate on both sides
+ nodes1_last_blockhash = self.generate(self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1]
+ self.generate(self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0]
+
+ self.join_network()
+
+ # Renaming the block file to induce unsuccessful block read
+ blk_dat = (self.nodes[0].blocks_path / "blk00000.dat")
+ blk_dat_moved = blk_dat.rename(self.nodes[0].blocks_path / "blk00000.dat.moved")
+ assert not blk_dat.exists()
+
+ # listsinceblock(nodes1_last_blockhash) should now fail as blocks are not accessible
+ assert_raises_rpc_error(-32603, "Can't read block from disk",
+ self.nodes[0].listsinceblock, nodes1_last_blockhash)
+
+ # Restoring block file
+ blk_dat_moved.rename(self.nodes[0].blocks_path / "blk00000.dat")
+ assert blk_dat.exists()
+
def test_double_spend(self):
'''
This tests the case where the same UTXO is spent twice on two separate
diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py
index 68bf45f7e3..145912025f 100755
--- a/test/functional/wallet_multisig_descriptor_psbt.py
+++ b/test/functional/wallet_multisig_descriptor_psbt.py
@@ -7,7 +7,6 @@
This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible.
"""
-from test_framework.address import base58_to_byte
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_approx,
@@ -30,10 +29,12 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
self.skip_if_no_sqlite()
@staticmethod
- def _get_xpub(wallet):
+ def _get_xpub(wallet, internal):
"""Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
- descriptor = next(filter(lambda d: d["desc"].startswith("pkh"), wallet.listdescriptors()["descriptors"]))
- return descriptor["desc"].split("]")[-1].split("/")[0]
+ pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
+ # Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
+ # See section 'Key origin identification' in 'doc/descriptors.md' for more details...
+ return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
@staticmethod
def _check_psbt(psbt, to, value, multisig):
@@ -47,19 +48,13 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
amount += vout["value"]
assert_approx(amount, float(value), vspan=0.001)
- def participants_create_multisigs(self, xpubs):
+ def participants_create_multisigs(self, external_xpubs, internal_xpubs):
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
- # some simple validation
- assert_equal(len(xpubs), self.N)
- # a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid
- for xpub in xpubs:
- base58_to_byte(xpub)
-
for i, node in enumerate(self.nodes):
node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True)
multisig = node.get_wallet_rpc(f"{self.name}_{i}")
- external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/0/*,'.join(xpubs)}/0/*))")
- internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/1/*,'.join(xpubs)}/1/*))")
+ external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f','.join(external_xpubs)}))")
+ internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f','.join(internal_xpubs)}))")
result = multisig.importdescriptors([
{ # receiving addresses (internal: False)
"desc": external["descriptor"],
@@ -93,10 +88,10 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
}
self.log.info("Generate and exchange xpubs...")
- xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
+ external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in participants["signers"]] for internal in [False, True]]
self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
- participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
+ participants["multisigs"] = list(self.participants_create_multisigs(external_xpubs, internal_xpubs))
self.log.info("Check that every participant's multisig generates the same addresses...")
for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs
diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py
index 0a0a8dba0d..bbb0d658d9 100755
--- a/test/functional/wallet_send.py
+++ b/test/functional/wallet_send.py
@@ -577,5 +577,39 @@ class WalletSendTest(BitcoinTestFramework):
# but rounded to nearest integer, it should be the same as the target fee rate
assert_equal(round(actual_fee_rate_sat_vb), target_fee_rate_sat_vb)
+ # Check tx creation size limits
+ self.test_weight_limits()
+
+ def test_weight_limits(self):
+ self.log.info("Test weight limits")
+
+ self.nodes[1].createwallet("test_weight_limits")
+ wallet = self.nodes[1].get_wallet_rpc("test_weight_limits")
+
+ # Generate future inputs; 272 WU per input (273 when high-s).
+ # Picking 1471 inputs will exceed the max standard tx weight.
+ outputs = []
+ for _ in range(1472):
+ outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1})
+ self.nodes[0].send(outputs=outputs)
+ self.generate(self.nodes[0], 1)
+
+ # 1) Try to fund transaction only using the preset inputs
+ inputs = wallet.listunspent()
+ assert_raises_rpc_error(-4, "Transaction too large",
+ wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}], options={"inputs": inputs, "add_inputs": False})
+
+ # 2) Let the wallet fund the transaction
+ assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
+ wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}])
+
+ # 3) Pre-select some inputs and let the wallet fill-up the remaining amount
+ inputs = inputs[0:1000]
+ assert_raises_rpc_error(-4, "The combination of the pre-selected inputs and the wallet automatic inputs selection exceeds the transaction maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
+ wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}], options={"inputs": inputs, "add_inputs": True})
+
+ self.nodes[1].unloadwallet("test_weight_limits")
+
+
if __name__ == '__main__':
WalletSendTest().main()
diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py
index c2b800df21..1d308c225d 100755
--- a/test/functional/wallet_sendall.py
+++ b/test/functional/wallet_sendall.py
@@ -379,6 +379,64 @@ class SendallTest(BitcoinTestFramework):
assert_equal(len(self.wallet.listunspent()), 1)
assert_equal(self.wallet.listunspent()[0]['confirmations'], 6)
+ @cleanup
+ def sendall_spends_unconfirmed_change(self):
+ self.log.info("Test that sendall spends unconfirmed change")
+ self.add_utxos([17])
+ self.wallet.sendtoaddress(self.remainder_target, 10)
+ assert_greater_than(self.wallet.getbalances()["mine"]["trusted"], 6)
+ self.test_sendall_success(sendall_args = [self.remainder_target])
+
+ assert_equal(self.wallet.getbalance(), 0)
+
+ @cleanup
+ def sendall_spends_unconfirmed_inputs_if_specified(self):
+ self.log.info("Test that sendall spends specified unconfirmed inputs")
+ self.def_wallet.sendtoaddress(self.wallet.getnewaddress(), 17)
+ self.wallet.syncwithvalidationinterfacequeue()
+ assert_equal(self.wallet.getbalances()["mine"]["untrusted_pending"], 17)
+ unspent = self.wallet.listunspent(minconf=0)[0]
+
+ self.wallet.sendall(recipients=[self.remainder_target], inputs=[unspent])
+ assert_equal(self.wallet.getbalance(), 0)
+
+ @cleanup
+ def sendall_does_ancestor_aware_funding(self):
+ self.log.info("Test that sendall does ancestor aware funding for unconfirmed inputs")
+
+ # higher parent feerate
+ self.def_wallet.sendtoaddress(address=self.wallet.getnewaddress(), amount=17, fee_rate=20)
+ self.wallet.syncwithvalidationinterfacequeue()
+
+ assert_equal(self.wallet.getbalances()["mine"]["untrusted_pending"], 17)
+ unspent = self.wallet.listunspent(minconf=0)[0]
+
+ parent_txid = unspent["txid"]
+ assert_equal(self.wallet.gettransaction(parent_txid)["confirmations"], 0)
+
+ res_1 = self.wallet.sendall(recipients=[self.def_wallet.getnewaddress()], inputs=[unspent], fee_rate=20, add_to_wallet=False, lock_unspents=True)
+ child_hex = res_1["hex"]
+
+ child_tx = self.wallet.decoderawtransaction(child_hex)
+ higher_parent_feerate_amount = child_tx["vout"][0]["value"]
+
+ # lower parent feerate
+ self.def_wallet.sendtoaddress(address=self.wallet.getnewaddress(), amount=17, fee_rate=10)
+ self.wallet.syncwithvalidationinterfacequeue()
+ assert_equal(self.wallet.getbalances()["mine"]["untrusted_pending"], 34)
+ unspent = self.wallet.listunspent(minconf=0)[0]
+
+ parent_txid = unspent["txid"]
+ assert_equal(self.wallet.gettransaction(parent_txid)["confirmations"], 0)
+
+ res_2 = self.wallet.sendall(recipients=[self.def_wallet.getnewaddress()], inputs=[unspent], fee_rate=20, add_to_wallet=False, lock_unspents=True)
+ child_hex = res_2["hex"]
+
+ child_tx = self.wallet.decoderawtransaction(child_hex)
+ lower_parent_feerate_amount = child_tx["vout"][0]["value"]
+
+ assert_greater_than(higher_parent_feerate_amount, lower_parent_feerate_amount)
+
# This tests needs to be the last one otherwise @cleanup will fail with "Transaction too large" error
def sendall_fails_with_transaction_too_large(self):
self.log.info("Test that sendall fails if resulting transaction is too large")
@@ -460,6 +518,15 @@ class SendallTest(BitcoinTestFramework):
# Sendall only uses outputs with less than a given number of confirmation when using minconf
self.sendall_with_maxconf()
+ # Sendall spends unconfirmed change
+ self.sendall_spends_unconfirmed_change()
+
+ # Sendall spends unconfirmed inputs if they are specified
+ self.sendall_spends_unconfirmed_inputs_if_specified()
+
+ # Sendall does ancestor aware funding when spending an unconfirmed UTXO
+ self.sendall_does_ancestor_aware_funding()
+
# Sendall fails when many inputs result to too large transaction
self.sendall_fails_with_transaction_too_large()