diff options
Diffstat (limited to 'test')
37 files changed, 612 insertions, 574 deletions
diff --git a/test/config.ini.in b/test/config.ini.in index 5888ef443b..af3d994c84 100644 --- a/test/config.ini.in +++ b/test/config.ini.in @@ -25,5 +25,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py @ENABLE_FUZZ_TRUE@ENABLE_FUZZ=true @ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true @ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true -@ENABLE_SYSCALL_SANDBOX_TRUE@ENABLE_SYSCALL_SANDBOX=true @ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 36ee79dab9..613d2eab14 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -35,7 +35,6 @@ from test_framework.blocktools import ( create_block, create_coinbase, ) -from test_framework.key import ECKey from test_framework.messages import ( CBlockHeader, COutPoint, @@ -46,9 +45,13 @@ from test_framework.messages import ( msg_headers, ) from test_framework.p2p import P2PInterface -from test_framework.script import (CScript, OP_TRUE) +from test_framework.script import ( + CScript, + OP_TRUE, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +from test_framework.wallet_util import generate_keypair class BaseNode(P2PInterface): @@ -90,9 +93,7 @@ class AssumeValidTest(BitcoinTestFramework): self.blocks = [] # Get a pubkey for the coinbase TXO - coinbase_key = ECKey() - coinbase_key.generate() - coinbase_pubkey = coinbase_key.get_pubkey().get_bytes() + _, coinbase_pubkey = generate_keypair() # Create the first block with a coinbase output to our key height = 1 diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 1080e77c40..765db97445 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -14,7 +14,6 @@ from test_framework.blocktools import ( get_legacy_sigopcount_block, MAX_BLOCK_SIGOPS, ) -from test_framework.key import ECKey from test_framework.messages import ( CBlock, COIN, @@ -55,6 +54,7 @@ from test_framework.util import ( assert_equal, assert_greater_than, ) +from test_framework.wallet_util import generate_keypair from data import invalid_txs @@ -98,9 +98,7 @@ class FullBlockTest(BitcoinTestFramework): self.bootstrap_p2p() # Add one p2p connection to the node self.block_heights = {} - self.coinbase_key = ECKey() - self.coinbase_key.generate() - self.coinbase_pubkey = self.coinbase_key.get_pubkey().get_bytes() + self.coinbase_key, self.coinbase_pubkey = generate_keypair() self.tip = None self.blocks = {} self.genesis_hash = int(self.nodes[0].getbestblockhash(), 16) diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index e5fc4f6885..2927355bda 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -253,7 +253,7 @@ class ConfArgsTest(BitcoinTestFramework): with self.nodes[0].assert_debug_log(expected_msgs=[ "Loaded 0 addresses from peers.dat", "DNS seeding disabled", - "Adding fixed seeds as -dnsseed=0 (or IPv4/IPv6 connections are disabled via -onlynet), -addnode is not provided and all -seednode(s) attempted\n", + "Adding fixed seeds as -dnsseed=0 (or IPv4/IPv6 connections are disabled via -onlynet) and neither -addnode nor -seednode are provided\n", ]): self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1']) assert time.time() - start < 60 diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 8cb633d454..adf6c13973 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -30,9 +30,6 @@ class NotificationsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True - # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify, - # -blocknotify, -walletnotify or -shutdownnotify (which all invoke execve). - self.disable_syscall_sandbox = True def setup_network(self): self.wallet = ''.join(chr(i) for i in range(FILE_CHAR_START, FILE_CHAR_END) if chr(i) not in FILE_CHARS_DISALLOWED) diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index c95657dbbb..7b2a29bdb4 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -35,8 +35,7 @@ from test_framework.util import ( assert_raises_rpc_error, ) from test_framework.wallet import getnewdestination -from test_framework.key import ECKey -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair NULLDUMMY_ERROR = "non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" @@ -71,12 +70,9 @@ class NULLDUMMYTest(BitcoinTestFramework): return tx_from_hex(signedtx["hex"]) def run_test(self): - eckey = ECKey() - eckey.generate() - self.privkey = bytes_to_wif(eckey.get_bytes()) - self.pubkey = eckey.get_pubkey().get_bytes().hex() - cms = self.nodes[0].createmultisig(1, [self.pubkey]) - wms = self.nodes[0].createmultisig(1, [self.pubkey], 'p2sh-segwit') + self.privkey, self.pubkey = generate_keypair(wif=True) + cms = self.nodes[0].createmultisig(1, [self.pubkey.hex()]) + wms = self.nodes[0].createmultisig(1, [self.pubkey.hex()], 'p2sh-segwit') self.ms_address = cms["address"] ms_unlock_details = {"scriptPubKey": address_to_scriptpubkey(self.ms_address).hex(), "redeemScript": cms["redeemScript"]} diff --git a/test/functional/feature_startupnotify.py b/test/functional/feature_startupnotify.py index 4eb6791123..a8e62c6244 100755 --- a/test/functional/feature_startupnotify.py +++ b/test/functional/feature_startupnotify.py @@ -15,7 +15,6 @@ FILE_NAME = "test.txt" class StartupNotifyTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.disable_syscall_sandbox = True def run_test(self): tmpdir_file = self.nodes[0].datadir_path / FILE_NAME diff --git a/test/functional/feature_syscall_sandbox.py b/test/functional/feature_syscall_sandbox.py deleted file mode 100755 index 2200f6c2e6..0000000000 --- a/test/functional/feature_syscall_sandbox.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2021-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. -"""Test bitcoind aborts if a disallowed syscall is used when compiled with the syscall sandbox.""" - -from test_framework.test_framework import BitcoinTestFramework, SkipTest - - -class SyscallSandboxTest(BitcoinTestFramework): - def set_test_params(self): - self.num_nodes = 1 - - def skip_test_if_missing_module(self): - if not self.is_syscall_sandbox_compiled(): - raise SkipTest("bitcoind has not been built with syscall sandbox enabled.") - if self.disable_syscall_sandbox: - raise SkipTest("--nosandbox passed to test runner.") - - def run_test(self): - disallowed_syscall_terminated_bitcoind = False - expected_log_entry = 'ERROR: The syscall "getgroups" (syscall number 115) is not allowed by the syscall sandbox' - with self.nodes[0].assert_debug_log([expected_log_entry]): - self.log.info("Invoking disallowed syscall") - try: - self.nodes[0].invokedisallowedsyscall() - except ConnectionError: - disallowed_syscall_terminated_bitcoind = True - assert disallowed_syscall_terminated_bitcoind - self.nodes = [] - - -if __name__ == "__main__": - SyscallSandboxTest().main() diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 8be2040d91..e32319961e 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -97,14 +97,15 @@ from test_framework.util import ( assert_equal, random_bytes, ) +from test_framework.wallet_util import generate_keypair from test_framework.key import ( generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey, - SECP256K1 ) +from test_framework import secp256k1 from test_framework.address import ( hash160, program_to_witness, @@ -694,7 +695,7 @@ def spenders_taproot_active(): # Generate an invalid public key while True: invalid_pub = random_bytes(32) - if not SECP256K1.is_x_coord(int.from_bytes(invalid_pub, 'big')): + if not secp256k1.GE.is_valid_x(int.from_bytes(invalid_pub, 'big')): break # Implement a test case that detects validation logic which maps invalid public keys to the @@ -738,7 +739,11 @@ def spenders_taproot_active(): scripts = [ ("pk_codesep", CScript(random_checksig_style(pubs[1]) + bytes([OP_CODESEPARATOR]))), # codesep after checksig ("codesep_pk", CScript(bytes([OP_CODESEPARATOR]) + random_checksig_style(pubs[1]))), # codesep before checksig - ("branched_codesep", CScript([random_bytes(random.randrange(511)), OP_DROP, OP_IF, OP_CODESEPARATOR, pubs[0], OP_ELSE, OP_CODESEPARATOR, pubs[1], OP_ENDIF, OP_CHECKSIG])), # branch dependent codesep + ("branched_codesep", CScript([random_bytes(random.randrange(2, 511)), OP_DROP, OP_IF, OP_CODESEPARATOR, pubs[0], OP_ELSE, OP_CODESEPARATOR, pubs[1], OP_ENDIF, OP_CHECKSIG])), # branch dependent codesep + # Note that the first data push in the "branched_codesep" script has the purpose of + # randomizing the sighash, both by varying script size and content. In order to + # avoid MINIMALDATA script verification errors caused by not-minimal-encoded data + # pushes (e.g. `OP_PUSH1 1` instead of `OP_1`), we set a minimum data size of 2 bytes. ] random.shuffle(scripts) tap = taproot_construct(pubs[0], scripts) @@ -1186,11 +1191,8 @@ def spenders_taproot_active(): # Also add a few legacy spends into the mix, so that transactions which combine taproot and pre-taproot spends get tested too. for compressed in [False, True]: - eckey1 = ECKey() - eckey1.set(generate_privkey(), compressed) - pubkey1 = eckey1.get_pubkey().get_bytes() - eckey2 = ECKey() - eckey2.set(generate_privkey(), compressed) + eckey1, pubkey1 = generate_keypair(compressed=compressed) + eckey2, _ = generate_keypair(compressed=compressed) for p2sh in [False, True]: for witv0 in [False, True]: for hashtype in VALID_SIGHASHES_ECDSA + [random.randrange(0x04, 0x80), random.randrange(0x84, 0x100)]: diff --git a/test/functional/feature_versionbits_warning.py b/test/functional/feature_versionbits_warning.py index 0a9e1d4448..073d3de812 100755 --- a/test/functional/feature_versionbits_warning.py +++ b/test/functional/feature_versionbits_warning.py @@ -28,9 +28,6 @@ class VersionBitsWarningTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - # The experimental syscall sandbox feature (-sandbox) is not compatible with -alertnotify - # (which invokes execve). - self.disable_syscall_sandbox = True def setup_network(self): self.alert_filename = os.path.join(self.options.tmpdir, "alert.txt") diff --git a/test/functional/interface_usdt_mempool.py b/test/functional/interface_usdt_mempool.py index 7f088a3ca8..f138fa44cc 100755 --- a/test/functional/interface_usdt_mempool.py +++ b/test/functional/interface_usdt_mempool.py @@ -139,6 +139,7 @@ class MempoolTracepointTest(BitcoinTestFramework): EXPECTED_ADDED_EVENTS = 1 handled_added_events = 0 + event = None self.log.info("Hooking into mempool:added tracepoint...") node = self.nodes[0] @@ -147,11 +148,8 @@ class MempoolTracepointTest(BitcoinTestFramework): bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) def handle_added_event(_, data, __): - nonlocal handled_added_events + nonlocal event, handled_added_events event = bpf["added_events"].event(data) - assert_equal(txid, bytes(event.hash)[::-1].hex()) - assert_equal(vsize, event.vsize) - assert_equal(fee, event.fee) handled_added_events += 1 bpf["added_events"].open_perf_buffer(handle_added_event) @@ -159,9 +157,6 @@ class MempoolTracepointTest(BitcoinTestFramework): self.log.info("Sending transaction...") fee = Decimal(31200) tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN) - # expected data - txid = tx["txid"] - vsize = tx["tx"].get_vsize() self.log.info("Polling buffer...") bpf.perf_buffer_poll(timeout=200) @@ -169,10 +164,13 @@ class MempoolTracepointTest(BitcoinTestFramework): self.log.info("Cleaning up mempool...") self.generate(node, 1) - bpf.cleanup() - self.log.info("Ensuring mempool:added event was handled successfully...") assert_equal(EXPECTED_ADDED_EVENTS, handled_added_events) + assert_equal(bytes(event.hash)[::-1].hex(), tx["txid"]) + assert_equal(event.vsize, tx["tx"].get_vsize()) + assert_equal(event.fee, fee) + + bpf.cleanup() self.generate(self.wallet, 1) def removed_test(self): @@ -181,6 +179,7 @@ class MempoolTracepointTest(BitcoinTestFramework): EXPECTED_REMOVED_EVENTS = 1 handled_removed_events = 0 + event = None self.log.info("Hooking into mempool:removed tracepoint...") node = self.nodes[0] @@ -189,13 +188,8 @@ class MempoolTracepointTest(BitcoinTestFramework): bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) def handle_removed_event(_, data, __): - nonlocal handled_removed_events + nonlocal event, handled_removed_events event = bpf["removed_events"].event(data) - assert_equal(txid, bytes(event.hash)[::-1].hex()) - assert_equal(reason, event.reason.decode("UTF-8")) - assert_equal(vsize, event.vsize) - assert_equal(fee, event.fee) - assert_equal(entry_time, event.entry_time) handled_removed_events += 1 bpf["removed_events"].open_perf_buffer(handle_removed_event) @@ -203,10 +197,7 @@ class MempoolTracepointTest(BitcoinTestFramework): self.log.info("Sending transaction...") fee = Decimal(31200) tx = self.wallet.send_self_transfer(from_node=node, fee=fee / COIN) - # expected data txid = tx["txid"] - reason = "expiry" - vsize = tx["tx"].get_vsize() self.log.info("Fast-forwarding time to mempool expiry...") entry_time = node.getmempoolentry(txid)["time"] @@ -220,10 +211,15 @@ class MempoolTracepointTest(BitcoinTestFramework): self.log.info("Polling buffer...") bpf.perf_buffer_poll(timeout=200) - bpf.cleanup() - self.log.info("Ensuring mempool:removed event was handled successfully...") assert_equal(EXPECTED_REMOVED_EVENTS, handled_removed_events) + assert_equal(bytes(event.hash)[::-1].hex(), txid) + assert_equal(event.reason.decode("UTF-8"), "expiry") + assert_equal(event.vsize, tx["tx"].get_vsize()) + assert_equal(event.fee, fee) + assert_equal(event.entry_time, entry_time) + + bpf.cleanup() self.generate(self.wallet, 1) def replaced_test(self): @@ -232,6 +228,7 @@ class MempoolTracepointTest(BitcoinTestFramework): EXPECTED_REPLACED_EVENTS = 1 handled_replaced_events = 0 + event = None self.log.info("Hooking into mempool:replaced tracepoint...") node = self.nodes[0] @@ -240,15 +237,8 @@ class MempoolTracepointTest(BitcoinTestFramework): bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) def handle_replaced_event(_, data, __): - nonlocal handled_replaced_events + nonlocal event, handled_replaced_events event = bpf["replaced_events"].event(data) - assert_equal(replaced_txid, bytes(event.replaced_hash)[::-1].hex()) - assert_equal(replaced_vsize, event.replaced_vsize) - assert_equal(replaced_fee, event.replaced_fee) - assert_equal(replaced_entry_time, event.replaced_entry_time) - assert_equal(replacement_txid, bytes(event.replacement_hash)[::-1].hex()) - assert_equal(replacement_vsize, event.replacement_vsize) - assert_equal(replacement_fee, event.replacement_fee) handled_replaced_events += 1 bpf["replaced_events"].open_perf_buffer(handle_replaced_event) @@ -267,21 +257,20 @@ class MempoolTracepointTest(BitcoinTestFramework): from_node=node, utxo_to_spend=utxo, fee=replacement_fee / COIN ) - # expected data - replaced_txid = original_tx["txid"] - replaced_vsize = original_tx["tx"].get_vsize() - replaced_fee = original_fee - replaced_entry_time = entry_time - replacement_txid = replacement_tx["txid"] - replacement_vsize = replacement_tx["tx"].get_vsize() - self.log.info("Polling buffer...") bpf.perf_buffer_poll(timeout=200) - bpf.cleanup() - self.log.info("Ensuring mempool:replaced event was handled successfully...") assert_equal(EXPECTED_REPLACED_EVENTS, handled_replaced_events) + assert_equal(bytes(event.replaced_hash)[::-1].hex(), original_tx["txid"]) + assert_equal(event.replaced_vsize, original_tx["tx"].get_vsize()) + assert_equal(event.replaced_fee, original_fee) + assert_equal(event.replaced_entry_time, entry_time) + assert_equal(bytes(event.replacement_hash)[::-1].hex(), replacement_tx["txid"]) + assert_equal(event.replacement_vsize, replacement_tx["tx"].get_vsize()) + assert_equal(event.replacement_fee, replacement_fee) + + bpf.cleanup() self.generate(self.wallet, 1) def rejected_test(self): @@ -290,6 +279,7 @@ class MempoolTracepointTest(BitcoinTestFramework): EXPECTED_REJECTED_EVENTS = 1 handled_rejected_events = 0 + event = None self.log.info("Adding P2P connection...") node = self.nodes[0] @@ -301,10 +291,8 @@ class MempoolTracepointTest(BitcoinTestFramework): bpf = BPF(text=MEMPOOL_TRACEPOINTS_PROGRAM, usdt_contexts=[ctx], debug=0) def handle_rejected_event(_, data, __): - nonlocal handled_rejected_events + nonlocal event, handled_rejected_events event = bpf["rejected_events"].event(data) - assert_equal(txid, bytes(event.hash)[::-1].hex()) - assert_equal(reason, event.reason.decode("UTF-8")) handled_rejected_events += 1 bpf["rejected_events"].open_perf_buffer(handle_rejected_event) @@ -313,17 +301,15 @@ class MempoolTracepointTest(BitcoinTestFramework): tx = self.wallet.create_self_transfer(fee_rate=Decimal(0)) node.p2ps[0].send_txs_and_test([tx["tx"]], node, success=False) - # expected data - txid = tx["tx"].hash - reason = "min relay fee not met" - self.log.info("Polling buffer...") bpf.perf_buffer_poll(timeout=200) - bpf.cleanup() - self.log.info("Ensuring mempool:rejected event was handled successfully...") assert_equal(EXPECTED_REJECTED_EVENTS, handled_rejected_events) + assert_equal(bytes(event.hash)[::-1].hex(), tx["tx"].hash) + assert_equal(event.reason.decode("UTF-8"), "min relay fee not met") + + bpf.cleanup() self.generate(self.wallet, 1) def run_test(self): diff --git a/test/functional/interface_usdt_net.py b/test/functional/interface_usdt_net.py index 2235da702b..d1f94637c9 100755 --- a/test/functional/interface_usdt_net.py +++ b/test/functional/interface_usdt_net.py @@ -116,13 +116,10 @@ class NetTracepointTest(BitcoinTestFramework): fn_name="trace_outbound_message") bpf = BPF(text=net_tracepoints_program, usdt_contexts=[ctx], debug=0) - # The handle_* function is a ctypes callback function called from C. When - # we assert in the handle_* function, the AssertError doesn't propagate - # back to Python. The exception is ignored. We manually count and assert - # that the handle_* functions succeeded. EXPECTED_INOUTBOUND_VERSION_MSG = 1 checked_inbound_version_msg = 0 checked_outbound_version_msg = 0 + events = [] def check_p2p_message(event, inbound): nonlocal checked_inbound_version_msg, checked_outbound_version_msg @@ -142,12 +139,13 @@ class NetTracepointTest(BitcoinTestFramework): checked_outbound_version_msg += 1 def handle_inbound(_, data, __): + nonlocal events event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents - check_p2p_message(event, True) + events.append((event, True)) def handle_outbound(_, data, __): event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents - check_p2p_message(event, False) + events.append((event, False)) bpf["inbound_messages"].open_perf_buffer(handle_inbound) bpf["outbound_messages"].open_perf_buffer(handle_outbound) @@ -158,12 +156,15 @@ class NetTracepointTest(BitcoinTestFramework): bpf.perf_buffer_poll(timeout=200) self.log.info( - "check that we got both an inbound and outbound version message") + "check receipt and content of in- and outbound version messages") + for event, inbound in events: + check_p2p_message(event, inbound) assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, checked_inbound_version_msg) assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, checked_outbound_version_msg) + bpf.cleanup() diff --git a/test/functional/interface_usdt_utxocache.py b/test/functional/interface_usdt_utxocache.py index 6774db7c5f..5f2ba49026 100755 --- a/test/functional/interface_usdt_utxocache.py +++ b/test/functional/interface_usdt_utxocache.py @@ -188,13 +188,16 @@ class UTXOCacheTracepointTest(BitcoinTestFramework): nonlocal handle_uncache_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_uncache(): {event}") - assert_equal(block_1_coinbase_txid, bytes(event.txid[::-1]).hex()) - assert_equal(0, event.index) # prevout index - assert_equal(EARLY_BLOCK_HEIGHT, event.height) - assert_equal(50 * COIN, event.value) - assert_equal(True, event.is_coinbase) - - handle_uncache_succeeds += 1 + try: + assert_equal(block_1_coinbase_txid, bytes(event.txid[::-1]).hex()) + assert_equal(0, event.index) # prevout index + assert_equal(EARLY_BLOCK_HEIGHT, event.height) + assert_equal(50 * COIN, event.value) + assert_equal(True, event.is_coinbase) + except AssertionError: + self.log.exception("Assertion failed") + else: + handle_uncache_succeeds += 1 bpf["utxocache_uncache"].open_perf_buffer(handle_utxocache_uncache) @@ -260,24 +263,32 @@ class UTXOCacheTracepointTest(BitcoinTestFramework): event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_add(): {event}") add = expected_utxocache_adds.pop(0) - assert_equal(add["txid"], bytes(event.txid[::-1]).hex()) - assert_equal(add["index"], event.index) - assert_equal(add["height"], event.height) - assert_equal(add["value"], event.value) - assert_equal(add["is_coinbase"], event.is_coinbase) - handle_add_succeeds += 1 + try: + assert_equal(add["txid"], bytes(event.txid[::-1]).hex()) + assert_equal(add["index"], event.index) + assert_equal(add["height"], event.height) + assert_equal(add["value"], event.value) + assert_equal(add["is_coinbase"], event.is_coinbase) + except AssertionError: + self.log.exception("Assertion failed") + else: + handle_add_succeeds += 1 def handle_utxocache_spent(_, data, __): nonlocal handle_spent_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_spent(): {event}") spent = expected_utxocache_spents.pop(0) - assert_equal(spent["txid"], bytes(event.txid[::-1]).hex()) - assert_equal(spent["index"], event.index) - assert_equal(spent["height"], event.height) - assert_equal(spent["value"], event.value) - assert_equal(spent["is_coinbase"], event.is_coinbase) - handle_spent_succeeds += 1 + try: + assert_equal(spent["txid"], bytes(event.txid[::-1]).hex()) + assert_equal(spent["index"], event.index) + assert_equal(spent["height"], event.height) + assert_equal(spent["value"], event.value) + assert_equal(spent["is_coinbase"], event.is_coinbase) + except AssertionError: + self.log.exception("Assertion failed") + else: + handle_spent_succeeds += 1 bpf["utxocache_add"].open_perf_buffer(handle_utxocache_add) bpf["utxocache_spent"].open_perf_buffer(handle_utxocache_spent) diff --git a/test/functional/interface_usdt_validation.py b/test/functional/interface_usdt_validation.py index 4323aef771..f9d9b525cd 100755 --- a/test/functional/interface_usdt_validation.py +++ b/test/functional/interface_usdt_validation.py @@ -85,13 +85,10 @@ class ValidationTracepointTest(BitcoinTestFramework): self.sigops, self.duration) - # The handle_* function is a ctypes callback function called from C. When - # we assert in the handle_* function, the AssertError doesn't propagate - # back to Python. The exception is ignored. We manually count and assert - # that the handle_* functions succeeded. BLOCKS_EXPECTED = 2 blocks_checked = 0 expected_blocks = dict() + events = [] self.log.info("hook into the validation:block_connected tracepoint") ctx = USDT(pid=self.nodes[0].process.pid) @@ -101,19 +98,10 @@ class ValidationTracepointTest(BitcoinTestFramework): usdt_contexts=[ctx], debug=0) def handle_blockconnected(_, data, __): - nonlocal expected_blocks, blocks_checked + nonlocal events, blocks_checked event = ctypes.cast(data, ctypes.POINTER(Block)).contents self.log.info(f"handle_blockconnected(): {event}") - block_hash = bytes(event.hash[::-1]).hex() - block = expected_blocks[block_hash] - assert_equal(block["hash"], block_hash) - assert_equal(block["height"], event.height) - assert_equal(len(block["tx"]), event.transactions) - assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs) - assert_equal(0, event.sigops) # no sigops in coinbase tx - # only plausibility checks - assert event.duration > 0 - del expected_blocks[block_hash] + events.append(event) blocks_checked += 1 bpf["block_connected"].open_perf_buffer( @@ -126,12 +114,24 @@ class ValidationTracepointTest(BitcoinTestFramework): expected_blocks[block_hash] = self.nodes[0].getblock(block_hash, 2) bpf.perf_buffer_poll(timeout=200) - bpf.cleanup() - self.log.info(f"check that we traced {BLOCKS_EXPECTED} blocks") + self.log.info(f"check that we correctly traced {BLOCKS_EXPECTED} blocks") + for event in events: + block_hash = bytes(event.hash[::-1]).hex() + block = expected_blocks[block_hash] + assert_equal(block["hash"], block_hash) + assert_equal(block["height"], event.height) + assert_equal(len(block["tx"]), event.transactions) + assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs) + assert_equal(0, event.sigops) # no sigops in coinbase tx + # only plausibility checks + assert event.duration > 0 + del expected_blocks[block_hash] assert_equal(BLOCKS_EXPECTED, blocks_checked) assert_equal(0, len(expected_blocks)) + bpf.cleanup() + if __name__ == '__main__': ValidationTracepointTest().main() diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 737a8d0a2e..8f3aec96a7 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -9,7 +9,6 @@ from decimal import Decimal import math from test_framework.test_framework import BitcoinTestFramework -from test_framework.key import ECKey from test_framework.messages import ( MAX_BIP125_RBF_SEQUENCE, COIN, @@ -44,6 +43,7 @@ from test_framework.util import ( assert_raises_rpc_error, ) from test_framework.wallet import MiniWallet +from test_framework.wallet_util import generate_keypair class MempoolAcceptanceTest(BitcoinTestFramework): @@ -283,9 +283,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): rawtxs=[tx.serialize().hex()], ) tx = tx_from_hex(raw_tx_reference) - key = ECKey() - key.generate() - pubkey = key.get_pubkey().get_bytes() + _, pubkey = generate_keypair() tx.vout[0].scriptPubKey = keys_to_multisig_script([pubkey] * 3, k=2) # Some bare multisig script (2-of-3) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'bare-multisig'}], diff --git a/test/functional/mempool_dust.py b/test/functional/mempool_dust.py index 41a26e82da..f4e385a112 100755 --- a/test/functional/mempool_dust.py +++ b/test/functional/mempool_dust.py @@ -5,7 +5,6 @@ """Test dust limit mempool policy (`-dustrelayfee` parameter)""" from decimal import Decimal -from test_framework.key import ECKey from test_framework.messages import ( COIN, CTxOut, @@ -32,6 +31,7 @@ from test_framework.util import ( get_fee, ) from test_framework.wallet import MiniWallet +from test_framework.wallet_util import generate_keypair DUST_RELAY_TX_FEE = 3000 # default setting [sat/kvB] @@ -74,11 +74,8 @@ class DustRelayFeeTest(BitcoinTestFramework): self.wallet = MiniWallet(self.nodes[0]) # prepare output scripts of each standard type - key = ECKey() - key.generate(compressed=False) - uncompressed_pubkey = key.get_pubkey().get_bytes() - key.generate(compressed=True) - pubkey = key.get_pubkey().get_bytes() + _, uncompressed_pubkey = generate_keypair(compressed=False) + _, pubkey = generate_keypair(compressed=True) output_scripts = ( (key_to_p2pk_script(uncompressed_pubkey), "P2PK (uncompressed)"), diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index b0900e49b8..bfae190c66 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -14,7 +14,6 @@ from test_framework.blocktools import ( create_block, create_coinbase, ) -from test_framework.key import ECKey from test_framework.messages import ( MAX_BIP125_RBF_SEQUENCE, CBlockHeader, @@ -89,6 +88,7 @@ from test_framework.util import ( assert_raises_rpc_error, ) from test_framework.wallet import MiniWallet +from test_framework.wallet_util import generate_keypair MAX_SIGOP_COST = 80000 @@ -1448,9 +1448,7 @@ class SegWitTest(BitcoinTestFramework): # Segwit transactions using uncompressed pubkeys are not accepted # under default policy, but should still pass consensus. - key = ECKey() - key.generate(False) - pubkey = key.get_pubkey().get_bytes() + key, pubkey = generate_keypair(compressed=False) assert_equal(len(pubkey), 65) # This should be an uncompressed pubkey utxo = self.utxo.pop(0) @@ -1544,11 +1542,7 @@ class SegWitTest(BitcoinTestFramework): @subtest def test_signature_version_1(self): - - key = ECKey() - key.generate() - pubkey = key.get_pubkey().get_bytes() - + key, pubkey = generate_keypair() witness_script = key_to_p2pk_script(pubkey) script_pubkey = script_to_p2wsh_script(witness_script) diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index 0f79ce8588..65d7b4c422 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -12,13 +12,13 @@ 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, ECKey +from test_framework.key import ECPubKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_raises_rpc_error, assert_equal, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair from test_framework.wallet import ( MiniWallet, getnewdestination, @@ -38,10 +38,9 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): self.priv = [] node0, node1, node2 = self.nodes for _ in range(self.nkeys): - k = ECKey() - k.generate() - self.pub.append(k.get_pubkey().get_bytes().hex()) - self.priv.append(bytes_to_wif(k.get_bytes(), k.is_compressed)) + 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: diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index e2fb4673ea..c4ed4da0f2 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -8,7 +8,7 @@ from decimal import Decimal from itertools import product from test_framework.descriptors import descsum_create -from test_framework.key import ECKey, H_POINT +from test_framework.key import H_POINT from test_framework.messages import ( COutPoint, CTransaction, @@ -43,8 +43,8 @@ from test_framework.util import ( random_bytes, ) from test_framework.wallet_util import ( - bytes_to_wif, - get_generate_key + generate_keypair, + get_generate_key, ) import json @@ -710,9 +710,7 @@ class PSBTTest(BitcoinTestFramework): self.log.info("Test that we can fund psbts with external inputs specified") - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) + privkey, _ = generate_keypair(wif=True) self.nodes[1].createwallet("extfund") wallet = self.nodes[1].get_wallet_rpc("extfund") @@ -825,11 +823,9 @@ class PSBTTest(BitcoinTestFramework): self.nodes[1].createwallet(wallet_name="scriptwatchonly", disable_private_keys=True) watchonly = self.nodes[1].get_wallet_rpc("scriptwatchonly") - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) + privkey, pubkey = generate_keypair(wif=True) - desc = descsum_create("wsh(pkh({}))".format(eckey.get_pubkey().get_bytes().hex())) + desc = descsum_create("wsh(pkh({}))".format(pubkey.hex())) if self.options.descriptors: res = watchonly.importdescriptors([{"desc": desc, "timestamp": "now"}]) else: @@ -846,11 +842,9 @@ class PSBTTest(BitcoinTestFramework): # Same test but for taproot if self.options.descriptors: - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) + privkey, pubkey = generate_keypair(wif=True) - desc = descsum_create("tr({},pk({}))".format(H_POINT, eckey.get_pubkey().get_bytes().hex())) + desc = descsum_create("tr({},pk({}))".format(H_POINT, pubkey.hex())) res = watchonly.importdescriptors([{"desc": desc, "timestamp": "now"}]) assert res[0]["success"] addr = self.nodes[0].deriveaddresses(desc)[0] diff --git a/test/functional/rpc_signer.py b/test/functional/rpc_signer.py index 4300190387..5ba0d35835 100755 --- a/test/functional/rpc_signer.py +++ b/test/functional/rpc_signer.py @@ -27,9 +27,6 @@ class RPCSignerTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 - # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which - # invokes execve). - self.disable_syscall_sandbox = True self.extra_args = [ [], diff --git a/test/functional/rpc_signrawtransactionwithkey.py b/test/functional/rpc_signrawtransactionwithkey.py index 580f63063d..ac7a86704f 100755 --- a/test/functional/rpc_signrawtransactionwithkey.py +++ b/test/functional/rpc_signrawtransactionwithkey.py @@ -11,7 +11,6 @@ from test_framework.address import ( address_to_scriptpubkey, script_to_p2sh, ) -from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -23,16 +22,16 @@ from test_framework.script_util import ( script_to_p2sh_p2wsh_script, script_to_p2wsh_script, ) +from test_framework.wallet import ( + getnewdestination, +) from test_framework.wallet_util import ( - bytes_to_wif, + generate_keypair, ) from decimal import ( Decimal, ) -from test_framework.wallet import ( - getnewdestination, -) class SignRawTransactionWithKeyTest(BitcoinTestFramework): @@ -80,11 +79,8 @@ class SignRawTransactionWithKeyTest(BitcoinTestFramework): def witness_script_test(self): self.log.info("Test signing transaction to P2SH-P2WSH addresses without wallet") # Create a new P2SH-P2WSH 1-of-1 multisig address: - eckey = ECKey() - eckey.generate() - embedded_privkey = bytes_to_wif(eckey.get_bytes()) - embedded_pubkey = eckey.get_pubkey().get_bytes().hex() - p2sh_p2wsh_address = self.nodes[1].createmultisig(1, [embedded_pubkey], "p2sh-segwit") + embedded_privkey, embedded_pubkey = generate_keypair(wif=True) + p2sh_p2wsh_address = self.nodes[1].createmultisig(1, [embedded_pubkey.hex()], "p2sh-segwit") # send transaction to P2SH-P2WSH 1-of-1 multisig address self.block_hash = self.generate(self.nodes[0], COINBASE_MATURITY + 1) self.blk_idx = 0 @@ -109,10 +105,7 @@ class SignRawTransactionWithKeyTest(BitcoinTestFramework): def verify_txn_with_witness_script(self, tx_type): self.log.info("Test with a {} script as the witnessScript".format(tx_type)) - eckey = ECKey() - eckey.generate() - embedded_privkey = bytes_to_wif(eckey.get_bytes()) - embedded_pubkey = eckey.get_pubkey().get_bytes().hex() + embedded_privkey, embedded_pubkey = generate_keypair(wif=True) witness_script = { 'P2PKH': key_to_p2pkh_script(embedded_pubkey).hex(), 'P2PK': key_to_p2pk_script(embedded_pubkey).hex() diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index efb4934ff0..c250fc6fe8 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -1,7 +1,7 @@ # Copyright (c) 2019-2020 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test-only secp256k1 elliptic curve implementation +"""Test-only secp256k1 elliptic curve protocols implementation WARNING: This code is slow, uses bad randomness, does not properly protect keys, and is trivially vulnerable to side channel attacks. Do not use for @@ -13,9 +13,13 @@ import os import random import unittest +from test_framework import secp256k1 + # Point with no known discrete log. H_POINT = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" +# Order of the secp256k1 curve +ORDER = secp256k1.GE.ORDER def TaggedHash(tag, data): ss = hashlib.sha256(tag.encode('utf-8')).digest() @@ -23,233 +27,18 @@ def TaggedHash(tag, data): ss += data return hashlib.sha256(ss).digest() -def jacobi_symbol(n, k): - """Compute the Jacobi symbol of n modulo k - - See https://en.wikipedia.org/wiki/Jacobi_symbol - - For our application k is always prime, so this is the same as the Legendre symbol.""" - assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k" - n %= k - t = 0 - while n != 0: - while n & 1 == 0: - n >>= 1 - r = k & 7 - t ^= (r == 3 or r == 5) - n, k = k, n - t ^= (n & k & 3 == 3) - n = n % k - if k == 1: - return -1 if t else 1 - return 0 - -def modsqrt(a, p): - """Compute the square root of a modulo p when p % 4 = 3. - - The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm - - Limiting this function to only work for p % 4 = 3 means we don't need to - iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd - is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4) - - secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4. - """ - if p % 4 != 3: - raise NotImplementedError("modsqrt only implemented for p % 4 = 3") - sqrt = pow(a, (p + 1)//4, p) - if pow(sqrt, 2, p) == a % p: - return sqrt - return None - -class EllipticCurve: - def __init__(self, p, a, b): - """Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p).""" - self.p = p - self.a = a % p - self.b = b % p - - def affine(self, p1): - """Convert a Jacobian point tuple p1 to affine form, or None if at infinity. - - An affine point is represented as the Jacobian (x, y, 1)""" - x1, y1, z1 = p1 - if z1 == 0: - return None - inv = pow(z1, -1, self.p) - inv_2 = (inv**2) % self.p - inv_3 = (inv_2 * inv) % self.p - return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1) - - def has_even_y(self, p1): - """Whether the point p1 has an even Y coordinate when expressed in affine coordinates.""" - return not (p1[2] == 0 or self.affine(p1)[1] & 1) - - def negate(self, p1): - """Negate a Jacobian point tuple p1.""" - x1, y1, z1 = p1 - return (x1, (self.p - y1) % self.p, z1) - - def on_curve(self, p1): - """Determine whether a Jacobian tuple p is on the curve (and not infinity)""" - x1, y1, z1 = p1 - z2 = pow(z1, 2, self.p) - z4 = pow(z2, 2, self.p) - return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0 - - def is_x_coord(self, x): - """Test whether x is a valid X coordinate on the curve.""" - x_3 = pow(x, 3, self.p) - return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1 - - def lift_x(self, x): - """Given an X coordinate on the curve, return a corresponding affine point for which the Y coordinate is even.""" - x_3 = pow(x, 3, self.p) - v = x_3 + self.a * x + self.b - y = modsqrt(v, self.p) - if y is None: - return None - return (x, self.p - y if y & 1 else y, 1) - - def double(self, p1): - """Double a Jacobian tuple p1 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling""" - x1, y1, z1 = p1 - if z1 == 0: - return (0, 1, 0) - y1_2 = (y1**2) % self.p - y1_4 = (y1_2**2) % self.p - x1_2 = (x1**2) % self.p - s = (4*x1*y1_2) % self.p - m = 3*x1_2 - if self.a: - m += self.a * pow(z1, 4, self.p) - m = m % self.p - x2 = (m**2 - 2*s) % self.p - y2 = (m*(s - x2) - 8*y1_4) % self.p - z2 = (2*y1*z1) % self.p - return (x2, y2, z2) - - def add_mixed(self, p1, p2): - """Add a Jacobian tuple p1 and an affine tuple p2 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)""" - x1, y1, z1 = p1 - x2, y2, z2 = p2 - assert z2 == 1 - # Adding to the point at infinity is a no-op - if z1 == 0: - return p2 - z1_2 = (z1**2) % self.p - z1_3 = (z1_2 * z1) % self.p - u2 = (x2 * z1_2) % self.p - s2 = (y2 * z1_3) % self.p - if x1 == u2: - if (y1 != s2): - # p1 and p2 are inverses. Return the point at infinity. - return (0, 1, 0) - # p1 == p2. The formulas below fail when the two points are equal. - return self.double(p1) - h = u2 - x1 - r = s2 - y1 - h_2 = (h**2) % self.p - h_3 = (h_2 * h) % self.p - u1_h_2 = (x1 * h_2) % self.p - x3 = (r**2 - h_3 - 2*u1_h_2) % self.p - y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p - z3 = (h*z1) % self.p - return (x3, y3, z3) - - def add(self, p1, p2): - """Add two Jacobian tuples p1 and p2 - - See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition""" - x1, y1, z1 = p1 - x2, y2, z2 = p2 - # Adding the point at infinity is a no-op - if z1 == 0: - return p2 - if z2 == 0: - return p1 - # Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1 - if z1 == 1: - return self.add_mixed(p2, p1) - if z2 == 1: - return self.add_mixed(p1, p2) - z1_2 = (z1**2) % self.p - z1_3 = (z1_2 * z1) % self.p - z2_2 = (z2**2) % self.p - z2_3 = (z2_2 * z2) % self.p - u1 = (x1 * z2_2) % self.p - u2 = (x2 * z1_2) % self.p - s1 = (y1 * z2_3) % self.p - s2 = (y2 * z1_3) % self.p - if u1 == u2: - if (s1 != s2): - # p1 and p2 are inverses. Return the point at infinity. - return (0, 1, 0) - # p1 == p2. The formulas below fail when the two points are equal. - return self.double(p1) - h = u2 - u1 - r = s2 - s1 - h_2 = (h**2) % self.p - h_3 = (h_2 * h) % self.p - u1_h_2 = (u1 * h_2) % self.p - x3 = (r**2 - h_3 - 2*u1_h_2) % self.p - y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p - z3 = (h*z1*z2) % self.p - return (x3, y3, z3) - - def mul(self, ps): - """Compute a (multi) point multiplication - - ps is a list of (Jacobian tuple, scalar) pairs. - """ - r = (0, 1, 0) - for i in range(255, -1, -1): - r = self.double(r) - for (p, n) in ps: - if ((n >> i) & 1): - r = self.add(r, p) - return r - -SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977 -SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7) -SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1) -SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2 - -class ECPubKey(): + +class ECPubKey: """A secp256k1 public key""" def __init__(self): """Construct an uninitialized public key""" - self.valid = False + self.p = None def set(self, data): """Construct a public key from a serialization in compressed or uncompressed format""" - if (len(data) == 65 and data[0] == 0x04): - p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1) - self.valid = SECP256K1.on_curve(p) - if self.valid: - self.p = p - self.compressed = False - elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)): - x = int.from_bytes(data[1:33], 'big') - if SECP256K1.is_x_coord(x): - p = SECP256K1.lift_x(x) - # Make the Y coordinate odd if required (lift_x always produces - # a point with an even Y coordinate). - if data[0] & 1: - p = SECP256K1.negate(p) - self.p = p - self.valid = True - self.compressed = True - else: - self.valid = False - else: - self.valid = False + self.p = secp256k1.GE.from_bytes(data) + self.compressed = len(data) == 33 @property def is_compressed(self): @@ -257,24 +46,21 @@ class ECPubKey(): @property def is_valid(self): - return self.valid + return self.p is not None def get_bytes(self): - assert self.valid - p = SECP256K1.affine(self.p) - if p is None: - return None + assert self.is_valid if self.compressed: - return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big') + return self.p.to_bytes_compressed() else: - return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big') + return self.p.to_bytes_uncompressed() def verify_ecdsa(self, sig, msg, low_s=True): """Verify a strictly DER-encoded ECDSA signature against this pubkey. See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the ECDSA verifier algorithm""" - assert self.valid + assert self.is_valid # Extract r and s from the DER formatted signature. Return false for # any DER encoding errors. @@ -310,24 +96,22 @@ class ECPubKey(): s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big') # Verify that r and s are within the group order - if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER: + if r < 1 or s < 1 or r >= ORDER or s >= ORDER: return False - if low_s and s >= SECP256K1_ORDER_HALF: + if low_s and s >= secp256k1.GE.ORDER_HALF: return False z = int.from_bytes(msg, 'big') # Run verifier algorithm on r, s - w = pow(s, -1, SECP256K1_ORDER) - u1 = z*w % SECP256K1_ORDER - u2 = r*w % SECP256K1_ORDER - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)])) - if R is None or (R[0] % SECP256K1_ORDER) != r: + w = pow(s, -1, ORDER) + R = secp256k1.GE.mul((z * w, secp256k1.G), (r * w, self.p)) + if R.infinity or (int(R.x) % ORDER) != r: return False return True def generate_privkey(): """Generate a valid random 32-byte private key.""" - return random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big') + return random.randrange(1, ORDER).to_bytes(32, 'big') def rfc6979_nonce(key): """Compute signing nonce using RFC6979.""" @@ -339,7 +123,7 @@ def rfc6979_nonce(key): v = hmac.new(k, v, 'sha256').digest() return hmac.new(k, v, 'sha256').digest() -class ECKey(): +class ECKey: """A secp256k1 private key""" def __init__(self): @@ -349,7 +133,7 @@ class ECKey(): """Construct a private key object with given 32-byte secret and compressed flag.""" assert len(secret) == 32 secret = int.from_bytes(secret, 'big') - self.valid = (secret > 0 and secret < SECP256K1_ORDER) + self.valid = (secret > 0 and secret < ORDER) if self.valid: self.secret = secret self.compressed = compressed @@ -375,9 +159,7 @@ class ECKey(): """Compute an ECPubKey object for this secret key.""" assert self.valid ret = ECPubKey() - p = SECP256K1.mul([(SECP256K1_G, self.secret)]) - ret.p = p - ret.valid = True + ret.p = self.secret * secp256k1.G ret.compressed = self.compressed return ret @@ -392,12 +174,12 @@ class ECKey(): if rfc6979: k = int.from_bytes(rfc6979_nonce(self.secret.to_bytes(32, 'big') + msg), 'big') else: - k = random.randrange(1, SECP256K1_ORDER) - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)])) - r = R[0] % SECP256K1_ORDER - s = (pow(k, -1, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER - if low_s and s > SECP256K1_ORDER_HALF: - s = SECP256K1_ORDER - s + k = random.randrange(1, ORDER) + R = k * secp256k1.G + r = int(R.x) % ORDER + s = (pow(k, -1, ORDER) * (z + self.secret * r)) % ORDER + if low_s and s > secp256k1.GE.ORDER_HALF: + s = ORDER - s # Represent in DER format. The byte representations of r and s have # length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33 # bytes). @@ -413,10 +195,10 @@ def compute_xonly_pubkey(key): assert len(key) == 32 x = int.from_bytes(key, 'big') - if x == 0 or x >= SECP256K1_ORDER: + if x == 0 or x >= ORDER: return (None, None) - P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, x)])) - return (P[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(P)) + P = x * secp256k1.G + return (P.to_bytes_xonly(), not P.y.is_even()) def tweak_add_privkey(key, tweak): """Tweak a private key (after negating it if needed).""" @@ -425,14 +207,14 @@ def tweak_add_privkey(key, tweak): assert len(tweak) == 32 x = int.from_bytes(key, 'big') - if x == 0 or x >= SECP256K1_ORDER: + if x == 0 or x >= ORDER: return None - if not SECP256K1.has_even_y(SECP256K1.mul([(SECP256K1_G, x)])): - x = SECP256K1_ORDER - x + if not (x * secp256k1.G).y.is_even(): + x = ORDER - x t = int.from_bytes(tweak, 'big') - if t >= SECP256K1_ORDER: + if t >= ORDER: return None - x = (x + t) % SECP256K1_ORDER + x = (x + t) % ORDER if x == 0: return None return x.to_bytes(32, 'big') @@ -443,19 +225,16 @@ def tweak_add_pubkey(key, tweak): assert len(key) == 32 assert len(tweak) == 32 - x_coord = int.from_bytes(key, 'big') - if x_coord >= SECP256K1_FIELD_SIZE: - return None - P = SECP256K1.lift_x(x_coord) + P = secp256k1.GE.from_bytes_xonly(key) if P is None: return None t = int.from_bytes(tweak, 'big') - if t >= SECP256K1_ORDER: + if t >= ORDER: return None - Q = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, t), (P, 1)])) - if Q is None: + Q = t * secp256k1.G + P + if Q.infinity: return None - return (Q[0].to_bytes(32, 'big'), not SECP256K1.has_even_y(Q)) + return (Q.to_bytes_xonly(), not Q.y.is_even()) def verify_schnorr(key, sig, msg): """Verify a Schnorr signature (see BIP 340). @@ -468,23 +247,20 @@ def verify_schnorr(key, sig, msg): assert len(msg) == 32 assert len(sig) == 64 - x_coord = int.from_bytes(key, 'big') - if x_coord == 0 or x_coord >= SECP256K1_FIELD_SIZE: - return False - P = SECP256K1.lift_x(x_coord) + P = secp256k1.GE.from_bytes_xonly(key) if P is None: return False r = int.from_bytes(sig[0:32], 'big') - if r >= SECP256K1_FIELD_SIZE: + if r >= secp256k1.FE.SIZE: return False s = int.from_bytes(sig[32:64], 'big') - if s >= SECP256K1_ORDER: + if s >= ORDER: return False - e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), 'big') % SECP256K1_ORDER - R = SECP256K1.mul([(SECP256K1_G, s), (P, SECP256K1_ORDER - e)]) - if not SECP256K1.has_even_y(R): + e = int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), 'big') % ORDER + R = secp256k1.GE.mul((s, secp256k1.G), (-e, P)) + if R.infinity or not R.y.is_even(): return False - if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]: + if r != R.x: return False return True @@ -499,23 +275,24 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): assert len(aux) == 32 sec = int.from_bytes(key, 'big') - if sec == 0 or sec >= SECP256K1_ORDER: + if sec == 0 or sec >= ORDER: return None - P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, sec)])) - if SECP256K1.has_even_y(P) == flip_p: - sec = SECP256K1_ORDER - sec + P = sec * secp256k1.G + if P.y.is_even() == flip_p: + sec = ORDER - sec t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big') - kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER + kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P.to_bytes_xonly() + msg), 'big') % ORDER assert kp != 0 - R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) - k = kp if SECP256K1.has_even_y(R) != flip_r else SECP256K1_ORDER - kp - e = int.from_bytes(TaggedHash("BIP0340/challenge", R[0].to_bytes(32, 'big') + P[0].to_bytes(32, 'big') + msg), 'big') % SECP256K1_ORDER - return R[0].to_bytes(32, 'big') + ((k + e * sec) % SECP256K1_ORDER).to_bytes(32, 'big') + R = kp * secp256k1.G + k = kp if R.y.is_even() != flip_r else ORDER - kp + e = int.from_bytes(TaggedHash("BIP0340/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg), 'big') % ORDER + return R.to_bytes_xonly() + ((k + e * sec) % ORDER).to_bytes(32, 'big') + class TestFrameworkKey(unittest.TestCase): def test_schnorr(self): """Test the Python Schnorr implementation.""" - byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, SECP256K1_ORDER - 1, SECP256K1_ORDER, 2**256 - 1]] + 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 in byte_arrays: # build array of key/pubkey pairs pubkey, _ = compute_xonly_pubkey(privkey) diff --git a/test/functional/test_framework/secp256k1.py b/test/functional/test_framework/secp256k1.py new file mode 100644 index 0000000000..2e9e419da5 --- /dev/null +++ b/test/functional/test_framework/secp256k1.py @@ -0,0 +1,346 @@ +# Copyright (c) 2022-2023 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-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + + +class FE: + """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977). + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE = 2**256 - 2**32 - 977 + + def __init__(self, a=0, b=1): + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, FE): + num = a._num + den = a._den + else: + num = a % FE.SIZE + den = 1 + if isinstance(b, FE): + den = (den * b._num) % FE.SIZE + num = (num * b._den) % FE.SIZE + else: + den = (den * b) % FE.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num = num + self._den = den + + def __add__(self, a): + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._den + self._den * a._num, self._den * a._den) + return FE(self._num + self._den * a, self._den) + + def __radd__(self, a): + """Compute the sum of an integer and a field element.""" + return FE(a) + self + + def __sub__(self, a): + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._den - self._den * a._num, self._den * a._den) + return FE(self._num - self._den * a, self._den) + + def __rsub__(self, a): + """Compute the difference of an integer and a field element.""" + return FE(a) - self + + def __mul__(self, a): + """Compute the product of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._num, self._den * a._den) + return FE(self._num * a, self._den) + + def __rmul__(self, a): + """Compute the product of an integer with a field element.""" + return FE(a) * self + + def __truediv__(self, a): + """Compute the ratio of two field elements (second may be int).""" + return FE(self, a) + + def __pow__(self, a): + """Raise a field element to an integer power.""" + return FE(pow(self._num, a, FE.SIZE), pow(self._den, a, FE.SIZE)) + + def __neg__(self): + """Negate a field element.""" + return FE(-self._num, self._den) + + def __int__(self): + """Convert a field element to an integer in range 0..p-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, FE.SIZE)) % FE.SIZE + self._den = 1 + return self._num + + def sqrt(self): + """Compute the square root of a field element if it exists (None otherwise). + + Due to the fact that our modulus is of the form (p % 4) == 3, the Tonelli-Shanks + algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + raising the argument to the power (p + 1) / 4. + + To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + and thus only half of the non-zero field elements are squares. An element a is + a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.""" + v = int(self) + s = pow(v, (FE.SIZE + 1) // 4, FE.SIZE) + if s**2 % FE.SIZE == v: + return FE(s) + return None + + def is_square(self): + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self): + """Determine whether this field element, represented as integer in 0..p-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a): + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, FE): + return (self._num * a._den - self._den * a._num) % FE.SIZE == 0 + return (self._num - self._den * a) % FE.SIZE == 0 + + def to_bytes(self): + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @staticmethod + def from_bytes(b): + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + if v >= FE.SIZE: + return None + return FE(v) + + def __str__(self): + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self): + """Get a string representation of this field element.""" + return f"FE(0x{int(self):x})" + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + def __init__(self, x=None, y=None): + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self.infinity = True + else: + # Initialize as point on the curve (and check that it is). + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self.infinity = False + self.x = fx + self.y = fy + + def __add__(self, a): + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def mul(*aps): + """Compute a (batch) scalar group element multiplication. + + GE.mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(a % GE.ORDER, p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a): + """Multiply an integer with a group element.""" + if self == G: + return FAST_G.mul(a) + return GE.mul((a, self)) + + def __neg__(self): + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def to_bytes_compressed(self): + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_uncompressed(self): + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self): + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x): + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + return None + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes(b): + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + if b[0] != 2 and b[0] != 3: + return None + x = FE.from_bytes(b[1:]) + if x is None: + return None + r = GE.lift_x(x) + if r is None: + return None + if b[0] == 3: + r = -r + return r + else: + if b[0] != 4: + return None + x = FE.from_bytes(b[1:33]) + y = FE.from_bytes(b[33:]) + if y**2 != x**3 + 7: + return None + return GE(x, y) + + @staticmethod + def from_bytes_xonly(b): + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes(b) + if x is None: + return None + return GE.lift_x(x) + + @staticmethod + def is_valid_x(x): + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self): + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self): + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p): + self.table = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a): + result = GE() + a = a % GE.ORDER + for bit in range(a.bit_length()): + if a & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index d4dc90a517..d3aae3fb9a 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -103,7 +103,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.supports_cli = True self.bind_to_localhost_only = True self.parse_args() - self.disable_syscall_sandbox = self.options.nosandbox or self.options.valgrind self.default_wallet_name = "default_wallet" if self.options.descriptors else "" self.wallet_data_filename = "wallet.dat" # Optional list of wallet names that can be set in set_test_params to @@ -160,8 +159,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") - parser.add_argument("--nosandbox", dest="nosandbox", default=False, action="store_true", - help="Don't use the syscall sandbox") parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", help="Don't stop bitcoinds after the test execution") parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), @@ -188,7 +185,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): parser.add_argument("--perf", dest="perf", default=False, action="store_true", help="profile running nodes with perf for the duration of the test") parser.add_argument("--valgrind", dest="valgrind", default=False, action="store_true", - help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required. Forces --nosandbox.") + help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required.") parser.add_argument("--randomseed", type=int, help="set a random seed for deterministically reproducing a previous test run") parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts") @@ -497,11 +494,6 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): extra_args = [[]] * num_nodes if versions is None: versions = [None] * num_nodes - if self.is_syscall_sandbox_compiled() and not self.disable_syscall_sandbox: - for i in range(len(extra_args)): - # The -sandbox argument is not present in the v22.0 release. - if versions[i] is None or versions[i] >= 229900: - extra_args[i] = extra_args[i] + ["-sandbox=log-and-abort"] if binary is None: binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions] if binary_cli is None: @@ -987,7 +979,3 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): def is_bdb_compiled(self): """Checks whether the wallet module was compiled with BDB support.""" return self.config["components"].getboolean("USE_BDB") - - def is_syscall_sandbox_compiled(self): - """Checks whether the syscall sandbox was compiled.""" - return self.config["components"].getboolean("ENABLE_SYSCALL_SANDBOX") diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index 1d546e12bd..271095ea21 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -20,6 +20,7 @@ from test_framework.address import ( key_to_p2wpkh, output_key_to_p2tr, ) +from test_framework.blocktools import COINBASE_MATURITY from test_framework.descriptors import descsum_create from test_framework.key import ( ECKey, @@ -53,7 +54,7 @@ from test_framework.util import ( assert_equal, assert_greater_than_or_equal, ) -from test_framework.blocktools import COINBASE_MATURITY +from test_framework.wallet_util import generate_keypair DEFAULT_FEE = Decimal("0.0001") @@ -395,9 +396,7 @@ def getnewdestination(address_type='bech32m'): 'legacy', 'p2sh-segwit', 'bech32' and 'bech32m'. Can be used when a random destination is needed, but no compiled wallet is available (e.g. as replacement to the getnewaddress/getaddressinfo RPCs).""" - key = ECKey() - key.generate() - pubkey = key.get_pubkey().get_bytes() + key, pubkey = generate_keypair() if address_type == 'legacy': scriptpubkey = key_to_p2pkh_script(pubkey) address = key_to_p2pkh(pubkey) diff --git a/test/functional/test_framework/wallet_util.py b/test/functional/test_framework/wallet_util.py index 410d85cd8c..319f120297 100755 --- a/test/functional/test_framework/wallet_util.py +++ b/test/functional/test_framework/wallet_util.py @@ -63,12 +63,9 @@ def get_generate_key(): """Generate a fresh key Returns a named tuple of privkey, pubkey and all address and scripts.""" - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) - pubkey = eckey.get_pubkey().get_bytes().hex() + privkey, pubkey = generate_keypair(wif=True) return Key(privkey=privkey, - pubkey=pubkey, + pubkey=pubkey.hex(), p2pkh_script=key_to_p2pkh_script(pubkey).hex(), p2pkh_addr=key_to_p2pkh(pubkey), p2wpkh_script=key_to_p2wpkh_script(pubkey).hex(), @@ -114,8 +111,14 @@ def bytes_to_wif(b, compressed=True): b += b'\x01' return byte_to_base58(b, 239) -def generate_wif_key(): - # Makes a WIF privkey for imports - k = ECKey() - k.generate() - return bytes_to_wif(k.get_bytes(), k.is_compressed) +def generate_keypair(compressed=True, wif=False): + """Generate a new random keypair and return the corresponding ECKey / + bytes objects. The private key can also be provided as WIF (wallet + import format) string instead, which is often useful for wallet RPC + interaction.""" + privkey = ECKey() + privkey.generate(compressed) + pubkey = privkey.get_pubkey().get_bytes() + if wif: + privkey = bytes_to_wif(privkey.get_bytes(), compressed) + return privkey, pubkey diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c834086b6f..e5df29b135 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -210,7 +210,6 @@ BASE_SCRIPTS = [ 'rpc_users.py', 'rpc_whitelist.py', 'feature_proxy.py', - 'feature_syscall_sandbox.py', 'wallet_signrawtransactionwithwallet.py --legacy-wallet', 'wallet_signrawtransactionwithwallet.py --descriptors', 'rpc_signrawtransactionwithkey.py', diff --git a/test/functional/wallet_blank.py b/test/functional/wallet_blank.py index eda3fda35b..4836eba3b2 100755 --- a/test/functional/wallet_blank.py +++ b/test/functional/wallet_blank.py @@ -10,11 +10,10 @@ from test_framework.address import ( ADDRESS_BCRT1_UNSPENDABLE, ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, ) -from test_framework.key import ECKey from test_framework.util import ( assert_equal, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair class WalletBlankTest(BitcoinTestFramework): @@ -50,10 +49,8 @@ class WalletBlankTest(BitcoinTestFramework): assert_equal(info["descriptors"], False) assert_equal(info["blank"], True) - eckey = ECKey() - eckey.generate(compressed=comp) - - wallet.importpubkey(eckey.get_pubkey().get_bytes().hex()) + _, pubkey = generate_keypair(compressed=comp) + wallet.importpubkey(pubkey.hex()) assert_equal(wallet.getwalletinfo()["blank"], False) def test_importprivkey(self): @@ -67,10 +64,7 @@ class WalletBlankTest(BitcoinTestFramework): assert_equal(info["descriptors"], False) assert_equal(info["blank"], True) - eckey = ECKey() - eckey.generate(compressed=comp) - wif = bytes_to_wif(eckey.get_bytes(), eckey.is_compressed) - + wif, _ = generate_keypair(compressed=comp, wif=True) wallet.importprivkey(wif) assert_equal(wallet.getwalletinfo()["blank"], False) diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index a4e6f96cce..75b507c387 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -7,13 +7,13 @@ from test_framework.address import key_to_p2wpkh from test_framework.descriptors import descsum_create -from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) -from test_framework.wallet_util import bytes_to_wif, generate_wif_key +from test_framework.wallet_util import generate_keypair + EMPTY_PASSPHRASE_MSG = "Empty string given as passphrase, wallet will not be encrypted." LEGACY_WALLET_MSG = "Wallet created successfully. The legacy wallet type is being deprecated and support for creating and opening legacy wallets will be removed in the future." @@ -50,14 +50,12 @@ class CreateWalletTest(BitcoinTestFramework): w1.importpubkey(w0.getaddressinfo(address1)['pubkey']) self.log.info('Test that private keys cannot be imported') - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) + privkey, pubkey = generate_keypair(wif=True) assert_raises_rpc_error(-4, 'Cannot import private keys to a wallet with private keys disabled', w1.importprivkey, privkey) if self.options.descriptors: result = w1.importdescriptors([{'desc': descsum_create('wpkh(' + privkey + ')'), 'timestamp': 'now'}]) else: - result = w1.importmulti([{'scriptPubKey': {'address': key_to_p2wpkh(eckey.get_pubkey().get_bytes())}, 'timestamp': 'now', 'keys': [privkey]}]) + result = w1.importmulti([{'scriptPubKey': {'address': key_to_p2wpkh(pubkey)}, 'timestamp': 'now', 'keys': [privkey]}]) assert not result[0]['success'] assert 'warnings' not in result[0] assert_equal(result[0]['error']['code'], -4) @@ -77,7 +75,7 @@ class CreateWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getrawchangeaddress) # Import private key - w3.importprivkey(generate_wif_key()) + w3.importprivkey(generate_keypair(wif=True)[0]) # Imported private keys are currently ignored by the keypool assert_equal(w3.getwalletinfo()['keypoolsize'], 0) assert_raises_rpc_error(-4, "Error: This wallet has no available keys", w3.getnewaddress) diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index c88e0b3f6e..46706d6ad2 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -10,7 +10,6 @@ from itertools import product from math import ceil from test_framework.descriptors import descsum_create -from test_framework.key import ECKey from test_framework.messages import ( COIN, ) @@ -25,7 +24,7 @@ from test_framework.util import ( count_bytes, find_vout_for_address, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair ERR_NOT_ENOUGH_PRESET_INPUTS = "The preselected coins total amount does not cover the transaction target. " \ "Please allow other inputs to be automatically selected or include more coins manually" @@ -999,11 +998,7 @@ class RawTransactionsTest(BitcoinTestFramework): def test_external_inputs(self): self.log.info("Test funding with external inputs") - - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) - + privkey, _ = generate_keypair(wif=True) self.nodes[2].createwallet("extfund") wallet = self.nodes[2].get_wallet_rpc("extfund") diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 77b407579f..5fe7c4b591 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -7,7 +7,6 @@ from decimal import Decimal from test_framework.address import key_to_p2wpkh from test_framework.blocktools import COINBASE_MATURITY -from test_framework.key import ECKey from test_framework.messages import ( CMerkleBlock, from_hex, @@ -17,7 +16,7 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair class ImportPrunedFundsTest(BitcoinTestFramework): @@ -40,10 +39,8 @@ class ImportPrunedFundsTest(BitcoinTestFramework): # pubkey address2 = self.nodes[0].getnewaddress() # privkey - eckey = ECKey() - eckey.generate() - address3_privkey = bytes_to_wif(eckey.get_bytes()) - address3 = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + address3_privkey, address3_pubkey = generate_keypair(wif=True) + address3 = key_to_p2wpkh(address3_pubkey) self.nodes[0].importprivkey(address3_privkey) # Check only one address diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index bfca344fd1..a19a3ac2cb 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -7,7 +7,6 @@ from test_framework.address import key_to_p2wpkh from test_framework.blocktools import COINBASE_MATURITY from test_framework.descriptors import descsum_create -from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import MAX_BIP125_RBF_SEQUENCE from test_framework.util import ( @@ -15,7 +14,7 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair from decimal import Decimal @@ -202,10 +201,8 @@ class ListSinceBlockTest(BitcoinTestFramework): self.sync_all() # share utxo between nodes[1] and nodes[2] - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) - address = key_to_p2wpkh(eckey.get_pubkey().get_bytes()) + privkey, pubkey = generate_keypair(wif=True) + address = key_to_p2wpkh(pubkey) self.nodes[2].sendtoaddress(address, 10) self.generate(self.nodes[2], 6) self.nodes[2].importprivkey(privkey) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index ac3ec06eec..d7bb6ab1e7 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -9,7 +9,6 @@ from itertools import product from test_framework.authproxy import JSONRPCException from test_framework.descriptors import descsum_create -from test_framework.key import ECKey from test_framework.messages import ( ser_compact_size, WITNESS_SCALE_FACTOR, @@ -22,7 +21,8 @@ from test_framework.util import ( assert_raises_rpc_error, count_bytes, ) -from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet_util import generate_keypair + class WalletSendTest(BitcoinTestFramework): def add_options(self, parser): @@ -500,9 +500,7 @@ class WalletSendTest(BitcoinTestFramework): assert res["complete"] self.log.info("External outputs") - eckey = ECKey() - eckey.generate() - privkey = bytes_to_wif(eckey.get_bytes()) + privkey, _ = generate_keypair(wif=True) self.nodes[1].createwallet("extsend") ext_wallet = self.nodes[1].get_wallet_rpc("extsend") diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index c414147c65..3e7c613e55 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -45,9 +45,6 @@ class WalletSignerTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 - # The experimental syscall sandbox feature (-sandbox) is not compatible with -signer (which - # invokes execve). - self.disable_syscall_sandbox = True self.extra_args = [ [], diff --git a/test/fuzz/test_runner.py b/test/fuzz/test_runner.py index af21e7b956..d953f48584 100755 --- a/test/fuzz/test_runner.py +++ b/test/fuzz/test_runner.py @@ -6,6 +6,7 @@ """ from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path import argparse import configparser import logging @@ -42,6 +43,11 @@ def main(): help='If true, run fuzzing binaries under the valgrind memory error detector', ) parser.add_argument( + "--empty_min_time", + type=int, + help="If set, run at least this long, if the existing fuzz inputs directory is empty.", + ) + parser.add_argument( '-x', '--exclude', help="A comma-separated list of targets to exclude", @@ -76,6 +82,7 @@ def main(): ) args = parser.parse_args() + args.corpus_dir = Path(args.corpus_dir) # Set up logging logging.basicConfig( @@ -180,6 +187,7 @@ def main(): src_dir=config['environment']['SRCDIR'], build_dir=config["environment"]["BUILDDIR"], use_valgrind=args.valgrind, + empty_min_time=args.empty_min_time, ) @@ -251,16 +259,22 @@ def merge_inputs(*, fuzz_pool, corpus, test_list, src_dir, build_dir, merge_dir) future.result() -def run_once(*, fuzz_pool, corpus, test_list, src_dir, build_dir, use_valgrind): +def run_once(*, fuzz_pool, corpus, test_list, src_dir, build_dir, use_valgrind, empty_min_time): jobs = [] for t in test_list: - corpus_path = os.path.join(corpus, t) + corpus_path = corpus / t os.makedirs(corpus_path, exist_ok=True) args = [ os.path.join(build_dir, 'src', 'test', 'fuzz', 'fuzz'), - '-runs=1', - corpus_path, ] + empty_dir = not any(corpus_path.iterdir()) + if empty_min_time and empty_dir: + args += [f"-max_total_time={empty_min_time}"] + else: + args += [ + "-runs=1", + corpus_path, + ] if use_valgrind: args = ['valgrind', '--quiet', '--error-exitcode=1'] + args diff --git a/test/lint/lint-assertions.py b/test/lint/lint-assertions.py index e7eecebce5..6da59b0d48 100755 --- a/test/lint/lint-assertions.py +++ b/test/lint/lint-assertions.py @@ -45,6 +45,16 @@ def main(): ":(exclude)src/rpc/server.cpp", ], "CHECK_NONFATAL(condition) or NONFATAL_UNREACHABLE should be used instead of assert for RPC code.") + # The `BOOST_ASSERT` macro requires to `#include boost/assert.hpp`, + # which is an unnecessary Boost dependency. + exit_code |= git_grep([ + "-E", + r"BOOST_ASSERT *\(.*\);", + "--", + "*.cpp", + "*.h", + ], "BOOST_ASSERT must be replaced with Assert, BOOST_REQUIRE, or BOOST_CHECK.") + sys.exit(exit_code) diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan index 8808a83e32..74703b04ec 100644 --- a/test/sanitizer_suppressions/ubsan +++ b/test/sanitizer_suppressions/ubsan @@ -23,6 +23,7 @@ implicit-integer-sign-change:crc32c/ # implicit-integer-sign-change in FuzzedDataProvider's ConsumeIntegralInRange implicit-integer-sign-change:FuzzedDataProvider.h implicit-integer-sign-change:minisketch/ +implicit-signed-integer-truncation:*/include/c++/ implicit-signed-integer-truncation:leveldb/ implicit-unsigned-integer-truncation:*/include/c++/ implicit-unsigned-integer-truncation:leveldb/ |