From a0c5fc62ad285f223a676ffdc4d49ef5dcaebcf4 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 10 Mar 2023 14:50:42 +0100 Subject: [PATCH 001/207] [DEL] Remove deprecated redeemscript methods --- bitcoinlib/scripts.py | 6 ++ bitcoinlib/transactions.py | 122 ++++++++++++++++++------------------- tests/test_transactions.py | 12 ++-- 3 files changed, 73 insertions(+), 67 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index e8e00a60..912fbbbe 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -181,6 +181,12 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True >>> s.stack [] + >>> key1 = '5JruagvxNLXTnkksyLMfgFgf3CagJ3Ekxu5oGxpTm5mPfTAPez3' + >>> key2 = '5JX3qAwDEEaapvLXRfbXRMSiyRgRSW9WjgxeyJQWwBugbudCwsk' + >>> key3 = '5JjHVMwJdjPEPQhq34WMUhzLcEd4SD7HgZktEh8WHstWcCLRceV' + >>> keylist = [Key(k) for k in [key1, key2, key3]] + >>> redeemscript = Script(keys=keylist, sigs_required=2, script_types=['multisig']) + :param commands: List of script language commands :type commands: list :param message: Signed message to verify, normally a transaction hash. Used to validate script diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 9b5f1590..e25d319d 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -451,67 +451,67 @@ def script_to_string(script, name_data=False): # pragma: no cover return ' '.join(scriptstr) -@deprecated # Replaced by Script class in version 0.6 -def _serialize_multisig_redeemscript(public_key_list, n_required=None): # pragma: no cover - # Serialize m-to-n multisig script. Needs a list of public keys - for key in public_key_list: - if not isinstance(key, (str, bytes)): - raise TransactionError("Item %s in public_key_list is not of type string or bytes") - if n_required is None: - n_required = len(public_key_list) - - script = int_to_varbyteint(op.op_1 + n_required - 1) - for key in public_key_list: - script += varstr(key) - script += int_to_varbyteint(op.op_1 + len(public_key_list) - 1) - script += b'\xae' # 'OP_CHECKMULTISIG' - - return script - - -@deprecated # Replaced by Script class in version 0.6 -def serialize_multisig_redeemscript(key_list, n_required=None, compressed=True): # pragma: no cover - """ - Create a multisig redeemscript used in a p2sh. - - Contains the number of signatures, followed by the list of public keys and the OP-code for the number of signatures required. - - :param key_list: List of public keys - :type key_list: Key, list - :param n_required: Number of required signatures - :type n_required: int - :param compressed: Use compressed public keys? - :type compressed: bool - - :return bytes: A multisig redeemscript - """ - - if not key_list: - return b'' - if not isinstance(key_list, list): - raise TransactionError("Argument public_key_list must be of type list") - if len(key_list) > 15: - raise TransactionError("Redeemscripts with more then 15 keys are non-standard and could result in " - "locked up funds") - public_key_list = [] - for k in key_list: - if isinstance(k, Key): - if compressed: - public_key_list.append(k.public_byte) - else: - public_key_list.append(k.public_uncompressed_byte) - elif len(k) == 65 and k[0:1] == b'\x04' or len(k) == 33 and k[0:1] in [b'\x02', b'\x03']: - public_key_list.append(k) - elif len(k) == 132 and k[0:2] == '04' or len(k) == 66 and k[0:2] in ['02', '03']: - public_key_list.append(bytes.fromhex(k)) - else: - kobj = Key(k) - if compressed: - public_key_list.append(kobj.public_byte) - else: - public_key_list.append(kobj.public_uncompressed_byte) - - return _serialize_multisig_redeemscript(public_key_list, n_required) +# @deprecated # Replaced by Script class in version 0.6 +# def _serialize_multisig_redeemscript(public_key_list, n_required=None): # pragma: no cover +# # Serialize m-to-n multisig script. Needs a list of public keys +# for key in public_key_list: +# if not isinstance(key, (str, bytes)): +# raise TransactionError("Item %s in public_key_list is not of type string or bytes") +# if n_required is None: +# n_required = len(public_key_list) +# +# script = int_to_varbyteint(op.op_1 + n_required - 1) +# for key in public_key_list: +# script += varstr(key) +# script += int_to_varbyteint(op.op_1 + len(public_key_list) - 1) +# script += b'\xae' # 'OP_CHECKMULTISIG' +# +# return script +# +# +# @deprecated # Replaced by Script class in version 0.6 +# def serialize_multisig_redeemscript(key_list, n_required=None, compressed=True): # pragma: no cover +# """ +# Create a multisig redeemscript used in a p2sh. +# +# Contains the number of signatures, followed by the list of public keys and the OP-code for the number of signatures required. +# +# :param key_list: List of public keys +# :type key_list: Key, list +# :param n_required: Number of required signatures +# :type n_required: int +# :param compressed: Use compressed public keys? +# :type compressed: bool +# +# :return bytes: A multisig redeemscript +# """ +# +# if not key_list: +# return b'' +# if not isinstance(key_list, list): +# raise TransactionError("Argument public_key_list must be of type list") +# if len(key_list) > 15: +# raise TransactionError("Redeemscripts with more then 15 keys are non-standard and could result in " +# "locked up funds") +# public_key_list = [] +# for k in key_list: +# if isinstance(k, Key): +# if compressed: +# public_key_list.append(k.public_byte) +# else: +# public_key_list.append(k.public_uncompressed_byte) +# elif len(k) == 65 and k[0:1] == b'\x04' or len(k) == 33 and k[0:1] in [b'\x02', b'\x03']: +# public_key_list.append(k) +# elif len(k) == 132 and k[0:2] == '04' or len(k) == 66 and k[0:2] in ['02', '03']: +# public_key_list.append(bytes.fromhex(k)) +# else: +# kobj = Key(k) +# if compressed: +# public_key_list.append(kobj.public_byte) +# else: +# public_key_list.append(kobj.public_uncompressed_byte) +# +# return _serialize_multisig_redeemscript(public_key_list, n_required) @deprecated # Replaced by Script class in version 0.6 diff --git a/tests/test_transactions.py b/tests/test_transactions.py index b4082dff..6e34b633 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1058,12 +1058,12 @@ def test_transaction_errors(self): class TestTransactionsScripts(unittest.TestCase, CustomAssertions): - def test_transaction_redeemscript_errors(self): - exp_error = "Redeemscripts with more then 15 keys are non-standard and could result in locked up funds" - keys = [] - for n in range(20): - keys.append(HDKey().public_hex) - self.assertRaisesRegexp(TransactionError, exp_error, serialize_multisig_redeemscript, keys) + # def test_transaction_redeemscript_errors(self): + # exp_error = "Redeemscripts with more then 15 keys are non-standard and could result in locked up funds" + # keys = [] + # for n in range(20): + # keys.append(HDKey().public_hex) + # self.assertRaisesRegexp(TransactionError, exp_error, serialize_multisig_redeemscript, keys) def test_transaction_script_type_string(self): # Locking script From cb3c73d16bbc2c0279393f8d2f818620e266d745 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 22 Sep 2023 14:00:59 +0200 Subject: [PATCH 002/207] [REF] Use segwit for keys and wallets by default --- bitcoinlib/config/config.py | 2 +- bitcoinlib/db.py | 5 ++-- bitcoinlib/keys.py | 12 ++++----- bitcoinlib/wallets.py | 7 ++--- tests/test_keys.py | 27 ++++++++++++-------- tests/test_mnemonic.py | 2 +- tests/test_wallets.py | 51 ++++++++++++++++++++----------------- 7 files changed, 58 insertions(+), 48 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index b0284f50..e50ae1d3 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -132,7 +132,7 @@ # Keys / Addresses SUPPORTED_ADDRESS_ENCODINGS = ['base58', 'bech32'] ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'tdash', 'tdash', 'blt'] -DEFAULT_WITNESS_TYPE = 'legacy' +DEFAULT_WITNESS_TYPE = 'segwit' BECH32M_CONST = 0x2bc830a3 # Wallets diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index dbe79529..91d697db 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -44,7 +44,7 @@ class Db: Bitcoinlib Database object used by Service() and HDWallet() class. Initialize database and open session when creating database object. - Create new database if is doesn't exist yet + Create new database if it doesn't exist yet """ def __init__(self, db_uri=None, password=None): @@ -244,7 +244,8 @@ class DbWallet(Base): __table_args__ = ( CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), name='wallet_constraint_allowed_types'), + CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr', 'mixed']), + name='wallet_constraint_allowed_types'), ) def __repr__(self): diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 79afd289..7c94ae5b 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1401,9 +1401,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger :return HDKey: """ - if not encoding and witness_type: - encoding = get_encoding_from_witness(witness_type) - self.script_type = script_type_default(witness_type, multisig) + script_type = None # if (key and not chain) or (not key and chain): # raise BKeyError("Please specify both key and chain, use import_key attribute " @@ -1431,10 +1429,9 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger if kf['format'] == 'address': raise BKeyError("Can not create HDKey object from address") if len(kf['script_types']) == 1: - self.script_type = kf['script_types'][0] + script_type = kf['script_types'][0] if len(kf['witness_types']) == 1 and not witness_type: witness_type = kf['witness_types'][0] - encoding = get_encoding_from_witness(witness_type) if len(kf['multisig']) == 1: multisig = kf['multisig'][0] network = Network(check_network_and_key(import_key, network, kf["networks"])) @@ -1463,7 +1460,10 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger key_type = 'private' if is_private else 'public' if witness_type is None: - witness_type = DEFAULT_WITNESS_TYPE + witness_type = 'legacy' if network == 'dash' else DEFAULT_WITNESS_TYPE + self.script_type = script_type if script_type else script_type_default(witness_type, multisig) + if not encoding: + encoding = get_encoding_from_witness(witness_type) Key.__init__(self, key, network, compressed, password, is_private) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index bc94a02d..7333a4fb 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -358,6 +358,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 network = k.network.name elif network != k.network.name: raise WalletError("Specified network and key network should be the same") + witness_type = k.witness_type elif isinstance(key, Address): k = key key_is_address = True @@ -368,7 +369,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 else: if network is None: network = DEFAULT_NETWORK - k = HDKey(import_key=key, network=network) + k = HDKey(import_key=key, network=network, witness_type=witness_type) if not encoding and witness_type: encoding = get_encoding_from_witness(witness_type) script_type = script_type_default(witness_type, multisig) @@ -1225,7 +1226,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if isinstance(key, str) and len(key.split(" ")) > 1: if not network: raise WalletError("Please specify network when using passphrase to create a key") - key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network) + key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network, witness_type=witness_type) else: try: if isinstance(key, WalletKey): @@ -1247,7 +1248,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if network is None: network = DEFAULT_NETWORK if witness_type is None: - witness_type = DEFAULT_WITNESS_TYPE + witness_type = DEFAULT_WITNESS_TYPE if network != 'dash' else 'legacy' if network in ['dash', 'dash_testnet', 'dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': raise WalletError("Segwit is not supported for %s wallets" % network.capitalize()) elif network in ('dogecoin', 'dogecoin_testnet') and witness_type not in ('legacy', 'p2sh-segwit'): diff --git a/tests/test_keys.py b/tests/test_keys.py index e535145e..5681c1f7 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -62,9 +62,9 @@ def test_dict_and_json_outputs(self): self.assertTrue(isinstance(k.as_dict(include_private=True), dict)) def test_path_expand(self): - self.assertListEqual(path_expand([0]), ['m', "44'", "0'", "0'", '0', '0']) - self.assertListEqual(path_expand([10, 20]), ['m', "44'", "0'", "0'", '10', '20']) - self.assertListEqual(path_expand([10, 20], witness_type='segwit'), ['m', "84'", "0'", "0'", '10', '20']) + self.assertListEqual(path_expand([0], witness_type='legacy'), ['m', "44'", "0'", "0'", '0', '0']) + self.assertListEqual(path_expand([10, 20], witness_type='legacy'), ['m', "44'", "0'", "0'", '10', '20']) + self.assertListEqual(path_expand([10, 20]), ['m', "84'", "0'", "0'", '10', '20']) self.assertListEqual(path_expand([], witness_type='p2sh-segwit'), ['m', "49'", "0'", "0'", '0', '0']) self.assertListEqual(path_expand([99], witness_type='p2sh-segwit', multisig=True), ['m', "48'", "0'", "0'", "1'", '0', '99']) @@ -271,11 +271,11 @@ def test_public_key_address_uncompressed(self): class TestHDKeysImport(unittest.TestCase): def setUp(self): - self.k = HDKey.from_seed('000102030405060708090a0b0c0d0e0f') + self.k = HDKey.from_seed('000102030405060708090a0b0c0d0e0f', witness_type='legacy') self.k2 = HDKey.from_seed('fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a878' - '4817e7b7875726f6c696663605d5a5754514e4b484542') + '4817e7b7875726f6c696663605d5a5754514e4b484542', witness_type='legacy') self.xpub = HDKey('xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqse' - 'fD265TMg7usUDFdp6W1EGMcet8') + 'fD265TMg7usUDFdp6W1EGMcet8', witness_type='legacy') def test_hdkey_import_seed_1(self): @@ -290,9 +290,14 @@ def test_hdkey_import_seed_2(self): self.assertEqual('xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJ' 'Y47LJhkJ8UB7WEGuduB', self.k2.wif_public()) + def test_hdkey_random_legacy(self): + self.k = HDKey(witness_type='legacy') + self.assertEqual('xprv', self.k.wif(is_private=True)[:4]) + self.assertEqual(111, len(self.k.wif(is_private=True))) + def test_hdkey_random(self): self.k = HDKey() - self.assertEqual('xprv', self.k.wif(is_private=True)[:4]) + self.assertEqual('zprv', self.k.wif(is_private=True)[:4]) self.assertEqual(111, len(self.k.wif(is_private=True))) def test_hdkey_import_extended_private_key(self): @@ -351,7 +356,7 @@ def test_hdkey_import_segwit_wifs(self): def test_hdkey_import_from_private_byte(self): keystr = b"fch\xe4w\xa8\xdd\xd4h\x08\xc5'\xcc %s" % (v[0], phrase)) self.assertEqual(v[1], phrase) self.assertEqual(v[2], seed) - k = HDKey.from_seed(seed) + k = HDKey.from_seed(seed, witness_type='legacy') self.assertEqual(k.wif(is_private=True), v[3]) # From Copyright (c) 2013 Pavol Rusnak diff --git a/tests/test_wallets.py b/tests/test_wallets.py index f0833b28..f4249283 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -142,7 +142,7 @@ class TestWalletCreate(TestWalletMixin, unittest.TestCase): def setUpClass(cls): cls.db_remove() cls.wallet = Wallet.create( - name='test_wallet_create', + name='test_wallet_create', witness_type='legacy', db_uri=cls.DATABASE_URI) def test_wallet_create(self): @@ -231,7 +231,7 @@ def test_wallet_key_for_path_normalized(self): def test_wallet_create_with_passphrase(self): passphrase = "always reward element perfect chunk father margin slab pond suffer episode deposit" - wlt = Wallet.create("wallet-passphrase", keys=passphrase, network='testnet', + wlt = Wallet.create("wallet-passphrase", keys=passphrase, network='testnet', witness_type='legacy', db_uri=self.DATABASE_URI) key0 = wlt.get_key() self.assertEqual(key0.address, "mqDeXXaFnWKNWhLmAae7zHhZDW4PMsLHPp") @@ -239,7 +239,7 @@ def test_wallet_create_with_passphrase(self): def test_wallet_create_with_passphrase_litecoin(self): passphrase = "always reward element perfect chunk father margin slab pond suffer episode deposit" wlt = Wallet.create("wallet-passphrase-litecoin", keys=passphrase, network='litecoin', - db_uri=self.DATABASE_URI) + witness_type='legacy', db_uri=self.DATABASE_URI) keys = wlt.get_keys(number_of_keys=5) self.assertEqual(keys[4].address, "Li5nEi62nAKWjv6fpixEpoLzN1pYFK621g") @@ -316,7 +316,7 @@ def test_wallet_create_bip38(self): if not USING_MODULE_SCRYPT: self.skipTest('Need scrypt module to test BIP38 wallets') passphrase = "region kite swamp float card flag chalk click gadget share wage clever" - k = HDKey().from_passphrase(passphrase) + k = HDKey().from_passphrase(passphrase, witness_type='legacy') ke = k.encrypt('hoihoi') w = wallet_create_or_open('kewallet', ke, password='hoihoi', network='bitcoin', db_uri=self.DATABASE_URI) self.assertEqual(k.private_hex, w.main_key.key_private.hex()) @@ -855,7 +855,7 @@ def test_wallet_multi_networks_send_transaction(self): pk = 'tobacco defy swarm leaf flat pyramid velvet pen minor twist maximum extend' wallet = Wallet.create( keys=pk, network='bitcoin', - name='test_wallet_multi_network_multi_account', + name='test_wallet_multi_network_multi_account', witness_type='legacy', db_uri=self.DATABASE_URI) wallet.new_key() @@ -900,7 +900,7 @@ def test_wallet_get_account_defaults(self): self.assertIn('account', account_key.name) def test_wallet_update_attributes(self): - w = Wallet.create('test_wallet_set_attributes', db_uri=self.DATABASE_URI) + w = Wallet.create('test_wallet_set_attributes', witness_type='legacy', db_uri=self.DATABASE_URI) w.new_account(network='litecoin', account_id=1200) owner = 'Satoshi' w.owner = owner @@ -1124,9 +1124,9 @@ def test_wallet_multisig_2of2(self): keys = [ HDKey('YXscyqNJ5YK411nwB4wzazXjJn9L9iLAR1zEMFcpLipDA25rZregBGgwXmprsvQLeQAsuTvemtbCWR1AHaPv2qmvkartoiFUU6' - 'qu1uafT2FETtXT', network='bitcoinlib_test'), + 'qu1uafT2FETtXT', network='bitcoinlib_test', witness_type='legacy'), HDKey('YXscyqNJ5YK411nwB4EyGbNZo9eQSUWb64vAFKHt7E2LYnbmoNz8Gyjs6xc7iYAudcnkgf127NPnaanuUgyRngAiwYBcXKGsSJ' - 'wadGhxByT2MnLd', network='bitcoinlib_test')] + 'wadGhxByT2MnLd', network='bitcoinlib_test', witness_type='legacy')] msw1 = Wallet.create('msw1', [keys[0], keys[1].subkey_for_path("m/45'").wif_public()], network='bitcoinlib_test', sort_keys=False, sigs_required=2, @@ -1318,15 +1318,15 @@ def test_wallet_multisig_reopen_wallet(self): def _open_all_wallets(): wl1 = wallet_create_or_open( 'multisigmulticur1_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', keys=[pk1, pk2.public_master(), pk3.public_master()]) wl2 = wallet_create_or_open( 'multisigmulticur2_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', keys=[pk1.public_master(), pk2, pk3.public_master()]) wl3 = wallet_create_or_open( 'multisigmulticur3_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', keys=[pk1.public_master(), pk2.public_master(), pk3]) return wl1, wl2, wl3 @@ -1356,7 +1356,7 @@ def test_wallet_multisig_network_mixups(self): pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network) wlt = wallet_create_or_open( 'multisig_network_mixups', sigs_required=2, network=network, db_uri=self.DATABASE_URI, - keys=[phrase1, pk2.public_master(), pk3.public_master()], + keys=[phrase1, pk2.public_master(), pk3.public_master()], witness_type='legacy', sort_keys=False) self.assertEqual(wlt.get_key().address, 'QjecchURWzhzUzLkhJ8Xijnm29Z9PscSqD') self.assertEqual(wlt.get_key().network.name, network) @@ -1753,7 +1753,8 @@ def test_wallet_balance_update_total(self): def test_wallet_add_dust_to_fee(self): # Send bitcoinlib test transaction and check if dust resume amount is added to fee - wlt = Wallet.create('bcltestwlt', network='bitcoinlib_test', db_uri=self.DATABASE_URI) + wlt = Wallet.create('bcltestwlt', network='bitcoinlib_test', witness_type='legacy', + db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 99992000, offline=False) @@ -1842,7 +1843,8 @@ def test_wallet_transaction_import_dict(self): del wlt def test_wallet_transaction_fee_limits(self): - wlt = Wallet.create('bcltestwlt6', network='bitcoinlib_test', db_uri=self.DATABASE_URI) + wlt = Wallet.create('bcltestwlt6', network='bitcoinlib_test', witness_type='legacy', + db_uri=self.DATABASE_URI) to_key = wlt.get_key() wlt.utxos_update() self.assertRaisesRegexp(WalletError, 'Fee per kB of 660 is lower then minimal network fee of 1000', @@ -1924,7 +1926,7 @@ def test_wallet_transaction_from_txid(self): self.assertIsNone(WalletTransaction.from_txid(w, '112233')) def test_wallet_transaction_sign_with_hex(self): - k = HDKey(network='bitcoinlib_test') + k = HDKey(network='bitcoinlib_test', witness_type='legacy') pmk = k.public_master() w = Wallet.create('wallet_tx_tests', keys=pmk, network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.utxos_update() @@ -2257,7 +2259,7 @@ def setUpClass(cls): def test_wallet_create_with_passphrase_dash(self): passphrase = "always reward element perfect chunk father margin slab pond suffer episode deposit" - wlt = Wallet.create("wallet-passphrase-litecoin", keys=passphrase, network='dash', + wlt = Wallet.create("wallet-passphrase-dash", keys=passphrase, network='dash', witness_type='legacy', db_uri=self.DATABASE_URI) keys = wlt.get_keys(number_of_keys=5) self.assertEqual(keys[4].address, "XhxXcRvTm4yZZzbH4MYz2udkdHWEMMf9GM") @@ -2277,8 +2279,8 @@ def test_wallet_import_dash(self): def test_wallet_multisig_dash(self): network = 'dash' - pk1 = HDKey(network=network) - pk2 = HDKey(network=network) + pk1 = HDKey(network=network, witness_type='legacy') + pk2 = HDKey(network=network, witness_type='legacy') wl1 = Wallet.create('multisig_test_wallet1', [pk1, pk2.public_master(multisig=True)], sigs_required=2, db_uri=self.DATABASE_URI) wl2 = Wallet.create('multisig_test_wallet2', [pk1.public_master(multisig=True), pk2], sigs_required=2, @@ -2289,9 +2291,9 @@ def test_wallet_multisig_dash(self): def test_wallet_import_private_for_known_public_multisig_dash(self): network = 'dash' - pk1 = HDKey(network=network) - pk2 = HDKey(network=network) - pk3 = HDKey(network=network) + pk1 = HDKey(network=network, witness_type='legacy') + pk2 = HDKey(network=network, witness_type='legacy') + pk3 = HDKey(network=network, witness_type='legacy') with wallet_create_or_open("mstest_dash", [pk1.public_master(multisig=True), pk2.public_master(multisig=True), pk3.public_master(multisig=True)], 2, network=network, sort_keys=False, cosigner_id=0, db_uri=self.DATABASE_URI) as wlt: @@ -2569,7 +2571,8 @@ def setUpClass(cls): cls.db_remove() def test_wallet_path_expand(self): - wlt = wallet_create_or_open('wallet_path_expand', network='bitcoin', db_uri=self.DATABASE_URI) + wlt = wallet_create_or_open('wallet_path_expand', network='bitcoin', witness_type='legacy', + db_uri=self.DATABASE_URI) self.assertListEqual(wlt.path_expand([8]), ['m', "44'", "0'", "0'", '0', '8']) self.assertListEqual(wlt.path_expand(['8']), ['m', "44'", "0'", "0'", '0', '8']) self.assertListEqual(wlt.path_expand(["99'", 1, 2]), ['m', "44'", "0'", "99'", '1', '2']) @@ -2586,7 +2589,7 @@ def test_wallet_path_expand(self): wlt.path_expand, ['m', "bestaatnie'", "coin_type'", "1", 2, 3]) def test_wallet_exotic_key_paths(self): - w = Wallet.create("simple_custom_keypath", key_path="m/change/address_index", + w = Wallet.create("simple_custom_keypath", key_path="m/change/address_index", witness_type='legacy', db_uri=self.DATABASE_URI) self.assertEqual(w.new_key().path, "m/0/1") self.assertEqual(w.new_key_change().path, "m/1/0") @@ -2594,7 +2597,7 @@ def test_wallet_exotic_key_paths(self): w = Wallet.create( "strange_key_path", keys=[HDKey(), HDKey()], purpose=100, cosigner_id=0, - key_path="m/purpose'/cosigner_index/change/address_index", + key_path="m/purpose'/cosigner_index/change/address_index", witness_type='legacy', db_uri=self.DATABASE_URI) self.assertEqual(w.new_key().path, "m/100'/0/0/0") self.assertEqual(w.new_key_change().path, "m/100'/0/1/0") From 08ef79f186bfb150d9511d382eabd676a69ed2a0 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 22 Sep 2023 18:41:58 +0200 Subject: [PATCH 003/207] [REF] Use distinguisable prefixes for bitcoinlib_testnet network --- bitcoinlib/data/networks.json | 24 +++++++-------- tests/test_keys.py | 22 ++++++++------ tests/test_wallets.py | 55 ++++++++++++++++++----------------- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/bitcoinlib/data/networks.json b/bitcoinlib/data/networks.json index 5eac02c3..cb1e378e 100644 --- a/bitcoinlib/data/networks.json +++ b/bitcoinlib/data/networks.json @@ -11,18 +11,18 @@ "prefix_bech32": "blt", "prefix_wif": "99", "prefixes_wif": [ - ["9488B21E", "YXsf", "public", false, "legacy", "p2pkh"], - ["9488B21E", "YXsf", "public", true, "legacy", "p2sh"], - ["9488ADE4", "YXsc", "private", false, "legacy", "p2pkh"], - ["9488ADE4", "YXsc", "private", true, "legacy", "p2sh"], - ["9488B21E", "YXsf", "public", false, "p2sh-segwit", "p2sh_p2wpkh"], - ["9488B21E", "YXsf", "public", true, "p2sh-segwit", "p2sh_p2wsh"], - ["9488ADE4", "YXsc", "private", false, "p2sh-segwit", "p2sh_p2wpkh"], - ["9488ADE4", "YXsc", "private", true, "p2sh-segwit", "p2sh_p2wsh"], - ["9488B21E", "YXsf", "public", false, "segwit", "p2wpkh"], - ["9488B21E", "YXsf", "public", true, "segwit", "p2wsh"], - ["9488ADE4", "YXsc", "private", false, "segwit", "p2wpkh"], - ["9488ADE4", "YXsc", "private", true, "segwit", "p2wsh"] + ["2FFFACCC", "BC11", "public", false, "legacy", "p2pkh"], + ["2FFFACCC", "BC11", "public", true, "legacy", "p2sh"], + ["2FFFADDD", "BC12", "private", false, "legacy", "p2pkh"], + ["2FFFADDD", "BC12", "private", true, "legacy", "p2sh"], + ["2FFFAEEE", "BC13", "public", false, "p2sh-segwit", "p2sh_p2wpkh"], + ["2FFFB100", "BC14", "public", true, "p2sh-segwit", "p2sh_p2wsh"], + ["2FFFB300", "BC15", "private", false, "p2sh-segwit", "p2sh_p2wpkh"], + ["2FFFB500", "BC16", "private", true, "p2sh-segwit", "p2sh_p2wsh"], + ["2FFFB666", "BC17", "public", false, "segwit", "p2wpkh"], + ["2FFFB800", "BC18", "public", true, "segwit", "p2wsh"], + ["2FFFB900", "BC19", "private", false, "segwit", "p2wpkh"], + ["2FFFBA00", "BC1A", "private", true, "segwit", "p2wsh"] ], "bip44_cointype": 9999999, "denominator": 0.00000001, diff --git a/tests/test_keys.py b/tests/test_keys.py index 5681c1f7..f3ca403c 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -360,24 +360,28 @@ def test_hdkey_import_from_private_byte(self): self.assertEqual(hdkey.address(), '17N9VQbP89ThunSq7Yo2VooXCFTW1Lp8bd') def test_hdkey_import_private_uncompressed(self): - wif = 'YXscyqNJ5YK411nwB2T35cvM1M4F4VDzbfpTxgBxFL3eDFcUrPGfGSzLG4DmwGyRY6nzRGqyBjMMnw9x7oq2mw2mUQWpCZyfopCUEvU458AMAhqj' + wif = ('BC12Se7KL1uS2bA6QNFneYit57Ac2wGdCrn5CTr94xr1NqLxvPozYzpm4d72ojPFnpLyAgpoXdad78PL9HaATYH2Y695hYcs8AF' + '1iLxUL5fk2jQv') + pk_hex = '681e34705a758455e75d761a8d33aaef6d0507e3750fb7c3848ab119438626a3' + pubkey_uncompressed = ( + '04007d7ff2fbf9486746f8beffc34e7a68f06a4938edd3b1eed4a2fe23148423c7e8d714ef853988adc2fef434' + '3ccdcb07356cfd9b8f361e3c8ec43598210c946d') + pubkey_compressed = '03007d7ff2fbf9486746f8beffc34e7a68f06a4938edd3b1eed4a2fe23148423c7' k = HDKey(wif, compressed=False) self.assertFalse(k.compressed) - self.assertEqual(k.private_hex, '427ea8c9c286fc050b8ccd8d8a7a8de57322c519ba79b3fad745bc4e97ed5a37') - self.assertEqual(k.public_hex, '0403c6eb8873620ac55a6035b0fd527e623f7659c97f30cb8d6a3fe90e924da5fa197aa4' - '9ee663e360ac71dbb514e2f8b02a90301a0ae52e8014ec805fa36040d9') + self.assertEqual(k.private_hex, pk_hex) + self.assertEqual(k.public_hex, pubkey_uncompressed) k2 = HDKey.from_wif(wif, compressed=False) self.assertFalse(k2.compressed) - self.assertEqual(k2.private_hex, '427ea8c9c286fc050b8ccd8d8a7a8de57322c519ba79b3fad745bc4e97ed5a37') - self.assertEqual(k2.public_hex, '0403c6eb8873620ac55a6035b0fd527e623f7659c97f30cb8d6a3fe90e924da5fa197aa4' - '9ee663e360ac71dbb514e2f8b02a90301a0ae52e8014ec805fa36040d9') + self.assertEqual(k2.private_hex, pk_hex) + self.assertEqual(k2.public_hex, pubkey_uncompressed) k3 = HDKey.from_wif(wif) self.assertTrue(k3.compressed) - self.assertEqual(k3.private_hex, '427ea8c9c286fc050b8ccd8d8a7a8de57322c519ba79b3fad745bc4e97ed5a37') - self.assertEqual(k3.public_hex, '0303c6eb8873620ac55a6035b0fd527e623f7659c97f30cb8d6a3fe90e924da5fa') + self.assertEqual(k3.private_hex, pk_hex) + self.assertEqual(k3.public_hex, pubkey_compressed) class TestHDKeysChildKeyDerivation(unittest.TestCase): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index f4249283..43775202 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -185,7 +185,8 @@ def test_delete_wallet(self): self.assertEqual(wallet_delete('wallet_to_remove', db_uri=self.DATABASE_URI), 1) def test_delete_wallet_no_private(self): - key = "YXsfembHRwpatrAVUFhHAd7mErqzPtfehhq5YEt4dFM2B6hVNtxnqj3JqiP4nAf9BZ9SRBmH8esrDamoA3ZFNdNshvYA8rf7bDxhEdUyuoPZ7HTF" + key = ("BC19UtECk2r9PVQYhZo2RsN5SJVTmTt6NjCqGh6KH7FoVGhV9oV3f6UdyMtSzWPUBPw2S313ZJqCCNd6kTV9xbNQWzPBUVufnp" + "sNKhh3vb3ut5bY") Wallet.create('delete_watch_only_wallet', keys=key, network='bitcoinlib_test', db_uri=self.DATABASE_URI) self.assertEqual(wallet_delete('delete_watch_only_wallet', db_uri=self.DATABASE_URI), 1) @@ -820,8 +821,8 @@ def test_wallet_multiple_networks_import_key_network(self): self.assertIn(address_ltc, addresses_ltc_in_wallet) def test_wallet_multiple_networks_import_error(self): - pk_dashtest = 'YXsfembHRwpatrAVUGY8MBUuKwhUDf9EEWeZwGoEfE5appg5rLjSfZ1GwoaNB5DgUZ2aVuU1ezmg7zDefubuWkZ17et5o' \ - 'KoMgKnjvAZ6a4Yn2QZg' + pk_dashtest = ('BC19UtECk2r9PVQYhZYzX3m4arsu6tCL5VMpKPbeGpZdpzd9FHweoSRreTFKo96FAEFsUWBrASfKussgoxTrNQfm' + 'jWFrVraLbiHf4gCkUvwHEocA') error_str = "Network bitcoinlib_test not available in this wallet, please create an account for this network " \ "first." self.assertRaisesRegexp(WalletError, error_str, self.wallet.import_key, pk_dashtest) @@ -1123,10 +1124,10 @@ def test_wallet_multisig_2of2(self): self.db_remove() keys = [ - HDKey('YXscyqNJ5YK411nwB4wzazXjJn9L9iLAR1zEMFcpLipDA25rZregBGgwXmprsvQLeQAsuTvemtbCWR1AHaPv2qmvkartoiFUU6' - 'qu1uafT2FETtXT', network='bitcoinlib_test', witness_type='legacy'), - HDKey('YXscyqNJ5YK411nwB4EyGbNZo9eQSUWb64vAFKHt7E2LYnbmoNz8Gyjs6xc7iYAudcnkgf127NPnaanuUgyRngAiwYBcXKGsSJ' - 'wadGhxByT2MnLd', network='bitcoinlib_test', witness_type='legacy')] + HDKey('BC12Se7KL1uS2bA6QQaWWrcA5kApD8UAM78dx91LrFvsvdvua3irnpQNjHUTCPJR7tZ72eGhMsy3mLPp5C' + 'SJcmKPchBvaf72i1mNY6yhrmY4RFSr', network='bitcoinlib_test', witness_type='legacy'), + HDKey('BC12Se7KL1uS2bA6QPiq4cdXWKfmQwuPPTXkRUJNBSLFZt9tPgLfgrRSfkVLRLYCYpgzsTmKybPHSX165w' + '42VBjw4Neub1KyPBfNpjFfgx9SyynF', network='bitcoinlib_test', witness_type='legacy')] msw1 = Wallet.create('msw1', [keys[0], keys[1].subkey_for_path("m/45'").wif_public()], network='bitcoinlib_test', sort_keys=False, sigs_required=2, @@ -1157,10 +1158,10 @@ def test_wallet_multisig_2of2_different_database(self): self.db_remove() keys = [ - HDKey('YXscyqNJ5YK411nwB4wzazXjJn9L9iLAR1zEMFcpLipDA25rZregBGgwXmprsvQLeQAsuTvemtbCWR1AHaPv2qmvkartoiFUU6' - 'qu1uafT2FETtXT', network='bitcoinlib_test'), - HDKey('YXscyqNJ5YK411nwB4EyGbNZo9eQSUWb64vAFKHt7E2LYnbmoNz8Gyjs6xc7iYAudcnkgf127NPnaanuUgyRngAiwYBcXKGsSJ' - 'wadGhxByT2MnLd', network='bitcoinlib_test')] + HDKey('BC12Se7KL1uS2bA6QQaWWrcA5kApD8UAM78dx91LrFvsvdvua3irnpQNjHUTCPJR7tZ72eGhMsy3mLPp5C' + 'SJcmKPchBvaf72i1mNY6yhrmY4RFSr', network='bitcoinlib_test', witness_type='legacy'), + HDKey('BC12Se7KL1uS2bA6QPiq4cdXWKfmQwuPPTXkRUJNBSLFZt9tPgLfgrRSfkVLRLYCYpgzsTmKybPHSX165w' + '42VBjw4Neub1KyPBfNpjFfgx9SyynF', network='bitcoinlib_test', witness_type='legacy')] msw1 = Wallet.create('msw1', [keys[0], keys[1].public_master(multisig=True)], network='bitcoinlib_test', sort_keys=False, sigs_required=2, db_uri=self.DATABASE_URI) @@ -1352,8 +1353,8 @@ def test_wallet_multisig_network_mixups(self): phrase1 = 'shop cloth bench traffic vintage security hour engage omit almost episode fragile' phrase2 = 'exclude twice mention orchard grit ignore display shine cheap exercise same apart' phrase3 = 'citizen obscure tribe index little welcome deer wine exile possible pizza adjust' - pk2 = HDKey.from_passphrase(phrase2, multisig=True, network=network) - pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network) + pk2 = HDKey.from_passphrase(phrase2, multisig=True, network=network, witness_type='legacy') + pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network, witness_type='legacy') wlt = wallet_create_or_open( 'multisig_network_mixups', sigs_required=2, network=network, db_uri=self.DATABASE_URI, keys=[phrase1, pk2.public_master(), pk3.public_master()], witness_type='legacy', @@ -1550,10 +1551,10 @@ def test_wallet_import_private_for_known_public_multisig(self): self.assertTrue(wlt.cosigner[2].main_key.is_private) def test_wallet_import_private_for_known_public_p2sh_segwit(self): - pk1 = HDKey('YXscyqNJ5YK411nwB3VjLYgjht3dqfKxyLdGSqNMGKhYdcK4Gh1CRSJyxS2So8KXSQrxtysS1jAmHtLnxRKa47xEiAx6hP' - 'vrj8tuEzyeR8TQNu5e') - pk2 = HDKey('YXscyqNJ5YK411nwB4Jo3JCQ1GZNetf4BrLJjZiqdWKVzoXwPtyJ5xyNdZjuEWtqCeSZGtmg7SuQerERwniHLYL3aVcnyS' - 'ciEAxk7gLgDkoZC5Lq') + pk1 = HDKey('BC15gwSkKnLsWk3GmCxBwzdbij4fXUa5j6UB8bSYJt9a81aSUCVjg7tVJaEDpxRZ4X2dt3VKjuy8po1fbo6opZ6tCqxVAg' + 'XgQKUBwWi6EGh2eLRC') + pk2 = HDKey('BC15gwSkKnLsWk3GmCWRgmp2edaya3UgmQ4TqjiBfx2cuvMC5ASQwJ5N5wwKcMp627AucznuYvTzKnhYRERcPFnEAn1a7w' + 'VKQy7FMLXzMq7N2nQq') w = Wallet.create('segwit-p2sh-p2wsh-import', [pk1, pk2.public_master(witness_type='p2sh-segwit', multisig=True)], witness_type='p2sh-segwit', network='bitcoinlib_test', db_uri=self.DATABASE_URI) @@ -1803,8 +1804,8 @@ def test_wallet_transaction_import_raw_locktime(self): def test_wallet_transaction_import_raw_segwit_fee(self): wallet_delete_if_exists('bcltestwlt-size', force=True, db_uri=self.DATABASE_URI) - pk = 'YXscyqNJ5YK411nwB2peYdMeJPmkJmMJCfNdo9JuWkEKLZhVSoUjbRRinVqqtBN2GNC2A6L1Taz1e3LWApHkC84GgTp3vr7neD' \ - 'ZTxXnvGkUwVz4c' + pk = ('BC19UtECk2r9PVQYhZT9iJZvzK7jDgQXFQxRiguB28ESn53b8BjZjT4ZyQEStPD9yKAXBhTq6Wtb9zyPQiRU4chaTjEwvtpKW' + 'EdrMscH3ZqPTtdV') wlt = Wallet.create('bcltestwlt-size', keys=pk, network='bitcoinlib_test', witness_type='segwit', db_uri=self.DATABASE_URI) wlt.utxos_update() @@ -1818,8 +1819,8 @@ def test_wallet_transaction_import_raw_segwit_fee(self): del wlt def test_wallet_transaction_load_segwit_size(self): - pk = 'YXscyqNJ5YK411nwB2peYdMeJPmkJmMJCfNdo9JuWkEKLZhVSoUjbRRinVqqtBN2GNC2A6L1Taz1e3LWApHkC84GgTp3vr7neD' \ - 'ZTxXnvGkUwVz4c' + pk = ('BC19UtECk2r9PVQYhZT9iJZvzK7jDgQXFQxRiguB28ESn53b8BjZjT4ZyQEStPD9yKAXBhTq6Wtb9zyPQiRU4chaTjEwvtpKW' + 'EdrMscH3ZqPTtdV') wlt = Wallet.create('bcltestwlt2-size', keys=pk, network='bitcoinlib_test', witness_type='segwit', db_uri=self.DATABASE_URI) wlt.utxos_update() @@ -1936,10 +1937,10 @@ def test_wallet_transaction_sign_with_hex(self): self.assertTrue(wt.verified) def test_wallet_transaction_sign_with_wif(self): - wif = 'YXscyqNJ5YK411nwB4eU6PmyGTJkBUHjgXEf53z4TTjHCDXPPXKJD2PyfXonwtT7VwSdqcZJS2oeDbvg531tEsx3yq4425Mfrb9aS' \ - 'PyNQ5bUGFwu' - wif2 = 'YXscyqNJ5YK411nwB4UK8ScMahPWewyKrTBjgM5BZKRkPg8B2HmKT3r8yc2GFg9GqgFXaWmxkTRhNkRGVxbzUREMH8L5HxoKGCY8' \ - 'WDdf1GcW2k8q' + wif = ('BC17qWy2RMw8AmwsqwTXpokwXXwhaWmUpqtAc5iGGzrFXs13PkKERJUyobB9YUbzT8hJ8EiCtcqdEpeRy7wyvE1esehD' + '8bVpgzzdEw9ndQbjyF5w') + wif2 = ('BC17qWy2RMw8AmwsqwTjmX2SwwSHBNA2c6KyGHs5Kghg3q6dPa4ajP1jwFBPCkoSeXWsPAiVD2iAcroVc6cJQmHrYatviN' + 'Ck5jDM83DkbPGFxbCK') w = wallet_create_or_open('test_wallet_transaction_sign_with_wif', keys=[wif, HDKey(wif2).public_master_multisig(witness_type='segwit')], witness_type='segwit', network='bitcoinlib_test', @@ -2029,8 +2030,8 @@ def test_wallet_select_inputs(self): self.assertEqual(len(wlt.select_inputs(150000000)), 2) def test_wallet_transaction_create_exceptions(self): - wif = 'YXscyqNJ5YK411nwB3kahnuWqF2KUJfJNRGG1n3zLPi3MSPCRcD2cmcVz1UKBjTMuMxAfXrbSAe5qJfu5nnSuRZKEtqJt' \ - 'FwNjcNknbseoKp1vR2h' + wif = ('BC17qWy2RMw8AmwsqwTjmX2SwwSHBNA2c6KyGHs5Kghg3q6dPa4ajP1jwFBPCkoSeXWsPAiVD2iAcroVc6cJQmHrYatvi' + 'NCk5jDM83DkbPGFxbCK') wlt = Wallet.create('test_wallet_transaction_create_exceptions', keys=wif, db_uri=self.DATABASE_URI) wlt.utxos_update() self.assertRaisesRegexp(WalletError, "Output array must be a list of tuples with address and amount. " From 51253b8896f05ebec318baf34b2d423c1371788c Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 22 Sep 2023 22:53:15 +0200 Subject: [PATCH 004/207] [REF] Fix unittests with legacy witness type --- tests/test_wallets.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 43775202..dc985112 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1264,7 +1264,7 @@ def test_wallet_multisig_2of2_with_single_key(self): k2 = wl.new_key() k3 = wl.new_key_change() wl.utxos_update() - self.assertEqual(wl.public_master()[1].wif, keys[1].wif()) + self.assertEqual(wl.public_master()[1].wif, keys[1].wif(multisig=True)) key_names = [k.name for k in wl.keys(is_active=False)] self.assertListEqual(key_names, [k1.name, k2.name, k3.name]) @@ -1319,15 +1319,15 @@ def test_wallet_multisig_reopen_wallet(self): def _open_all_wallets(): wl1 = wallet_create_or_open( 'multisigmulticur1_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='segwit', keys=[pk1, pk2.public_master(), pk3.public_master()]) wl2 = wallet_create_or_open( 'multisigmulticur2_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='segwit', keys=[pk1.public_master(), pk2, pk3.public_master()]) wl3 = wallet_create_or_open( 'multisigmulticur3_tst', sigs_required=2, network=network, - db_uri=self.DATABASE_URI, sort_keys=False, witness_type='legacy', + db_uri=self.DATABASE_URI, sort_keys=False, witness_type='segwit', keys=[pk1.public_master(), pk2.public_master(), pk3]) return wl1, wl2, wl3 @@ -1341,11 +1341,13 @@ def _open_all_wallets(): pk3 = HDKey.from_passphrase(phrase3, multisig=True, network=network) wallets = _open_all_wallets() for wlt in wallets: - self.assertEqual(wlt.get_key(cosigner_id=1).address, 'MQVt7KeRHGe35b9ziZo16T5y4fQPg6Up7q') + self.assertEqual(wlt.get_key(cosigner_id=1).address, + 'ltc1qmw3e97pgrwypr0378wjje984guu0jy3ye4n523lcymk3rctuef6q7t3sek') del wallets wallets2 = _open_all_wallets() for wlt in wallets2: - self.assertEqual(wlt.get_key(cosigner_id=1).address, 'MQVt7KeRHGe35b9ziZo16T5y4fQPg6Up7q') + self.assertEqual(wlt.get_key(cosigner_id=1).address, + 'ltc1qmw3e97pgrwypr0378wjje984guu0jy3ye4n523lcymk3rctuef6q7t3sek') def test_wallet_multisig_network_mixups(self): self.db_remove() @@ -1937,10 +1939,10 @@ def test_wallet_transaction_sign_with_hex(self): self.assertTrue(wt.verified) def test_wallet_transaction_sign_with_wif(self): - wif = ('BC17qWy2RMw8AmwsqwTXpokwXXwhaWmUpqtAc5iGGzrFXs13PkKERJUyobB9YUbzT8hJ8EiCtcqdEpeRy7wyvE1esehD' - '8bVpgzzdEw9ndQbjyF5w') - wif2 = ('BC17qWy2RMw8AmwsqwTjmX2SwwSHBNA2c6KyGHs5Kghg3q6dPa4ajP1jwFBPCkoSeXWsPAiVD2iAcroVc6cJQmHrYatviN' - 'Ck5jDM83DkbPGFxbCK') + wif = ('BC19UtECk2r9PVQYhZuLSVjB6M7QPkAQSJN59RJKZQuuuPxaxNBEwmnfpWYvrQTrJZCANKoXBm7HKY78dVHjTkqoqA67aUf' + 'NSLZjuwNGDBMQD7uM') + wif2 = ('BC19UtECk2r9PVQYhYJrXwB3We4E9Xc6uJngAEoqBrntN1gpGZwAWKRdcupdf2iKFLfY3pYRxHAi99EZ7dyYcKLZ2a7999' + 'Lu2NRSZzToFXib5kcE') w = wallet_create_or_open('test_wallet_transaction_sign_with_wif', keys=[wif, HDKey(wif2).public_master_multisig(witness_type='segwit')], witness_type='segwit', network='bitcoinlib_test', From 90f78173e545c1740fb51888698680ee2fea6314 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 22 Sep 2023 23:25:17 +0200 Subject: [PATCH 005/207] [REF] Use segwit in test_tools --- tests/test_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 68de0e98..e22753cb 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -79,7 +79,7 @@ def test_tools_clw_create_wallet(self): (self.python_executable, self.clw_executable, self.DATABASE_URI) cmd_wlt_delete = "%s %s test --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "14guS7uQpEbgf1e8TDo1zTEURJW3NGPc9E" + output_wlt_create = "bc1qdv5tuzrluh4lzhnu59je9n83w4hkqjhgg44d5g" output_wlt_delete = "Wallet test has been removed" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) @@ -147,7 +147,7 @@ def test_tools_clw_transaction_with_script(self): (self.python_executable, self.clw_executable, self.DATABASE_URI) cmd_wlt_delete = "%s %s test2 --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "21GPfxeCbBunsVev4uS6exPhqE8brPs1ZDF" + output_wlt_create = "blt1qj0mgwyhxuw9p0ngj5kqnxhlrx8ypecqekm2gr7" output_wlt_transaction = 'Transaction pushed to network' output_wlt_delete = "Wallet test2 has been removed" From 171b18aa322c98f6229c01c0a93eb75d439e2ef6 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 25 Sep 2023 13:45:01 +0200 Subject: [PATCH 006/207] [ADD] Option to use wallet with mixed witness types: segwit, p2sh-segwit and legacy --- bitcoinlib/db.py | 6 ++++++ bitcoinlib/wallets.py | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 91d697db..a01e4837 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -502,5 +502,11 @@ def db_update(db, version_db, code_version=BITCOINLIB_VERSION): column = Column('witnesses', LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") add_column(db.engine, 'transaction_inputs', column) # version_db = db_update_version_id(db, '0.6.4') + if True or version_db < '7.0.0' and code_version >= '7.0.0': + # Add witness_type to keys table so we can use mixed keys in a single wallet + column = Column('witness_type', String(20), doc="Wallet witness type. Can be 'legacy', 'segwit' or " + "'p2sh-segwit'. Default is segwit.") + add_column(db.engine, 'keys', column) + version_db = db_update_version_id(db, code_version) return version_db diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 7333a4fb..ca0c6827 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1157,7 +1157,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 :type sort_keys: bool :param password: Password to protect passphrase, only used if a passphrase is supplied in the 'key' argument. :type password: str - :param witness_type: Specify witness type, default is 'legacy'. Use 'segwit' for native segregated witness wallet, or 'p2sh-segwit' for legacy compatible wallets + :param witness_type: Specify witness type, default is 'segwit', for native segregated witness + wallet. Use 'legacy' for an old-style wallets or 'p2sh-segwit' for legacy compatible wallets :type witness_type: str :param encoding: Encoding used for address generation: base58 or bech32. Default is derive from wallet and/or witness type :type encoding: str @@ -1190,7 +1191,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 multisig = False if scheme not in ['bip32', 'single']: raise WalletError("Only bip32 or single key scheme's are supported at the moment") - if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit']: + if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit', 'mixed']: raise WalletError("Witness type %s not supported at the moment" % witness_type) if name.isdigit(): raise WalletError("Wallet name '%s' invalid, please include letter characters" % name) @@ -1255,12 +1256,13 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " "Please use p2sh-segwit instead") + witness_type_keys = witness_type if witness_type != 'mixed' else 'segwit' if not key_path: if scheme == 'single': key_path = ['m'] purpose = 0 else: - ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and + ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type_keys and k['multisig'] == multisig and k['purpose'] is not None] if len(ks) > 1: raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " @@ -1274,7 +1276,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if purpose is None: purpose = 0 if not encoding: - encoding = get_encoding_from_witness(witness_type) + encoding = get_encoding_from_witness(witness_type_keys) if multisig: key = '' From d8e790dd2198c789c34dfb1c658d1a8a6323b260 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 25 Sep 2023 14:17:10 +0200 Subject: [PATCH 007/207] [REF] Update db to use segwit by default and add mixed witness type --- bitcoinlib/db.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index a01e4837..9fe39ac6 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -212,8 +212,9 @@ class DbWallet(Base): purpose = Column(Integer, doc="Wallet purpose ID. BIP-44 purpose field, indicating which key-scheme is used default is 44") scheme = Column(String(25), doc="Key structure type, can be BIP-32 or single") - witness_type = Column(String(20), default='legacy', - doc="Wallet witness type. Can be 'legacy', 'segwit' or 'p2sh-segwit'. Default is legacy.") + witness_type = Column(String(20), default='segwit', + doc="Wallet witness type. Can be 'legacy', 'segwit', 'p2sh-segwit' or 'mixed. Default is " + "segwit.") encoding = Column(String(15), default='base58', doc="Default encoding to use for address generation, i.e. base58 or bech32. Default is base58.") main_key_id = Column(Integer, @@ -306,6 +307,9 @@ class DbKey(Base): network_name = Column(String(20), ForeignKey('networks.name'), doc="Name of key network, i.e. bitcoin, litecoin, dash") latest_txid = Column(LargeBinary(32), doc="TxId of latest transaction downloaded from the blockchain") + witness_type = Column(String(20), default='segwit', + doc="Key witness type, only specify when using mixed wallets. Can be 'legacy', 'segwit' or " + "'p2sh-segwit'. Default is segwit.") network = relationship("DbNetwork", doc="DbNetwork object for this key") multisig_parents = relationship("DbKeyMultisigChildren", backref='child_key', primaryjoin=id == DbKeyMultisigChildren.child_id, @@ -368,7 +372,7 @@ class DbTransaction(Base): account_id = Column(Integer, index=True, doc="ID of account") wallet = relationship("DbWallet", back_populates="transactions", doc="Link to Wallet object which contains this transaction") - witness_type = Column(String(20), default='legacy', doc="Is this a legacy or segwit transaction?") + witness_type = Column(String(20), default='segwit', doc="Is this a legacy or segwit transaction?") version = Column(BigInteger, default=1, doc="Tranaction version. Default is 1 but some wallets use another version number") locktime = Column(BigInteger, default=0, @@ -434,8 +438,8 @@ class DbTransactionInput(Base): doc="Address string of input, used if no key is associated. " "An cryptocurrency address is a hash of the public key or a redeemscript") witnesses = Column(LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") - witness_type = Column(String(20), default='legacy', - doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is legacy") + witness_type = Column(String(20), default='segwit', + doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is segwit") prev_txid = Column(LargeBinary(32), doc="Transaction hash of previous transaction. Previous unspent outputs (UTXO) is spent " "in this input") @@ -507,6 +511,14 @@ def db_update(db, version_db, code_version=BITCOINLIB_VERSION): column = Column('witness_type', String(20), doc="Wallet witness type. Can be 'legacy', 'segwit' or " "'p2sh-segwit'. Default is segwit.") add_column(db.engine, 'keys', column) + # TODO: Add to upgrade script, use alembic?? + # - Add mixed to wallet_constraint_allowed_types + # - Wallet.witness_type default='segwit', doc="Wallet witness type. Can be 'legacy', 'segwit', 'p2sh-segwit' or + # 'mixed. Default is " "segwit.") + # - Transaction.witness_type default='segwit' + # - Transaction.input: default='segwit', doc="Type of transaction, can be legacy, + # segwit or p2sh-segwit. Default is segwit") + version_db = db_update_version_id(db, code_version) return version_db From 9d6e4ae2adf697b486bd37a4f8b568b83cc1534c Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 25 Sep 2023 16:40:51 +0200 Subject: [PATCH 008/207] [ADD] Allow to create mixed type wallet, use segwit as default --- bitcoinlib/wallets.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index ca0c6827..e59c83d5 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1039,6 +1039,9 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ _logger.info("Create new wallet '%s'" % name) if not name: raise WalletError("Please enter wallet name") + wallet_witness_type = witness_type + if witness_type == 'mixed': + witness_type = DEFAULT_WITNESS_TYPE if not isinstance(key_path, list): key_path = key_path.split('/') @@ -1062,7 +1065,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ key_path = '/'.join(key_path) session.merge(DbNetwork(name=network)) new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, - sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, + sort_keys=sort_keys, witness_type=wallet_witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, key_path=key_path) session.add(new_wallet) @@ -1193,6 +1196,9 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 raise WalletError("Only bip32 or single key scheme's are supported at the moment") if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit', 'mixed']: raise WalletError("Witness type %s not supported at the moment" % witness_type) + wallet_witness_type = witness_type + if witness_type == 'mixed': + witness_type = DEFAULT_WITNESS_TYPE if name.isdigit(): raise WalletError("Wallet name '%s' invalid, please include letter characters" % name) @@ -1300,7 +1306,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 main_key_path = 'm' hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, - scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, + scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=wallet_witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) @@ -1390,7 +1396,10 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key_id: self.main_key } self.providers = None + self.wallet_witness_type = db_wlt.witness_type self.witness_type = db_wlt.witness_type + if db_wlt.witness_type == 'mixed': + self.witness_type = DEFAULT_WITNESS_TYPE self.encoding = db_wlt.encoding self.multisig = db_wlt.multisig self.cosigner_id = db_wlt.cosigner_id @@ -1717,7 +1726,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_typenetwork=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -4167,7 +4176,7 @@ def info(self, detail=3): if self.multisig: print(" Multisig Wallet IDs %s" % str([w.wallet_id for w in self.cosigner]).strip('[]')) print(" Cosigner ID %s" % self.cosigner_id) - print(" Witness type %s" % self.witness_type) + print(" Witness type %s" % self.wallet_witness_type) print(" Main network %s" % self.network.name) print(" Latest update %s" % self.last_updated) From dca94d168f25a405b83ea9f8ec57721adb2f01b1 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 25 Sep 2023 20:45:06 +0200 Subject: [PATCH 009/207] [REV] Removed extra witness_type 'mixed' --- bitcoinlib/db.py | 2 +- bitcoinlib/wallets.py | 24 +++++++----------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 9fe39ac6..03b98d2b 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -245,7 +245,7 @@ class DbWallet(Base): __table_args__ = ( CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr', 'mixed']), + CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr']), name='wallet_constraint_allowed_types'), ) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index e59c83d5..3b745f71 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1039,9 +1039,6 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ _logger.info("Create new wallet '%s'" % name) if not name: raise WalletError("Please enter wallet name") - wallet_witness_type = witness_type - if witness_type == 'mixed': - witness_type = DEFAULT_WITNESS_TYPE if not isinstance(key_path, list): key_path = key_path.split('/') @@ -1065,7 +1062,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ key_path = '/'.join(key_path) session.merge(DbNetwork(name=network)) new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, - sort_keys=sort_keys, witness_type=wallet_witness_type, parent_id=parent_id, encoding=encoding, + sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, key_path=key_path) session.add(new_wallet) @@ -1194,11 +1191,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 multisig = False if scheme not in ['bip32', 'single']: raise WalletError("Only bip32 or single key scheme's are supported at the moment") - if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit', 'mixed']: + if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit']: raise WalletError("Witness type %s not supported at the moment" % witness_type) - wallet_witness_type = witness_type - if witness_type == 'mixed': - witness_type = DEFAULT_WITNESS_TYPE if name.isdigit(): raise WalletError("Wallet name '%s' invalid, please include letter characters" % name) @@ -1262,13 +1256,12 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " "Please use p2sh-segwit instead") - witness_type_keys = witness_type if witness_type != 'mixed' else 'segwit' if not key_path: if scheme == 'single': key_path = ['m'] purpose = 0 else: - ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type_keys and + ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None] if len(ks) > 1: raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " @@ -1282,7 +1275,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if purpose is None: purpose = 0 if not encoding: - encoding = get_encoding_from_witness(witness_type_keys) + encoding = get_encoding_from_witness(witness_type) if multisig: key = '' @@ -1306,7 +1299,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 main_key_path = 'm' hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, - scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=wallet_witness_type, + scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) @@ -1396,10 +1389,7 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key_id: self.main_key } self.providers = None - self.wallet_witness_type = db_wlt.witness_type self.witness_type = db_wlt.witness_type - if db_wlt.witness_type == 'mixed': - self.witness_type = DEFAULT_WITNESS_TYPE self.encoding = db_wlt.encoding self.multisig = db_wlt.multisig self.cosigner_id = db_wlt.cosigner_id @@ -1726,7 +1716,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_typenetwork=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -4176,7 +4166,7 @@ def info(self, detail=3): if self.multisig: print(" Multisig Wallet IDs %s" % str([w.wallet_id for w in self.cosigner]).strip('[]')) print(" Cosigner ID %s" % self.cosigner_id) - print(" Witness type %s" % self.wallet_witness_type) + print(" Witness type %s" % self.witness_type) print(" Main network %s" % self.network.name) print(" Latest update %s" % self.last_updated) From 9e6a1d9270e8af09ccc7bd069c99f7bcfdb23628 Mon Sep 17 00:00:00 2001 From: Lennart Date: Sun, 1 Oct 2023 17:17:15 +0200 Subject: [PATCH 010/207] [ADD] witness type to new_key method --- bitcoinlib/wallets.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 3b745f71..83e8cb4a 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1716,7 +1716,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -2090,7 +2090,7 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, network=None, recreate=False): + address_index=0, change=0, witness_type=None, network=None, recreate=False): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2135,6 +2135,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi """ network, account_id, _ = self._get_account_defaults(network, account_id) + witness_type = self.witness_type if not witness_type else witness_type cosigner_id = cosigner_id if cosigner_id is not None else self.cosigner_id level_offset_key = level_offset if level_offset and self.main_key and level_offset > 0: @@ -2145,7 +2146,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi key_path = self.cosigner[cosigner_id].key_path fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id, purpose=self.purpose, address_index=address_index, change=change, - witness_type=self.witness_type, network=network) + witness_type=witness_type, network=network) if self.multisig and self.cosigner: public_keys = [] @@ -2201,7 +2202,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi key_name = key_name.replace("'", "").replace("_", " ") nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=self.purpose, path=newpath, parent_id=parent_id, - encoding=self.encoding, witness_type=self.witness_type, + encoding=self.encoding, witness_type=witness_type, cosigner_id=cosigner_id, network=network, session=self._session) self._key_objects.update({nk.key_id: nk}) parent_id = nk.key_id From b807b1ef0e6d2bed221cb9248a26114152593702 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 13 Oct 2023 10:44:26 +0200 Subject: [PATCH 011/207] [REV] Remove witness_type from create key methods --- bitcoinlib/db.py | 2 +- bitcoinlib/wallets.py | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 03b98d2b..9fe39ac6 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -245,7 +245,7 @@ class DbWallet(Base): __table_args__ = ( CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr']), + CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr', 'mixed']), name='wallet_constraint_allowed_types'), ) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 83e8cb4a..758668e9 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1039,6 +1039,9 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ _logger.info("Create new wallet '%s'" % name) if not name: raise WalletError("Please enter wallet name") + wallet_witness_type = witness_type + if witness_type == 'mixed': + witness_type = DEFAULT_WITNESS_TYPE if not isinstance(key_path, list): key_path = key_path.split('/') @@ -1062,7 +1065,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ key_path = '/'.join(key_path) session.merge(DbNetwork(name=network)) new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, - sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, + sort_keys=sort_keys, witness_type=wallet_witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, key_path=key_path) session.add(new_wallet) @@ -1191,8 +1194,11 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 multisig = False if scheme not in ['bip32', 'single']: raise WalletError("Only bip32 or single key scheme's are supported at the moment") - if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit']: + if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit', 'mixed']: raise WalletError("Witness type %s not supported at the moment" % witness_type) + wallet_witness_type = witness_type + if witness_type == 'mixed': + witness_type = DEFAULT_WITNESS_TYPE if name.isdigit(): raise WalletError("Wallet name '%s' invalid, please include letter characters" % name) @@ -1256,12 +1262,13 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " "Please use p2sh-segwit instead") + witness_type_keys = witness_type if witness_type != 'mixed' else 'segwit' if not key_path: if scheme == 'single': key_path = ['m'] purpose = 0 else: - ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and + ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type_keys and k['multisig'] == multisig and k['purpose'] is not None] if len(ks) > 1: raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " @@ -1275,7 +1282,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if purpose is None: purpose = 0 if not encoding: - encoding = get_encoding_from_witness(witness_type) + encoding = get_encoding_from_witness(witness_type_keys) if multisig: key = '' @@ -1299,7 +1306,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 main_key_path = 'm' hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, - scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, + scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=wallet_witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) @@ -1389,7 +1396,10 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key_id: self.main_key } self.providers = None + self.wallet_witness_type = db_wlt.witness_type self.witness_type = db_wlt.witness_type + if db_wlt.witness_type == 'mixed': + self.witness_type = DEFAULT_WITNESS_TYPE self.encoding = db_wlt.encoding self.multisig = db_wlt.multisig self.cosigner_id = db_wlt.cosigner_id @@ -1716,7 +1726,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -2090,7 +2100,7 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, witness_type=None, network=None, recreate=False): + address_index=0, change=0, network=None, recreate=False): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2135,7 +2145,6 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi """ network, account_id, _ = self._get_account_defaults(network, account_id) - witness_type = self.witness_type if not witness_type else witness_type cosigner_id = cosigner_id if cosigner_id is not None else self.cosigner_id level_offset_key = level_offset if level_offset and self.main_key and level_offset > 0: @@ -2146,7 +2155,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi key_path = self.cosigner[cosigner_id].key_path fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id, purpose=self.purpose, address_index=address_index, change=change, - witness_type=witness_type, network=network) + witness_type=self.witness_type, network=network) if self.multisig and self.cosigner: public_keys = [] @@ -2202,7 +2211,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi key_name = key_name.replace("'", "").replace("_", " ") nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=self.purpose, path=newpath, parent_id=parent_id, - encoding=self.encoding, witness_type=witness_type, + encoding=self.encoding, witness_type=self.witness_type, cosigner_id=cosigner_id, network=network, session=self._session) self._key_objects.update({nk.key_id: nk}) parent_id = nk.key_id @@ -4167,7 +4176,7 @@ def info(self, detail=3): if self.multisig: print(" Multisig Wallet IDs %s" % str([w.wallet_id for w in self.cosigner]).strip('[]')) print(" Cosigner ID %s" % self.cosigner_id) - print(" Witness type %s" % self.witness_type) + print(" Witness type %s" % self.wallet_witness_type) print(" Main network %s" % self.network.name) print(" Latest update %s" % self.last_updated) From fb4e04a4ffd157c0b147a9f16830c22be030a58a Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 13 Oct 2023 10:51:14 +0200 Subject: [PATCH 012/207] [REV] Fix f*@ed up reverted commits --- bitcoinlib/db.py | 2 +- bitcoinlib/wallets.py | 22 ++++++---------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 9fe39ac6..03b98d2b 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -245,7 +245,7 @@ class DbWallet(Base): __table_args__ = ( CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr', 'mixed']), + CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit', 'p2tr']), name='wallet_constraint_allowed_types'), ) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 758668e9..3b745f71 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1039,9 +1039,6 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ _logger.info("Create new wallet '%s'" % name) if not name: raise WalletError("Please enter wallet name") - wallet_witness_type = witness_type - if witness_type == 'mixed': - witness_type = DEFAULT_WITNESS_TYPE if not isinstance(key_path, list): key_path = key_path.split('/') @@ -1065,7 +1062,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ key_path = '/'.join(key_path) session.merge(DbNetwork(name=network)) new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, - sort_keys=sort_keys, witness_type=wallet_witness_type, parent_id=parent_id, encoding=encoding, + sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, key_path=key_path) session.add(new_wallet) @@ -1194,11 +1191,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 multisig = False if scheme not in ['bip32', 'single']: raise WalletError("Only bip32 or single key scheme's are supported at the moment") - if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit', 'mixed']: + if witness_type not in [None, 'legacy', 'p2sh-segwit', 'segwit']: raise WalletError("Witness type %s not supported at the moment" % witness_type) - wallet_witness_type = witness_type - if witness_type == 'mixed': - witness_type = DEFAULT_WITNESS_TYPE if name.isdigit(): raise WalletError("Wallet name '%s' invalid, please include letter characters" % name) @@ -1262,13 +1256,12 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " "Please use p2sh-segwit instead") - witness_type_keys = witness_type if witness_type != 'mixed' else 'segwit' if not key_path: if scheme == 'single': key_path = ['m'] purpose = 0 else: - ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type_keys and + ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None] if len(ks) > 1: raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " @@ -1282,7 +1275,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if purpose is None: purpose = 0 if not encoding: - encoding = get_encoding_from_witness(witness_type_keys) + encoding = get_encoding_from_witness(witness_type) if multisig: key = '' @@ -1306,7 +1299,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 main_key_path = 'm' hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, - scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=wallet_witness_type, + scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) @@ -1396,10 +1389,7 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key_id: self.main_key } self.providers = None - self.wallet_witness_type = db_wlt.witness_type self.witness_type = db_wlt.witness_type - if db_wlt.witness_type == 'mixed': - self.witness_type = DEFAULT_WITNESS_TYPE self.encoding = db_wlt.encoding self.multisig = db_wlt.multisig self.cosigner_id = db_wlt.cosigner_id @@ -4176,7 +4166,7 @@ def info(self, detail=3): if self.multisig: print(" Multisig Wallet IDs %s" % str([w.wallet_id for w in self.cosigner]).strip('[]')) print(" Cosigner ID %s" % self.cosigner_id) - print(" Witness type %s" % self.wallet_witness_type) + print(" Witness type %s" % self.witness_type) print(" Main network %s" % self.network.name) print(" Latest update %s" % self.last_updated) From 86bb94616affdac5aa8f425662b9c70e5193ff9b Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 13 Oct 2023 13:18:41 +0200 Subject: [PATCH 013/207] [REF] Use segwit as default in HDKey, Key --- bitcoinlib/keys.py | 7 ++++--- bitcoinlib/wallets.py | 4 ++-- tests/test_keys.py | 8 ++++---- tests/test_wallets.py | 40 ++++++++++++++++++++++++++-------------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 7c94ae5b..b73dbcc1 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -128,7 +128,7 @@ def get_key_format(key, is_private=None): key_format = "" networks = None script_types = [] - witness_types = ['legacy'] + witness_types = [DEFAULT_WITNESS_TYPE] multisig = [False] # if isinstance(key, bytes) and len(key) in [128, 130]: @@ -528,9 +528,10 @@ def parse(cls, address, compressed=None, encoding=None, depth=None, change=None, if network is None: network = addr_dict['network'] script_type = addr_dict['script_type'] + witness_type = addr_dict['witness_type'] return Address(hashed_data=public_key_hash_bytes, prefix=prefix, script_type=script_type, - compressed=compressed, encoding=addr_dict['encoding'], depth=depth, change=change, - address_index=address_index, network=network, network_overrides=network_overrides) + witness_type=witness_type, compressed=compressed, encoding=addr_dict['encoding'], depth=depth, + change=change, address_index=address_index, network=network, network_overrides=network_overrides) def __init__(self, data='', hashed_data='', prefix=None, script_type=None, compressed=None, encoding=None, witness_type=None, witver=0, depth=None, change=None, diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 3b745f71..997a0e45 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1233,7 +1233,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if isinstance(key, WalletKey): key = key._hdkey_object else: - key = HDKey(key, password=password, network=network) + key = HDKey(key, password=password, witness_type=witness_type, network=network) except BKeyError: try: scheme = 'single' @@ -1638,7 +1638,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_t if network not in self.network_list(): raise WalletError("Network %s not available in this wallet, please create an account for this " "network first." % network) - hdkey = HDKey(key, network=network, key_type=key_type) + hdkey = HDKey(key, network=network, key_type=key_type, witness_type=self.witness_type) if not self.multisig: if self.main_key and self.main_key.depth == self.depth_public_master and \ diff --git a/tests/test_keys.py b/tests/test_keys.py index f3ca403c..eeb88916 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -313,14 +313,14 @@ def test_hdkey_import_extended_public_key(self): self.assertEqual(extkey, self.k.wif()) def test_hdkey_import_simple_key(self): - self.k = HDKey('L45TpiVN3C8Q3MoosGDzug1acpnFjysseBLVboszztmEyotdSJ9g') + self.k = HDKey('L45TpiVN3C8Q3MoosGDzug1acpnFjysseBLVboszztmEyotdSJ9g', witness_type='legacy') self.assertEqual( 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAbeoRRpMHE67jGmBQKCr2YovK2G23x5uzaztRbEW9pc' 'j6SqMFd', self.k.wif(is_private=True)) def test_hdkey_import_bip38_key(self): if USING_MODULE_SCRYPT: - self.k = HDKey('6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo', + self.k = HDKey('6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo', witness_type='legacy', password='TestingOneTwoThree') self.assertEqual('L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP', self.k.wif_key()) @@ -548,7 +548,7 @@ def test_hdkey_info(self): def test_hdkey_network_change(self): pk = '688e4b153100f6d4526a00a3fffb47d971a32a54950ec00fab8c22fa8480edfe' - k = HDKey(pk) + k = HDKey(pk, witness_type='legacy') k.network_change('litecoin') self.assertEqual(k.address(), 'LPsPTgctprGZ6FEc7QFAugr6qg8XV3X4tg') @@ -596,7 +596,7 @@ def test_bip38_other_networks(self): def test_bip38_hdkey_method(self): pkwif = '5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5' bip38_wif = '6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq' - k = HDKey(pkwif) + k = HDKey(pkwif, witness_type='legacy') self.assertEqual(k.encrypt('Satoshi'), bip38_wif) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index dc985112..d5cf93a2 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -319,6 +319,16 @@ def test_wallet_create_bip38(self): passphrase = "region kite swamp float card flag chalk click gadget share wage clever" k = HDKey().from_passphrase(passphrase, witness_type='legacy') ke = k.encrypt('hoihoi') + w = wallet_create_or_open('kewallet', ke, password='hoihoi', network='bitcoin', witness_type='legacy', + db_uri=self.DATABASE_URI) + self.assertEqual(k.private_hex, w.main_key.key_private.hex()) + + def test_wallet_create_bip38_segwit(self): + if not USING_MODULE_SCRYPT: + self.skipTest('Need scrypt module to test BIP38 wallets') + passphrase = "region kite swamp float card flag chalk click gadget share wage clever" + k = HDKey().from_passphrase(passphrase) + ke = k.encrypt('hoihoi') w = wallet_create_or_open('kewallet', ke, password='hoihoi', network='bitcoin', db_uri=self.DATABASE_URI) self.assertEqual(k.private_hex, w.main_key.key_private.hex()) @@ -602,7 +612,7 @@ def test_wallet_keys_single_key(self): def test_wallet_create_uncompressed_masterkey(self): wlt = wallet_create_or_open('uncompressed_test', keys='68vBWcBndYGLpd4KmeNTk1gS1A71zyDX6uVQKCxq6umYKyYUav5', - network='bitcoinlib_test', db_uri=self.DATABASE_URI) + network='bitcoinlib_test', witness_type='legacy', db_uri=self.DATABASE_URI) wlt.get_key() wlt.utxos_update() self.assertIsNone(wlt.sweep('216xtQvbcG4o7Yz33n7VCGyaQhiytuvoqJY', offline=False).error) @@ -611,7 +621,7 @@ def test_wallet_create_invalid_key(self): # Test for issue #206 key_correct = HDKey(witness_type='segwit', network='testnet') key_invalid = HDKey(witness_type='segwit', network='testnet') - w = wallet_create_or_open('my-awesome-wallet55', keys=key_correct, witness_type='segwit', network='testnet', + wallet_create_or_open('my-awesome-wallet55', keys=key_correct, witness_type='segwit', network='testnet', db_uri=self.DATABASE_URI) self.assertRaisesRegexp(AssertionError, '', Wallet, 'my-awesome-wallet55', main_key_object=key_invalid, db_uri=self.DATABASE_URI) @@ -783,7 +793,7 @@ def setUpClass(cls): cls.pk = 'xprv9s21ZrQH143K4478MENLXpSXSvJSRYsjD2G3sY7s5sxAGubyujDmt9Qzfd1me5s1HokWGGKW9Uft8eB9dqryybAcFZ5JAs' \ 'rg84jAVYwKJ8c' cls.wallet = Wallet.create( - keys=cls.pk, network='dash', + keys=cls.pk, network='dash', witness_type='legacy', name='test_wallet_multicurrency', db_uri=cls.DATABASE_URI) @@ -1036,14 +1046,14 @@ def test_wallet_multisig_bitcoinlib_testnet_transaction_send(self): def test_wallet_multisig_bitcoin_transaction_send_offline(self): self.db_remove() - pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d') + pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', witness_type='legacy') key_list = [ - HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067'), + HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', witness_type='legacy'), pk2.public_master(multisig=True).public(), HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6').public_master( - multisig=True).public(), + multisig=True, witness_type='legacy').public(), ] - wl = Wallet.create('multisig_test_bitcoin_send', key_list, sigs_required=2, + wl = Wallet.create('multisig_test_bitcoin_send', key_list, sigs_required=2, witness_type='legacy', db_uri=self.DATABASE_URI) wl.utxo_add(wl.get_key().address, 200000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) @@ -1096,15 +1106,17 @@ def test_wallet_multisig_bitcoin_transaction_send_fee_priority(self): def test_wallet_multisig_litecoin_transaction_send_offline(self): self.db_remove() network = 'litecoin_legacy' - pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', network=network) + pk2 = HDKey('e2cbed99ad03c500f2110f1a3c90e0562a3da4ba0cff0e74028b532c3d69d29d', witness_type='legacy', + network=network) key_list = [ - HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', network=network), + HDKey('e9e5095d3e26643cc4d996efc6cb9a8d8eb55119fdec9fa28a684ba297528067', witness_type='legacy', + network=network), pk2.public_master(multisig=True), HDKey('86b77aee5cfc3a55eb0b1099752479d82cb6ebaa8f1c4e9ef46ca0d1dc3847e6', - network=network).public_master(multisig=True), + witness_type='legacy', network=network).public_master(multisig=True), ] wl = Wallet.create('multisig_test_litecoin_send', key_list, sigs_required=2, network=network, - db_uri=self.DATABASE_URI) + witness_type='legacy', db_uri=self.DATABASE_URI) wl.get_keys(number_of_keys=2) wl.utxo_add(wl.get_key().address, 200000, '46fcfdbdc3573756916a0ced8bbc5418063abccd2c272f17bf266f77549b62d5', 0, 1) @@ -1477,7 +1489,7 @@ def test_wallet_multisig_replace_sig_bug(self): '2631ab1a4745f657f7216c636fb8ac708a3f6b63a6cd5cf773bfc9a3ebe6e1ba', '97a66126f42fd3241cf256846e58cd7049d4d395f84b1811f73a3f5d33ff833e', ] - key_list = [HDKey(pk, network=network) for pk in pk_hex_list] + key_list = [HDKey(pk, witness_type='legacy', network=network) for pk in pk_hex_list] key_list_cosigners = [k.public_master(multisig=True) for k in key_list if k is not key_list[0]] key_list_wallet = [key_list[0]] + key_list_cosigners w = wallet_create_or_open(wallet_name, keys=key_list_wallet, sigs_required=sigs_req, witness_type=witness_type, @@ -1736,7 +1748,7 @@ def test_wallet_balance_update_multi_network(self): k = "tpubDCutwJADa3iSbFtB2LgnaaqJgZ8FPXRRzcrMq7Tms41QNnTV291rpkps9vRwyss9zgDc7hS5V1aM1by8nFip5VjpGpz1oP54peKt" \ "hJzfabX" wlt = Wallet.create("test_wallet_balance_update_multi_network", network='bitcoinlib_test', - db_uri=self.DATABASE_URI) + witness_type='legacy', db_uri=self.DATABASE_URI) wlt.new_key() wlt.new_account(network='testnet') wlt.import_key(k) @@ -1913,7 +1925,7 @@ def test_wallet_transaction_method(self): def test_wallet_transaction_from_txid(self): w = Wallet.create('testwltbcl', keys='dda84e87df25f32d73a7f7d008ed2b89fc00d9d07fde588d1b8af0af297023de', - network='bitcoinlib_test', db_uri=self.DATABASE_URI) + witness_type='legacy', network='bitcoinlib_test', db_uri=self.DATABASE_URI) w.utxos_update() wts = w.transactions() txid = wts[0].txid From 33c2ed7eb2ca18048f472e78acf93950b270d893 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 13 Oct 2023 22:07:35 +0200 Subject: [PATCH 014/207] [ADD] Method to get purpose, encoding, key_path from witness_type --- bitcoinlib/keys.py | 18 +++------- bitcoinlib/main.py | 17 ++++++++++ bitcoinlib/wallets.py | 78 +++++++++++++++++++++++++------------------ 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index b73dbcc1..aa493418 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -355,7 +355,7 @@ def addr_convert(addr, prefix, encoding=None, to_encoding=None): return pubkeyhash_to_addr(pkh, prefix=prefix, encoding=to_encoding) -def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=44, +def path_expand(path, path_template=None, level_offset=None, account_id=0, cosigner_id=0, purpose=84, address_index=0, change=0, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, network=DEFAULT_NETWORK): """ Create key path. Specify part of key path and path settings @@ -391,11 +391,7 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig if isinstance(path, TYPE_TEXT): path = path.split('/') if not path_template: - ks = [k for k in WALLET_KEY_STRUCTURES if - k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None] - if ks: - purpose = ks[0]['purpose'] - path_template = ks[0]['key_path'] + path_template, purpose, _ = get_key_structure_data(witness_type, multisig) if not isinstance(path, list): raise BKeyError("Please provide path as list with at least 1 item. Wallet key path format is %s" % path_template) @@ -1785,14 +1781,8 @@ def public_master(self, account_id=0, purpose=None, multisig=None, witness_type= self.multisig = multisig if witness_type: self.witness_type = witness_type - ks = [k for k in WALLET_KEY_STRUCTURES if - k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None] - if len(ks) > 1: - raise BKeyError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - "witness_type - multisig combination") - if ks and not purpose: - purpose = ks[0]['purpose'] - path_template = ks[0]['key_path'] + + path_template, purpose, _ = get_key_structure_data(self.witness_type, self.multisig, purpose) # Use last hardened key as public master root pm_depth = path_template.index([x for x in path_template if x[-1:] == "'"][-1]) + 1 diff --git a/bitcoinlib/main.py b/bitcoinlib/main.py index 17beafd3..322e0626 100644 --- a/bitcoinlib/main.py +++ b/bitcoinlib/main.py @@ -105,6 +105,23 @@ def get_encoding_from_witness(witness_type=None): raise ValueError("Unknown witness type %s" % witness_type) +def get_key_structure_data(witness_type, multisig=False, purpose=None, encoding=None): + if not witness_type: + return None, None, None + ks = [k for k in WALLET_KEY_STRUCTURES if + k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None] + if len(ks) > 1: + raise ValueError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + "witness_type - multisig combination: %s, %s" % (witness_type, multisig)) + if not ks: + raise ValueError("Please check definitions in WALLET_KEY_STRUCTURES. No options found for " + "witness_type - multisig combination: %s, %s" % (witness_type, multisig)) + purpose = ks[0]['purpose'] if not purpose else purpose + path_template = ks[0]['key_path'] + encoding = ks[0]['encoding'] if not encoding else encoding + return path_template, purpose, encoding + + def deprecated(func): """ This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 997a0e45..f8267969 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -301,7 +301,7 @@ class WalletKey(object): """ @staticmethod - def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=44, parent_id=0, + def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=84, parent_id=0, path='m', key_type=None, encoding=None, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, cosigner_id=None): """ @@ -331,7 +331,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 :type network: str :param change: Use 0 for normal key, and 1 for change key (for returned payments) :type change: int - :param purpose: BIP0044 purpose field, default is 44 + :param purpose: BIP0044 purpose field, default is 84 :type purpose: int :param parent_id: Key ID of parent, default is 0 (no parent) :type parent_id: int @@ -405,7 +405,8 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 account_id=account_id, depth=k.depth, change=change, address_index=k.child_index, wif=k.wif(witness_type=witness_type, multisig=multisig, is_private=True), address=address, parent_id=parent_id, compressed=k.compressed, is_private=k.is_private, path=path, - key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, + witness_type=witness_type) else: keyexists = session.query(DbKey).\ filter(DbKey.wallet_id == wallet_id, @@ -416,7 +417,8 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 nk = DbKey(name=name[:80], wallet_id=wallet_id, purpose=purpose, account_id=account_id, depth=k.depth, change=change, address=k.address, parent_id=parent_id, compressed=k.compressed, is_private=False, path=path, - key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id) + key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, + witness_type=witness_type) session.merge(DbNetwork(name=network)) session.add(nk) @@ -1261,16 +1263,17 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 key_path = ['m'] purpose = 0 else: - ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and - k['multisig'] == multisig and k['purpose'] is not None] - if len(ks) > 1: - raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - "witness_type - multisig combination") - if ks and not purpose: - purpose = ks[0]['purpose'] - if ks and not encoding: - encoding = ks[0]['encoding'] - key_path = ks[0]['key_path'] + # ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and + # k['multisig'] == multisig and k['purpose'] is not None] + # if len(ks) > 1: + # raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + # "witness_type - multisig combination") + # if ks and not purpose: + # purpose = ks[0]['purpose'] + # if ks and not encoding: + # encoding = ks[0]['encoding'] + # key_path = ks[0]['key_path'] + key_path, purpose, encoding = get_key_structure_data(witness_type, multisig, purpose, encoding) else: if purpose is None: purpose = 0 @@ -1568,7 +1571,6 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): if (self.main_key.depth != 1 and self.main_key.depth != 3 and self.main_key.depth != 4) or \ self.main_key.key_type != 'bip32': raise WalletError("Current main key is not a valid BIP32 public master key") - # pm = self.public_master() if not (self.network.name == self.main_key.network.name == hdkey.network.name): raise WalletError("Network of Wallet class, main account key and the imported private key must use " "the same network") @@ -1576,12 +1578,13 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): raise WalletError("This key does not correspond to current public master key") hdkey.key_type = 'bip32' - ks = [k for k in WALLET_KEY_STRUCTURES if - k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None] - if len(ks) > 1: - raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - "witness_type - multisig combination") - self.key_path = ks[0]['key_path'] + # ks = [k for k in WALLET_KEY_STRUCTURES if + # k['witness_type'] == self.witness_type and k['multisig'] == self.multisig and k['purpose'] is not None] + # if len(ks) > 1: + # raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " + # "witness_type - multisig combination") + # self.key_path = ks[0]['key_path'] + self.key_path, _, _ = get_key_structure_data(self.witness_type, self.multisig) self.main_key = WalletKey.from_key( key=hdkey, name=name, session=self._session, wallet_id=self.wallet_id, network=network, account_id=account_id, purpose=self.purpose, key_type='bip32', witness_type=self.witness_type) @@ -1599,7 +1602,7 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): self._commit() return self.main_key - def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_type=None): + def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_type=None): """ Add new single key to wallet. @@ -1611,7 +1614,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=44, key_t :type name: str :param network: Network name, method will try to extract from key if not specified. Raises warning if network could not be detected :type network: str - :param purpose: BIP definition used, default is BIP44 + :param purpose: BIP44 definition used, default is 84 (segwit) :type purpose: int :param key_type: Key type of imported key, can be single. Unrelated to wallet, bip32, bip44 or master for new or extra master key import. Default is 'single' :type key_type: str @@ -1707,7 +1710,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id, depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path, public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id, - key_type='multisig', network_name=network) + key_type='multisig', witness_type=self.witness_type, network_name=network) self._session.add(multisig_key) self._commit() for child_id in public_key_ids: @@ -1716,7 +1719,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -1746,12 +1749,15 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network= if network != self.network.name and "coin_type'" not in self.key_path: raise WalletError("Multiple networks not supported by wallet key structure") if self.multisig: + # if witness_type: + # TODO: raise error if not self.multisig_n_required: raise WalletError("Multisig_n_required not set, cannot create new key") if cosigner_id is None: if self.cosigner_id is None: raise WalletError("Missing Cosigner ID value, cannot create new key") cosigner_id = self.cosigner_id + witness_type = self.witness_type if not witness_type else witness_type address_index = 0 if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): @@ -1759,7 +1765,7 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, network= else: prevkey = self._session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network, account_id=account_id, - change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ + witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ order_by(DbKey.address_index.desc()).first() if prevkey: address_index = prevkey.address_index + 1 @@ -2090,7 +2096,7 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, network=None, recreate=False): + address_index=0, change=0, witness_type=None, network=None, recreate=False): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2141,11 +2147,17 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi level_offset_key = level_offset - self.main_key.depth key_path = self.key_path + witness_type = witness_type if witness_type else self.witness_type + purpose = self.purpose + encoding = self.encoding + # if witness_type != self.witness_type: + # purpose = 44 + # encoding = 'base58' if witness_type == 'legacy' else 'bech32' if self.multisig and cosigner_id is not None and len(self.cosigner) > cosigner_id: key_path = self.cosigner[cosigner_id].key_path fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id, - purpose=self.purpose, address_index=address_index, change=change, - witness_type=self.witness_type, network=network) + purpose=purpose, address_index=address_index, change=change, + witness_type=witness_type, network=network) if self.multisig and self.cosigner: public_keys = [] @@ -2158,7 +2170,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi public_keys.append(wk) return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index) - # Check for closest ancestor in wallet\ + # Check for closest ancestor in wallet wpath = fullpath if self.main_key.depth and fullpath and fullpath[0] != 'M': wpath = ["M"] + fullpath[self.main_key.depth + 1:] @@ -2183,6 +2195,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi nk = None parent_id = topkey.key_id ck = topkey.key() + ck.witness_type = witness_type + ck.encoding = encoding newpath = topkey.path n_items = len(str(dbkey.path).split('/')) for lvl in fullpath[n_items:]: @@ -2200,8 +2214,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi key_name = "%s %s" % (self.key_path[len(newpath.split('/'))-1], lvl) key_name = key_name.replace("'", "").replace("_", " ") nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, - change=change, purpose=self.purpose, path=newpath, parent_id=parent_id, - encoding=self.encoding, witness_type=self.witness_type, + change=change, purpose=purpose, path=newpath, parent_id=parent_id, + encoding=encoding, witness_type=witness_type, cosigner_id=cosigner_id, network=network, session=self._session) self._key_objects.update({nk.key_id: nk}) parent_id = nk.key_id From b55529e84d9b2d418eb5115af7fdae878c016728 Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 13 Oct 2023 22:39:22 +0200 Subject: [PATCH 015/207] [UPD] Update docstrings --- bitcoinlib/main.py | 17 ++++++++++++++++- bitcoinlib/wallets.py | 12 ++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/bitcoinlib/main.py b/bitcoinlib/main.py index 322e0626..d1172929 100644 --- a/bitcoinlib/main.py +++ b/bitcoinlib/main.py @@ -106,8 +106,23 @@ def get_encoding_from_witness(witness_type=None): def get_key_structure_data(witness_type, multisig=False, purpose=None, encoding=None): + """ + Get data from wallet key structure. Provide witness_type and multisig to determine key path, purpose (BIP44 + reference) and encoding. + + :param witness_type: Witness type used for transaction validation + :type witness_type: str + :param multisig: Multisig or single keys wallet, default is False: single key / 1-of-1 wallet + :type multisig: bool + :param purpose: Overrule purpose found in wallet structure. Do not use unless you known what you are doing. + :type purpose: int + :param encoding: Overrule encoding found in wallet structure. Do not use unless you known what you are doing. + :type encoding: str + + :return: (key_path, purpose, encoding) + """ if not witness_type: - return None, None, None + return None, purpose, encoding ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and k['multisig'] == multisig and k['purpose'] is not None] if len(ks) > 1: diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index f8267969..d4c46381 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1263,16 +1263,6 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 key_path = ['m'] purpose = 0 else: - # ks = [k for k in WALLET_KEY_STRUCTURES if k['witness_type'] == witness_type and - # k['multisig'] == multisig and k['purpose'] is not None] - # if len(ks) > 1: - # raise WalletError("Please check definitions in WALLET_KEY_STRUCTURES. Multiple options found for " - # "witness_type - multisig combination") - # if ks and not purpose: - # purpose = ks[0]['purpose'] - # if ks and not encoding: - # encoding = ks[0]['encoding'] - # key_path = ks[0]['key_path'] key_path, purpose, encoding = get_key_structure_data(witness_type, multisig, purpose, encoding) else: if purpose is None: @@ -2132,6 +2122,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi :type address_index: int :param change: Change key = 1 or normal = 0, normally provided to 'path' argument :type change: int + :param witness_type: Use to create key with different witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info From 16873720ef2788bfcd9a5035ed8051c3850bb574 Mon Sep 17 00:00:00 2001 From: Lennart Date: Sat, 14 Oct 2023 01:02:57 +0200 Subject: [PATCH 016/207] [REF] Add witness type to other key methods --- bitcoinlib/wallets.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index d4c46381..398dbc30 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1748,20 +1748,23 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ raise WalletError("Missing Cosigner ID value, cannot create new key") cosigner_id = self.cosigner_id witness_type = self.witness_type if not witness_type else witness_type + purpose = self.purpose + if witness_type != self.witness_type: + _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) address_index = 0 if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): req_path = [] else: prevkey = self._session.query(DbKey).\ - filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network, account_id=account_id, + filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ order_by(DbKey.address_index.desc()).first() if prevkey: address_index = prevkey.address_index + 1 req_path = [change, address_index] - return self.key_for_path(req_path, name=name, account_id=account_id, network=network, + return self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, cosigner_id=cosigner_id, address_index=address_index) def new_key_change(self, name='', account_id=None, network=None): @@ -1881,7 +1884,8 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False if not n_highest_updated: break - def _get_key(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0, as_list=False): + def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id=None, number_of_keys=1, change=0, + as_list=False): network, account_id, _ = self._get_account_defaults(network, account_id) if cosigner_id is None: cosigner_id = self.cosigner_id @@ -1889,17 +1893,19 @@ def _get_key(self, account_id=None, network=None, cosigner_id=None, number_of_ke raise WalletError("Cosigner ID (%d) can not be greater then number of cosigners for this wallet (%d)" % (cosigner_id, len(self.cosigner))) + witness_type = witness_type if witness_type else self.witness_type last_used_qr = self._session.query(DbKey.id).\ filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, - used=True, change=change, depth=self.key_depth).\ + used=True, change=change, depth=self.key_depth, witness_type=witness_type).\ order_by(DbKey.id.desc()).first() last_used_key_id = 0 if last_used_qr: last_used_key_id = last_used_qr.id - dbkey = self._session.query(DbKey).\ + dbkey = (self._session.query(DbKey). filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, - used=False, change=change, depth=self.key_depth).filter(DbKey.id > last_used_key_id).\ - order_by(DbKey.id.desc()).all() + used=False, change=change, depth=self.key_depth, witness_type=witness_type). + filter(DbKey.id > last_used_key_id). + order_by(DbKey.id.desc()).all()) key_list = [] if self.scheme == 'single' and len(dbkey): number_of_keys = len(dbkey) if number_of_keys > len(dbkey) else number_of_keys @@ -1908,14 +1914,15 @@ def _get_key(self, account_id=None, network=None, cosigner_id=None, number_of_ke dk = dbkey.pop() nk = self.key(dk.id) else: - nk = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, network=network) + nk = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, + witness_type=witness_type, network=network) key_list.append(nk) if as_list: return key_list else: return key_list[0] - def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): + def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id=None, change=0): """ Get a unused key / address or create a new one with :func:`new_key` if there are no unused keys. Returns a key from this wallet which has no transactions linked to it. @@ -1939,7 +1946,7 @@ def get_key(self, account_id=None, network=None, cosigner_id=None, change=0): :return WalletKey: """ - return self._get_key(account_id, network, cosigner_id, change=change, as_list=False) + return self._get_key(account_id, witness_type, network, cosigner_id, change=change, as_list=False) def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0): """ @@ -1963,7 +1970,7 @@ def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_ke """ if self.scheme == 'single': raise WalletError("Single wallet has only one (master)key. Use get_key() or main_key() method") - return self._get_key(account_id, network, cosigner_id, number_of_keys, change, as_list=True) + return self._get_key(account_id, self.witness_type, network, cosigner_id, number_of_keys, change, as_list=True) def get_key_change(self, account_id=None, network=None): """ @@ -2142,9 +2149,8 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi witness_type = witness_type if witness_type else self.witness_type purpose = self.purpose encoding = self.encoding - # if witness_type != self.witness_type: - # purpose = 44 - # encoding = 'base58' if witness_type == 'legacy' else 'bech32' + if witness_type != self.witness_type: + _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) if self.multisig and cosigner_id is not None and len(self.cosigner) > cosigner_id: key_path = self.cosigner[cosigner_id].key_path fullpath = path_expand(path, key_path, level_offset_key, account_id=account_id, cosigner_id=cosigner_id, @@ -2471,8 +2477,6 @@ def key(self, term): dbkey = None qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id) - if self.purpose: - qr = qr.filter_by(purpose=self.purpose) if isinstance(term, numbers.Number): dbkey = qr.filter_by(id=term).scalar() if not dbkey: From 8e0a37757430ff74495ca66f2e0d12b749463101 Mon Sep 17 00:00:00 2001 From: Lennart Date: Sat, 14 Oct 2023 14:23:41 +0200 Subject: [PATCH 017/207] [REF] Allow witness parameter in bulk key methods --- bitcoinlib/wallets.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 398dbc30..04e1c441 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1868,7 +1868,8 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False keys_to_scan = [self.key(k.id) for k in self.keys_addresses()[counter:counter+scan_gap_limit]] counter += scan_gap_limit else: - keys_to_scan = self.get_keys(account_id, network, number_of_keys=scan_gap_limit, change=chg) + keys_to_scan = self.get_keys(account_id, self.witness_type, network, + number_of_keys=scan_gap_limit, change=chg) n_highest_updated = 0 for key in keys_to_scan: if key.key_id in keys_ignore: @@ -1948,7 +1949,7 @@ def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id= """ return self._get_key(account_id, witness_type, network, cosigner_id, change=change, as_list=False) - def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_keys=1, change=0): + def get_keys(self, account_id=None, witness_type=None, network=None, cosigner_id=None, number_of_keys=1, change=0): """ Get a list of unused keys / addresses or create a new ones with :func:`new_key` if there are no unused keys. Returns a list of keys from this wallet which has no transactions linked to it. @@ -1970,9 +1971,9 @@ def get_keys(self, account_id=None, network=None, cosigner_id=None, number_of_ke """ if self.scheme == 'single': raise WalletError("Single wallet has only one (master)key. Use get_key() or main_key() method") - return self._get_key(account_id, self.witness_type, network, cosigner_id, number_of_keys, change, as_list=True) + return self._get_key(account_id, witness_type, network, cosigner_id, number_of_keys, change, as_list=True) - def get_key_change(self, account_id=None, network=None): + def get_key_change(self, account_id=None, witness_type=None, network=None): """ Get a unused change key or create a new one if there are no unused keys. Wrapper for the :func:`get_key` method @@ -1985,9 +1986,9 @@ def get_key_change(self, account_id=None, network=None): :return WalletKey: """ - return self._get_key(account_id=account_id, network=network, change=1, as_list=False) + return self._get_key(account_id, witness_type, network, change=1, as_list=False) - def get_keys_change(self, account_id=None, network=None, number_of_keys=1): + def get_keys_change(self, account_id=None, witness_type=None, network=None, number_of_keys=1): """ Get a unused change key or create a new one if there are no unused keys. Wrapper for the :func:`get_key` method @@ -2002,8 +2003,7 @@ def get_keys_change(self, account_id=None, network=None, number_of_keys=1): :return list of WalletKey: """ - return self._get_key(account_id=account_id, network=network, change=1, number_of_keys=number_of_keys, - as_list=True) + return self._get_key(account_id, witness_type, network, change=1, number_of_keys=number_of_keys, as_list=True) def new_account(self, name='', account_id=None, network=None): """ @@ -3716,9 +3716,9 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco "or lower fees") if self.scheme == 'single': - change_keys = [self.get_key(account_id=account_id, network=network, change=1)] + change_keys = [self.get_key(account_id, self.witness_type, network, change=1)] else: - change_keys = self.get_keys(account_id=account_id, network=network, change=1, + change_keys = self.get_keys(account_id, self.witness_type, network, change=1, number_of_keys=number_of_change_outputs) if number_of_change_outputs > 1: From f68947e576f4e0ab9a3062d85c88ea839f3ea371 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sun, 15 Oct 2023 10:38:50 +0200 Subject: [PATCH 018/207] [ADD] Create new wallet account with different witness type --- bitcoinlib/wallets.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 04e1c441..fa1336f7 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2005,7 +2005,7 @@ def get_keys_change(self, account_id=None, witness_type=None, network=None, numb return self._get_key(account_id, witness_type, network, change=1, number_of_keys=number_of_keys, as_list=True) - def new_account(self, name='', account_id=None, network=None): + def new_account(self, name='', account_id=None, witness_type=None, network=None): """ Create a new account with a child key for payments and 1 for change. @@ -2040,21 +2040,23 @@ def new_account(self, name='', account_id=None, network=None): raise WalletError("Can not create new account for network %s with same BIP44 cointype: %s" % (network, duplicate_cointypes)) + witness_type = witness_type if witness_type else self.witness_type # Determine account_id and name if account_id is None: account_id = 0 qr = self._session.query(DbKey). \ - filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=network). \ + filter_by(wallet_id=self.wallet_id, witness_type=witness_type, network_name=network). \ order_by(DbKey.account_id.desc()).first() if qr: account_id = qr.account_id + 1 - if self.keys(account_id=account_id, depth=self.depth_public_master, network=network): + if self.keys(account_id=account_id, depth=self.depth_public_master, witness_type=witness_type, + network=network): raise WalletError("Account with ID %d already exists for this wallet" % account_id) acckey = self.key_for_path([], level_offset=self.depth_public_master-self.key_depth, account_id=account_id, name=name, network=network) - self.key_for_path([0, 0], network=network, account_id=account_id) - self.key_for_path([1, 0], network=network, account_id=account_id) + self.key_for_path([0, 0], witness_type=witness_type, network=network, account_id=account_id) + self.key_for_path([1, 0], witness_type=witness_type, network=network, account_id=account_id) return acckey def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, address_index=None, change=0, @@ -2220,7 +2222,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi return nk def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, used=None, is_private=None, - has_balance=None, is_active=None, network=None, include_private=False, as_dict=False): + has_balance=None, is_active=None, witness_type=None, network=None, include_private=False, as_dict=False): """ Search for keys in database. Include 0 or more of account_id, name, key_id, change and depth. @@ -2262,6 +2264,8 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id).order_by(DbKey.id) if network is not None: qr = qr.filter(DbKey.network_name == network) + if witness_type is not None: + qr = qr.filter(DbKey.witness_type == witness_type) if account_id is not None: qr = qr.filter(DbKey.account_id == account_id) if self.scheme == 'bip32' and depth is None: From 61cbe59287577265e50d770b2aace85b2fd355dc Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Mon, 16 Oct 2023 22:15:00 +0200 Subject: [PATCH 019/207] [ADD] Get public master keys with different witness types --- bitcoinlib/wallets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index fa1336f7..e206c2b2 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -4100,7 +4100,7 @@ def wif(self, is_private=False, account_id=0): wiflist.append(cs.wif(is_private=is_private)) return wiflist - def public_master(self, account_id=None, name=None, as_private=False, network=None): + def public_master(self, account_id=None, name=None, as_private=False, witness_type=None, network=None): """ Return public master key(s) for this wallet. Use to import in other wallets to sign transactions or create keys. @@ -4127,9 +4127,10 @@ def public_master(self, account_id=None, name=None, as_private=False, network=No key = self.main_key return key if as_private else key.public() elif not self.cosigner: + witness_type = witness_type if witness_type else self.witness_type depth = -self.key_depth + self.depth_public_master key = self.key_for_path([], depth, name=name, account_id=account_id, network=network, - cosigner_id=self.cosigner_id) + cosigner_id=self.cosigner_id, witness_type=witness_type) return key if as_private else key.public() else: pm_list = [] From 61261047d4e550247fd2b786c5fb3ddb31ad9b4e Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Mon, 16 Oct 2023 23:00:23 +0200 Subject: [PATCH 020/207] [REF] Add unittest for mixed witness_type wallets --- tests/test_wallets.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index d5cf93a2..7227e8a1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2687,3 +2687,22 @@ def test_wallet_address_import_private_key(self): self.assertFalse(w.main_key.is_private) w.import_key(wif) self.assertTrue(w.main_key.is_private) + + +@parameterized_class(*params) +class TestWalletMixedWitnessTypes(TestWalletMixin, unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.db_remove() + + def test_wallet_mixed_witness_types_address(self): + pk = 'a9b723d90282710036e06ec5b3d0c303817e486fa3e8bc117aec839deaedb961' + wmix = Wallet.create(name='wallet_witness_type_mixed', keys=pk, db_uri=self.DATABASE_URI) + wleg = Wallet.create(name='wallet_witness_type_legacy', keys=pk, witness_type='legacy', + db_uri=self.DATABASE_URI) + wp2sh = Wallet.create(name='wallet_witness_type_p2sh', keys=pk, witness_type='p2sh-segwit', + db_uri=self.DATABASE_URI) + self.assertEqual(wmix.get_key(witness_type='legacy').address, wleg.get_key().address) + self.assertEqual(wmix.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) + self.assertEqual(wleg.get_key(witness_type='segwit').address, wmix.get_key().address) From bfa9c5d4da4e1534477428c0c43b7e474ae9df36 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Wed, 18 Oct 2023 21:52:07 +0200 Subject: [PATCH 021/207] [FIX] Return correct account key in Wallet.new_account --- bitcoinlib/wallets.py | 2 +- tests/test_wallets.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index e206c2b2..6e6ebfdf 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2054,7 +2054,7 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) raise WalletError("Account with ID %d already exists for this wallet" % account_id) acckey = self.key_for_path([], level_offset=self.depth_public_master-self.key_depth, account_id=account_id, - name=name, network=network) + name=name, witness_type=witness_type, network=network) self.key_for_path([0, 0], witness_type=witness_type, network=network, account_id=account_id) self.key_for_path([1, 0], witness_type=witness_type, network=network, account_id=account_id) return acckey diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 7227e8a1..54b1c2b8 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2706,3 +2706,4 @@ def test_wallet_mixed_witness_types_address(self): self.assertEqual(wmix.get_key(witness_type='legacy').address, wleg.get_key().address) self.assertEqual(wmix.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) self.assertEqual(wleg.get_key(witness_type='segwit').address, wmix.get_key().address) + self.assertEqual(wmix.new_account(witness_type='legacy').address, wleg.new_account().address) From 1035655151c3ac9f55112bc50f0c0d254583ca51 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Wed, 18 Oct 2023 22:27:38 +0200 Subject: [PATCH 022/207] [REF] Add witness_type to new_key_change --- bitcoinlib/wallets.py | 4 ++-- tests/test_wallets.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 6e6ebfdf..53129058 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1767,7 +1767,7 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ return self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, cosigner_id=cosigner_id, address_index=address_index) - def new_key_change(self, name='', account_id=None, network=None): + def new_key_change(self, name='', account_id=None, witness_type=None, network=None): """ Create new key to receive change for a transaction. Calls :func:`new_key` method with change=1. @@ -1781,7 +1781,7 @@ def new_key_change(self, name='', account_id=None, network=None): :return WalletKey: """ - return self.new_key(name=name, account_id=account_id, network=network, change=1) + return self.new_key(name=name, account_id=account_id, witness_type=witness_type, network=network, change=1) def scan_key(self, key): """ diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 54b1c2b8..c9b43a61 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2706,4 +2706,12 @@ def test_wallet_mixed_witness_types_address(self): self.assertEqual(wmix.get_key(witness_type='legacy').address, wleg.get_key().address) self.assertEqual(wmix.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) self.assertEqual(wleg.get_key(witness_type='segwit').address, wmix.get_key().address) - self.assertEqual(wmix.new_account(witness_type='legacy').address, wleg.new_account().address) + wmix_legkey = wmix.new_account(witness_type='legacy').address + self.assertEqual(wmix_legkey, '18nM5LxmzaEcf4rv9pK7FLiAtfmH1VgVWD') + self.assertEqual(wmix_legkey, wleg.new_account().address) + self.assertEqual(wmix.new_key(witness_type='p2sh-segwit').address, wp2sh.new_key().address) + self.assertEqual(wmix.new_key_change(witness_type='p2sh-segwit').address, wp2sh.new_key_change().address) + self.assertEqual(wleg.get_key_change(witness_type='segwit').address, + wp2sh.get_key_change(witness_type='segwit').address) + self.assertEqual(wleg.new_key_change(witness_type='p2sh-segwit', account_id=111).address, + wp2sh.new_key_change(account_id=111).address) From 8d1434e64c8478460d1dd7cfc746552cd8334ab7 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Fri, 20 Oct 2023 20:36:16 +0200 Subject: [PATCH 023/207] [ADD] Unittests for mixed witness_type wallets --- tests/test_wallets.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c9b43a61..ad10b258 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2698,20 +2698,33 @@ def setUpClass(cls): def test_wallet_mixed_witness_types_address(self): pk = 'a9b723d90282710036e06ec5b3d0c303817e486fa3e8bc117aec839deaedb961' - wmix = Wallet.create(name='wallet_witness_type_mixed', keys=pk, db_uri=self.DATABASE_URI) + wseg = Wallet.create(name='wallet_witness_type_segwit', keys=pk, db_uri=self.DATABASE_URI) wleg = Wallet.create(name='wallet_witness_type_legacy', keys=pk, witness_type='legacy', db_uri=self.DATABASE_URI) wp2sh = Wallet.create(name='wallet_witness_type_p2sh', keys=pk, witness_type='p2sh-segwit', db_uri=self.DATABASE_URI) - self.assertEqual(wmix.get_key(witness_type='legacy').address, wleg.get_key().address) - self.assertEqual(wmix.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) - self.assertEqual(wleg.get_key(witness_type='segwit').address, wmix.get_key().address) - wmix_legkey = wmix.new_account(witness_type='legacy').address + self.assertEqual(wseg.get_key(witness_type='legacy').address, wleg.get_key().address) + self.assertEqual(wseg.get_key(witness_type='p2sh-segwit').address, wp2sh.get_key().address) + self.assertEqual(wleg.get_key(witness_type='segwit').address, wseg.get_key().address) + wmix_legkey = wseg.new_account(witness_type='legacy').address self.assertEqual(wmix_legkey, '18nM5LxmzaEcf4rv9pK7FLiAtfmH1VgVWD') self.assertEqual(wmix_legkey, wleg.new_account().address) - self.assertEqual(wmix.new_key(witness_type='p2sh-segwit').address, wp2sh.new_key().address) - self.assertEqual(wmix.new_key_change(witness_type='p2sh-segwit').address, wp2sh.new_key_change().address) + self.assertEqual(wseg.new_key(witness_type='p2sh-segwit').address, wp2sh.new_key().address) + self.assertEqual(wseg.new_key_change(witness_type='p2sh-segwit').address, wp2sh.new_key_change().address) self.assertEqual(wleg.get_key_change(witness_type='segwit').address, wp2sh.get_key_change(witness_type='segwit').address) self.assertEqual(wleg.new_key_change(witness_type='p2sh-segwit', account_id=111).address, wp2sh.new_key_change(account_id=111).address) + + def test_wallet_mixed_witness_types_masterkeys(self): + pk = '5f5b1f7d8c023c4bf5deff1eefe7ee27c126879da7e65487cf9ff64bdc3a1518' + wseg = Wallet.create(name='wallet_witness_types_masterkey_segwit', keys=pk, db_uri=self.DATABASE_URI) + wleg = Wallet.create(name='wallet_witness_types_masterkey_legacy', keys=pk, witness_type='legacy', + db_uri=self.DATABASE_URI) + wp2sh = Wallet.create(name='wallet_witness_types_masterkey_p2sh', keys=pk, witness_type='p2sh-segwit', + db_uri=self.DATABASE_URI) + self.assertEqual(wseg.public_master().wif, wleg.public_master(witness_type='segwit').wif) + self.assertEqual(wseg.public_master(witness_type='p2sh-segwit').wif, + wleg.public_master(witness_type='p2sh-segwit').wif) + self.assertEqual(wp2sh.public_master().wif, wleg.public_master(witness_type='p2sh-segwit').wif) + self.assertEqual(wleg.public_master().wif, wp2sh.public_master(witness_type='legacy').wif) From d2328a6e98ea4c67ed801ac3afd2a8b91a6b7bd8 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 30 Oct 2023 20:45:35 +0100 Subject: [PATCH 024/207] [REF] Define witnesstype in WalletKey class --- bitcoinlib/wallets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 53129058..3484a897 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -480,6 +480,7 @@ def __init__(self, key_id, session, hdkey_object=None): self.encoding = wk.encoding self.cosigner_id = wk.cosigner_id self.used = wk.used + self.witness_type = wk.witness_type else: raise WalletError("Key with id %s not found" % key_id) From 5950e5e807258ce2022b0f464874a1a1718d2d5f Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 30 Oct 2023 21:11:09 +0100 Subject: [PATCH 025/207] [ADD] Mixed witness_type send unittest --- tests/test_wallets.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index ad10b258..afa3a1a7 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2728,3 +2728,20 @@ def test_wallet_mixed_witness_types_masterkeys(self): wleg.public_master(witness_type='p2sh-segwit').wif) self.assertEqual(wp2sh.public_master().wif, wleg.public_master(witness_type='p2sh-segwit').wif) self.assertEqual(wleg.public_master().wif, wp2sh.public_master(witness_type='legacy').wif) + + def test_wallet_mixed_witness_types_send(self): + w = Wallet.create(name='wallet_mixed_bcltest', network='bitcoinlib_test', db_uri=self.DATABASE_URI) + seg_key = w.get_key() + leg_key = w.new_key(witness_type='legacy') + p2sh_key = w.new_key(witness_type='p2sh-segwit') + self.assertEqual(seg_key.witness_type, 'segwit') + self.assertEqual(leg_key.witness_type, 'legacy') + self.assertEqual(p2sh_key.witness_type, 'p2sh-segwit') + w.utxos_update() + t = w.sweep('blt1qgk3zp30pnpggylp84enh0zpfpkdu63kv4xak4p', fee=30000) + self.assertEqual(len(t.inputs), len(w.addresslist()) * 2) + self.assertEqual(t.outputs[0].value, int(w.balance() - 30000)) + self.assertEqual(t.verified) + t.send() + self.assertIsNone(t.error) + From abda2a43ff89c9f651306a42b0cdc2da1019ce5b Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 30 Oct 2023 21:36:05 +0100 Subject: [PATCH 026/207] [FIX] Typo in unittest --- tests/test_wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index afa3a1a7..9497925e 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2741,7 +2741,7 @@ def test_wallet_mixed_witness_types_send(self): t = w.sweep('blt1qgk3zp30pnpggylp84enh0zpfpkdu63kv4xak4p', fee=30000) self.assertEqual(len(t.inputs), len(w.addresslist()) * 2) self.assertEqual(t.outputs[0].value, int(w.balance() - 30000)) - self.assertEqual(t.verified) + self.assertTrue(t.verified) t.send() self.assertIsNone(t.error) From 299667689b3b1f6a7de393657d8cf64449a71081 Mon Sep 17 00:00:00 2001 From: Lennart Date: Tue, 31 Oct 2023 09:07:37 +0100 Subject: [PATCH 027/207] [REF] Raise warning when trying to use mixed witness type in watch only wallets --- bitcoinlib/wallets.py | 8 +++++--- tests/test_wallets.py | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 3484a897..a9f55261 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2028,7 +2028,7 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) raise WalletError("A master private key of depth 0 is needed to create new accounts (depth: %d)" % self.main_key.depth) if "account'" not in self.key_path: - raise WalletError("Accounts are not supported for this wallet. Account not found in key path %s" % + raise WalletError("Accounts are not supported for this wallet. Account level not found in key path %s" % self.key_path) if network is None: network = self.network.name @@ -2147,9 +2147,11 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi level_offset_key = level_offset if level_offset and self.main_key and level_offset > 0: level_offset_key = level_offset - self.main_key.depth - - key_path = self.key_path witness_type = witness_type if witness_type else self.witness_type + if ((not self.main_key or not self.main_key.is_private or self.main_key.depth != 0) and + self.witness_type != witness_type): + raise WalletError("This wallet has no private key, cannot use multiple witness types") + key_path = self.key_path purpose = self.purpose encoding = self.encoding if witness_type != self.witness_type: diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 9497925e..005a20ca 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2745,3 +2745,11 @@ def test_wallet_mixed_witness_types_send(self): t.send() self.assertIsNone(t.error) + def test_wallet_mixed_witness_no_private_key(self): + pub_master = ('zpub6qwhKTArtsgtCpVweSyJdVqmXTkmH3HXE2sc7RdhF5drnmcW2HXuFBqRPzVxhQkdaER3bSZeJbAbYxNGeShwUu' + 'T49JfJqZLHNAsEUHD76AR') + address = 'bc1qgf8fzfj65lcr5vae0sh77akurh4zc9s9m4uspm' + w = Wallet.create('wallet_mix_no_private', keys=pub_master, db_uri=self.DATABASE_URI) + self.assertEqual(address, w.get_key().address) + self.assertRaisesRegexp(WalletError, "This wallet has no private key, cannot use multiple witness types", + w.get_key, witness_type='legacy') From 20594e8bc0ba06a8cfda58d6f006070cb7f9b945 Mon Sep 17 00:00:00 2001 From: Lennart Date: Tue, 31 Oct 2023 13:41:32 +0100 Subject: [PATCH 028/207] [ADD] Multiplication, addition, substraction of private keys --- bitcoinlib/keys.py | 48 ++++++++++++++++++++++++++++++++++++++++--- bitcoinlib/scripts.py | 2 +- tests/test_keys.py | 47 ++++++++++++++++++++++++++++-------------- tests/test_script.py | 2 +- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 79afd289..a2104d79 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -930,10 +930,52 @@ def __bytes__(self): return self.public_byte def __add__(self, other): - return self.public_byte + other + """ + Scalar addition over secp256k1 order of 2 keys secrets. Returns a new private key with network and compressed + attributes from first key. - def __radd__(self, other): - return other + self.public_byte + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert self.is_private + assert isinstance(other, Key) + assert other.is_private + return Key((self.secret + other.secret) % secp256k1_n, self.network, self.compressed) + + def __sub__(self, other): + """ + Scalar substraction over secp256k1 order of 2 keys secrets. Returns a new private key with network and + compressed attributes from first key. + + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert self.is_private + assert isinstance(other, Key) + assert other.is_private + return Key((self.secret - other.secret) % secp256k1_n, self.network, self.compressed) + + def __mul__(self, other): + """ + Scalar multiplication over secp256k1 order of 2 keys secrets. Returns a new private key with network and + compressed attributes from first key. + + :param other: Private Key class + :type other: Key + + :return: Key + """ + assert isinstance(other, Key) + assert self.secret + assert other.is_private + return Key((self.secret * other.secret) % secp256k1_n, self.network, self.compressed) + + def __rmul__(self, other): + return self * other def __len__(self): return len(self.public_byte) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 3e442f4b..05b2c959 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -557,7 +557,7 @@ def serialize(self): if isinstance(cmd, int): raw += bytes([cmd]) else: - raw += data_pack(cmd) + raw += data_pack(bytes(cmd)) self._raw = raw return raw diff --git a/tests/test_keys.py b/tests/test_keys.py index e535145e..839055f4 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -32,21 +32,38 @@ class TestKeyClasses(unittest.TestCase): def test_keys_classes_dunder_methods(self): - pk = 'xprv9s21ZrQH143K4EDmQNMBqXwUTcrRoUctKkTegGsaBcMLnR1fJkMjVSRwVswjHzJspfWCUwzge1F521cY4wfWD54tzXVUqeo' \ - 'TFkZo17HiK2y' - k = HDKey(pk) - self.assertEqual(str(k), '03dc86716b2be27a0575558bac73279290ac22c3ea0240e42a2152d584f2b4006b') - self.assertEqual(len(k), 33) - self.assertEqual(int(k), 95796105828208927954168018443072630832764875640480247096632116413925408206516) - k2 = HDKey(pk) - self.assertTrue(k == k2) - pubk2 = HDKey(k.wif_public()) - self.assertEqual(str(pubk2), '03dc86716b2be27a0575558bac73279290ac22c3ea0240e42a2152d584f2b4006b') - self.assertTrue(k.public() == pubk2) - self.assertEqual(k + k2, b'\x03\xdc\x86qk+\xe2z\x05uU\x8b\xacs\'\x92\x90\xac"\xc3\xea\x02@\xe4*!R\xd5' - b'\x84\xf2\xb4\x00k\x03\xdc\x86qk+\xe2z\x05uU\x8b\xacs\'\x92\x90\xac"' - b'\xc3\xea\x02@\xe4*!R\xd5\x84\xf2\xb4\x00k') - self.assertEqual(k + k2, k.public_byte + k2.public_byte) + secret_a = 91016841482436413813855602003356453732719866824300837492458390942862039054048 + secret_b = 78671675202523181504169507283123166972338313435344626818080535590471773062636 + secret_a_add_b = 53896427447643399894454124277791712852220615980570559927933763391815650622347 + secret_a_min_b = 12345166279913232309686094720233286760381553388956210674377855352390265991412 + ka = HDKey(secret_a) + ka2 = HDKey(secret_a) + kb = HDKey(secret_b) + self.assertEqual(str(ka), '02dff8866c7dc58055d9823dbc0ef098be76d8a1c87e545a13559460669b56a6a6') + self.assertEqual(len(ka), 33) + self.assertTrue(ka == ka2) + pub_ka = HDKey(ka.wif_public()) + self.assertEqual(str(pub_ka), '02dff8866c7dc58055d9823dbc0ef098be76d8a1c87e545a13559460669b56a6a6') + self.assertTrue(ka.public() == pub_ka) + self.assertEqual((ka + kb).secret, secret_a_add_b) + self.assertEqual((kb + ka).secret, secret_a_add_b) + self.assertEqual((ka - kb).secret, secret_a_min_b) + + def test_keys_classes_dunder_methods_mul(self): + secret_a = 101842203467542661703461476767681059717614296435193763347876672834253776929083 + secret_b = 48056918761728599432510813046582785545807011954742048381717688544631745412510 + secret_a_mul_b = 88863767166841201737805106153187292662619702602208852020796235484522800819015 + ka = HDKey(secret_a) + kb = HDKey(secret_b) + self.assertEqual((ka * kb).secret, secret_a_mul_b) + self.assertEqual((kb * ka).secret, secret_a_mul_b) + + def test_keys_proof_distributivity_of_scalar_operations(self): + # Proof: (a - b) * c == a * c - b * c over SECP256k1 + ka = HDKey() + kb = HDKey() + kc = HDKey() + self.assertTrue(((ka - kb) * kc) == ((ka * kc) - (kb * kc))) def test_dict_and_json_outputs(self): k = HDKey() diff --git a/tests/test_script.py b/tests/test_script.py index f7a33522..83f635d2 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -830,7 +830,7 @@ def test_script_create_redeemscript(self): '00bd217870a8b4f1f09f3a8e8353ae' self.assertEqual(expected_redeemscript, redeemscript.serialize().hex()) - redeemscript3 = b'\x52' + b''.join([varstr(k) for k in keylist]) + b'\x53\xae' + redeemscript3 = b'\x52' + b''.join([varstr(k.public_byte) for k in keylist]) + b'\x53\xae' self.assertEqual(redeemscript3, redeemscript.serialize()) def test_script_create_redeemscript_2(self): From e37fa6fc105f5291597ddde05a8b60f372e75db3 Mon Sep 17 00:00:00 2001 From: Lennart Date: Tue, 31 Oct 2023 15:01:23 +0100 Subject: [PATCH 029/207] [ADD] Inverse Key method --- bitcoinlib/config/secp256k1.py | 8 ++++---- bitcoinlib/keys.py | 4 ++++ tests/test_keys.py | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/config/secp256k1.py b/bitcoinlib/config/secp256k1.py index b97022f1..39e27168 100644 --- a/bitcoinlib/config/secp256k1.py +++ b/bitcoinlib/config/secp256k1.py @@ -23,12 +23,12 @@ # Parameters secp256k1 # from http://www.secg.org/sec2-v2.pdf, par 2.4.1 -secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +secp256k1_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F # field size +secp256k1_n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 # order secp256k1_b = 7 secp256k1_a = 0 -secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 -secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 +secp256k1_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 # generator point x +secp256k1_Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 # generator point y # secp256k1_curve = ecdsa.ellipticcurve.CurveFp(secp256k1_p, secp256k1_a, secp256k1_b) # secp256k1_generator = ecdsa.ellipticcurve.Point(secp256k1_curve, secp256k1_Gx, secp256k1_Gy, secp256k1_n) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index a2104d79..2eb24c78 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -977,6 +977,10 @@ def __mul__(self, other): def __rmul__(self, other): return self * other + def __neg__(self): + assert self.secret + return HDKey(secp256k1_n - self.secret) + def __len__(self): return len(self.public_byte) diff --git a/tests/test_keys.py b/tests/test_keys.py index 839055f4..3451fbd6 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -65,6 +65,15 @@ def test_keys_proof_distributivity_of_scalar_operations(self): kc = HDKey() self.assertTrue(((ka - kb) * kc) == ((ka * kc) - (kb * kc))) + def test_keys_inverse(self): + secret = 95695802915573022935630358993164660366922511389187789518108651759801046161623 + inv_x = 18153291153288219155018628681705413538294494009875615719062204619491226452658 + inv_y = 67935514921393906349711087930011707333238709725906400058836382320969451605430 + k = Key(secret) + k_inv = -k + self.assertEqual(k_inv.x, inv_x) + self.assertEqual(k_inv.y, inv_y) + def test_dict_and_json_outputs(self): k = HDKey() k.address(script_type='p2wsh', encoding='bech32') From 28a5e5cf4cc059de607f4557f03b818549bc3406 Mon Sep 17 00:00:00 2001 From: Lennart Date: Sat, 4 Nov 2023 12:55:11 +0100 Subject: [PATCH 030/207] [ADD] Get used witness types in wallet and use in scan method --- bitcoinlib/wallets.py | 29 +++++++++++++++++++++++++++-- tests/test_wallets.py | 11 +++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index a9f55261..5201e27f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1869,8 +1869,11 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False keys_to_scan = [self.key(k.id) for k in self.keys_addresses()[counter:counter+scan_gap_limit]] counter += scan_gap_limit else: - keys_to_scan = self.get_keys(account_id, self.witness_type, network, - number_of_keys=scan_gap_limit, change=chg) + keys_to_scan = [] + for witness_type in self.witness_types(network=network): + keys_to_scan += self.get_keys(account_id, witness_type, network, + number_of_keys=scan_gap_limit, change=chg) + n_highest_updated = 0 for key in keys_to_scan: if key.key_id in keys_ignore: @@ -2547,6 +2550,28 @@ def accounts(self, network=DEFAULT_NETWORK): accounts = [self.default_account_id] return list(dict.fromkeys(accounts)) + def witness_types(self, account_id=None, network=None): + """ + Get witness types in use by this wallet. For example 'legacy', 'segwit', 'p2sh-segwit' + + :param account_id: Account ID. Leave empty for default account + :type account_id: int + :param network: Network name filter. Default filter is DEFAULT_NETWORK + :type network: str + + :return list of str: + """ + + # network, account_id, _ = self._get_account_defaults(network, account_id) + qr = self._session.query(DbKey.witness_type).filter_by(wallet_id=self.wallet_id) + if network is not None: + qr = qr.filter(DbKey.network_name == network) + if account_id is not None: + qr = qr.filter(DbKey.account_id == account_id) + qr = qr.group_by(DbKey.witness_type).all() + return [x[0] for x in qr] + + def networks(self, as_dict=False): """ Get list of networks used by this wallet diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 005a20ca..40945209 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2753,3 +2753,14 @@ def test_wallet_mixed_witness_no_private_key(self): self.assertEqual(address, w.get_key().address) self.assertRaisesRegexp(WalletError, "This wallet has no private key, cannot use multiple witness types", w.get_key, witness_type='legacy') + + def test_wallet_mixed_witness_type_create(self): + w = Wallet.create('test_wallet_mixed_witness_type_create', network='testnet', db_uri=self.DATABASE_URI) + w.get_key(witness_type='legacy') + w.new_account('test-account', 101, witness_type='p2sh-segwit') + w.get_key(account_id=101) + kltc = w.get_key(network='litecoin') + self.assertEqual(kltc.network, 'litecoin') + self.assertListEqual(sorted(w.witness_types()), ['legacy', 'p2sh-segwit', 'segwit']) + self.assertListEqual(sorted(w.witness_types(account_id=101)), ['p2sh-segwit', 'segwit']) + self.assertListEqual(w.witness_types(network='litecoin'), ['segwit']) From 1566bd23952683b601a0b97e219c6e537453cadb Mon Sep 17 00:00:00 2001 From: Lennart Date: Wed, 8 Nov 2023 22:22:41 +0100 Subject: [PATCH 031/207] [FIX] Sending from wallets with multiple witness types --- bitcoinlib/wallets.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 5201e27f..abaf9992 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3613,12 +3613,13 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco amount_total_input += utxo.value inp_keys, key = self._objects_by_key_id(utxo.key_id) multisig = False if isinstance(inp_keys, list) and len(inp_keys) < 2 else True - unlock_script_type = get_unlocking_script_type(utxo.script_type, self.witness_type, multisig=multisig) + unlock_script_type = get_unlocking_script_type(utxo.script_type, utxo.key.witness_type, + multisig=multisig) transaction.add_input(utxo.transaction.txid, utxo.output_n, keys=inp_keys, script_type=unlock_script_type, sigs_required=self.multisig_n_required, sort=self.sort_keys, compressed=key.compressed, value=utxo.value, address=utxo.key.address, sequence=sequence, - key_path=utxo.key.path, witness_type=self.witness_type) + key_path=utxo.key.path, witness_type=utxo.key.witness_type) # FIXME: Missing locktime_cltv=locktime_cltv, locktime_csv=locktime_csv (?) else: for inp in input_arr: @@ -3639,6 +3640,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco sequence = inp.sequence locktime_cltv = inp.locktime_cltv locktime_csv = inp.locktime_csv + witness_type = inp.witness_type # elif isinstance(inp, DbTransactionOutput): # prev_txid = inp.transaction.txid # output_n = inp.output_n @@ -3657,6 +3659,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco signatures = None if len(inp) <= 4 else inp[4] unlocking_script = b'' if len(inp) <= 5 else inp[5] address = '' if len(inp) <= 6 else inp[6] + witness_type = self.witness_type # Get key_ids, value from Db if not specified if not (key_id and value and unlocking_script_type): if not isinstance(output_n, TYPE_INT): @@ -3670,7 +3673,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco value = inp_utxo.value address = inp_utxo.key.address unlocking_script_type = get_unlocking_script_type(inp_utxo.script_type, multisig=self.multisig) - # witness_type = inp_utxo.witness_type + witness_type = inp_utxo.key.witness_type else: _logger.info("UTXO %s not found in this wallet. Please update UTXO's if this is not an " "offline wallet" % to_hexstring(prev_txid)) @@ -3691,7 +3694,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco unlocking_script=unlocking_script, address=address, unlocking_script_unsigned=unlocking_script_unsigned, sequence=sequence, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, - witness_type=self.witness_type, key_path=key.path) + witness_type=witness_type, key_path=key.path) # Calculate fees transaction.fee = fee fee_per_output = None From fe14fda40de9dbc510468d6a076cdce99b3b040e Mon Sep 17 00:00:00 2001 From: Lennart Date: Thu, 9 Nov 2023 21:36:34 +0100 Subject: [PATCH 032/207] [REF] Assume bitcoin network when using passphrase --- bitcoinlib/wallets.py | 2 +- tests/test_wallets.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index abaf9992..ced04dfd 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1229,7 +1229,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 # If key consists of several words assume it is a passphrase and convert it to a HDKey object if isinstance(key, str) and len(key.split(" ")) > 1: if not network: - raise WalletError("Please specify network when using passphrase to create a key") + network = DEFAULT_NETWORK key = HDKey.from_seed(Mnemonic().to_seed(key, password), network=network, witness_type=witness_type) else: try: diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 40945209..c666d4c3 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -276,10 +276,6 @@ def test_wallet_create_errors(self): "Network from key \(dash\) is different then specified network \(bitcoin\)", Wallet.create, 'test_wallet_create_errors_multisig4', keys=[HDKey(), HDKey(network='dash')], db_uri=self.DATABASE_URI) - passphrase = 'usual olympic ride small mix follow trend baby stereo sweet lucky lend' - self.assertRaisesRegexp(WalletError, "Please specify network when using passphrase to create a key", - Wallet.create, 'test_wallet_create_errors3', keys=passphrase, - db_uri=self.DATABASE_URI) self.assertRaisesRegexp(WalletError, "Invalid key or address: zwqrC7h9pRj7SBhLRDG4FnkNBRQgene3y3", Wallet.create, 'test_wallet_create_errors4', keys='zwqrC7h9pRj7SBhLRDG4FnkNBRQgene3y3', db_uri=self.DATABASE_URI) From 7bc7d9448512f49d78e0b46ca87eb3556282f60e Mon Sep 17 00:00:00 2001 From: Lennart Date: Thu, 9 Nov 2023 23:16:43 +0100 Subject: [PATCH 033/207] [ADD] Use multiple witnesses in multisig wallet --- bitcoinlib/wallets.py | 17 ++++++++++------- tests/test_wallets.py | 6 ++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index ced04dfd..9d8358d4 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1672,7 +1672,8 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t return w.import_master_key(hdkey) raise WalletError("Unknown key: Can only import a private key for a known public key in multisig wallets") - def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, network, address_index): + def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, network, address_index, + witness_type): if self.sort_keys: public_keys.sort(key=lambda pubk: pubk.key_public) public_key_list = [pubk.key_public for pubk in public_keys] @@ -1685,9 +1686,9 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, redeemscript = Script(script_types=['multisig'], keys=public_key_list, sigs_required=self.multisig_n_required).serialize() script_type = 'p2sh' - if self.witness_type == 'p2sh-segwit': + if witness_type == 'p2sh-segwit': script_type = 'p2sh_p2wsh' - address = Address(redeemscript, encoding=self.encoding, script_type=script_type, network=network) + address = Address(redeemscript, script_type=script_type, network=network, witness_type=witness_type) already_found_key = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id, address=address.address).first() if already_found_key: @@ -1701,7 +1702,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id, depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path, public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id, - key_type='multisig', witness_type=self.witness_type, network_name=network) + key_type='multisig', witness_type=witness_type, network_name=network) self._session.add(multisig_key) self._commit() for child_id in public_key_ids: @@ -2152,7 +2153,7 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi level_offset_key = level_offset - self.main_key.depth witness_type = witness_type if witness_type else self.witness_type if ((not self.main_key or not self.main_key.is_private or self.main_key.depth != 0) and - self.witness_type != witness_type): + self.witness_type != witness_type) and not self.multisig: raise WalletError("This wallet has no private key, cannot use multiple witness types") key_path = self.key_path purpose = self.purpose @@ -2172,9 +2173,11 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi wk = wlt.main_key else: wk = wlt.key_for_path(path, level_offset=level_offset, account_id=account_id, name=name, - cosigner_id=cosigner_id, network=network, recreate=recreate) + cosigner_id=cosigner_id, network=network, recreate=recreate, + witness_type=witness_type) public_keys.append(wk) - return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index) + return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index, + witness_type) # Check for closest ancestor in wallet wpath = fullpath diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c666d4c3..bbb1b519 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2570,8 +2570,10 @@ def test_wallet_segwit_multiple_networks_accounts(self): self.assertListEqual(sorted([nw.name for nw in wallet.networks()]), networks_expected) self.assertListEqual([k.path for k in wallet.keys_accounts(network='litecoin')], ["m/48'/2'/0'/2'", "m/48'/2'/1'/2'"]) - self.assertEqual(wallet.keys(network='litecoin')[0].address, "MQNA8FYrN2fvD7SSYny3Ccvpapvsu9cVJH") - self.assertEqual(wallet.keys(network='bitcoin')[0].address, "3L6XFzC6RPeXSFpZS8v4S86v4gsNmKFnFT") + self.assertEqual(wallet.keys(network='litecoin')[0].address, + "ltc1qcu83hcdwy46dv85vmwnnfgzeu8d9arfy7kfyct52dxqcyvq2q6ds5kq2ah") + self.assertEqual(wallet.keys(network='bitcoin')[0].address, + "bc1qd0f952amxkmqc9e60u4g8w4r5a3cx22lt5vqeeyljllacxq8ezusclkwa0") @parameterized_class(*params) From 0749ed45491db713caa86dffcbc852a4b12c8bc1 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 13 Nov 2023 12:20:28 +0100 Subject: [PATCH 034/207] [ADD] sha256 method for convenience --- bitcoinlib/encoding.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index 7196fe81..a34b5cac 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -884,6 +884,23 @@ def double_sha256(string, as_hex=False): return hashlib.sha256(hashlib.sha256(string).digest()).hexdigest() +def sha256(string, as_hex=False): + """ + Get SHA256 hash of string + + :param string: String to be hashed + :type string: bytes + :param as_hex: Return value as hexadecimal string. Default is False + :type as_hex: bool + + :return bytes, str: + """ + if not as_hex: + return hashlib.sha256(string).digest() + else: + return hashlib.sha256(string).hexdigest() + + def ripemd160(string): try: return RIPEMD160.new(string).digest() From da752536ab390f4561d7e8949044a10fd6fbf155 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sun, 19 Nov 2023 20:19:02 +0100 Subject: [PATCH 035/207] [ADD] Inverse method, allow to inverse public keys --- bitcoinlib/keys.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 2eb24c78..cc17b70a 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -978,8 +978,7 @@ def __rmul__(self, other): return self * other def __neg__(self): - assert self.secret - return HDKey(secp256k1_n - self.secret) + return self.inverse() def __len__(self): return len(self.public_byte) @@ -1004,6 +1003,13 @@ def __int__(self): else: return None + def inverse(self): + if self.is_private: + return HDKey(secp256k1_n - self.secret) + else: + # Inverse y in init: self._y = secp256k1_p - self._y + return Key(('02' if self._y % 2 else '03') + self.x_hex) + @property def x(self): if not self._x and self.x_hex: From 3f523a5b6358c2fb2bc569b02f0e04145b1dd351 Mon Sep 17 00:00:00 2001 From: Lennart Date: Sun, 19 Nov 2023 21:31:15 +0100 Subject: [PATCH 036/207] [ADD] Unittest to test inverse method --- tests/test_keys.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_keys.py b/tests/test_keys.py index 3451fbd6..61e98dc2 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -99,6 +99,13 @@ def test_path_expand(self): self.assertRaisesRegexp(BKeyError, "Please provide path as list with at least 1 item", path_expand, 5) + def test_key_inverse(self): + k = HDKey() + pub_k = k.public() + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k.inverse().address()) + self.assertEqual((-k).address(), k.inverse().address()) + class TestGetKeyFormat(unittest.TestCase): From d69a2b497580bd469250dcaa39d21ee8a5d826d3 Mon Sep 17 00:00:00 2001 From: Lennart Date: Tue, 21 Nov 2023 12:51:54 +0100 Subject: [PATCH 037/207] [ADD] Allow to create key using point (x,y) on curve --- bitcoinlib/keys.py | 69 +++++++++++++++++++++++++++++----------------- tests/test_keys.py | 18 ++++++++---- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index cc17b70a..d3c37976 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -150,6 +150,9 @@ def get_key_format(key, is_private=None): elif isinstance(key, bytes) and len(key) == 32: key_format = 'bin' is_private = True + elif isinstance(key, tuple): + key_format = 'point' + is_private = False elif len(key) == 130 and key[:2] == '04' and not is_private: key_format = 'public_uncompressed' is_private = False @@ -737,7 +740,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', 12127227708610754620337553985245292396444216111803695028419544944213442390363 :param import_key: If specified import given private or public key. If not specified a new private key is generated. - :type import_key: str, int, bytes + :type import_key: str, int, bytes, tuple :param network: Bitcoin, testnet, litecoin or other network :type network: str, Network :param compressed: Is key compressed or not, default is True @@ -807,32 +810,46 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', if not self.is_private: self.secret = None - pub_key = to_hexstring(import_key) - if len(pub_key) == 130: - self._public_uncompressed_hex = pub_key - self.x_hex = pub_key[2:66] - self.y_hex = pub_key[66:130] - self._y = int(self.y_hex, 16) - self.compressed = False - if self._y % 2: - prefix = '03' - else: - prefix = '02' - self.public_hex = pub_key - self.public_compressed_hex = prefix + self.x_hex - else: - self.public_hex = pub_key - self.x_hex = pub_key[2:66] + if self.key_format == 'point': self.compressed = True - self._x = int(self.x_hex, 16) - self.public_compressed_hex = pub_key - self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) - if self._public_uncompressed_hex: - self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) - if self.compressed: - self.public_byte = self.public_compressed_byte + self._x = import_key[0] + self._y = import_key[1] + self.x_bytes = self._x.to_bytes(32, 'big') + self.y_bytes = self._y.to_bytes(32, 'big') + self.x_hex = self.x_bytes.hex() + self.y_hex = self.y_bytes.hex() + prefix = '03' if self._y % 2 else '02' + self.public_hex = prefix + self.x_hex + self.public_compressed_hex = prefix + self.x_hex + self.public_byte = (b'\3' if self._y % 2 else b'\2') + self.x_bytes else: - self.public_byte = self.public_uncompressed_byte + pub_key = to_hexstring(import_key) + if len(pub_key) == 130: + self._public_uncompressed_hex = pub_key + self.x_hex = pub_key[2:66] + self.y_hex = pub_key[66:130] + self._y = int(self.y_hex, 16) + self.compressed = False + if self._y % 2: + prefix = '03' + else: + prefix = '02' + self.public_hex = pub_key + self.public_compressed_hex = prefix + self.x_hex + else: + self.public_hex = pub_key + self.x_hex = pub_key[2:66] + self.compressed = True + self._x = int(self.x_hex, 16) + self.public_compressed_hex = pub_key + self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) + if self._public_uncompressed_hex: + self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) + if self.compressed: + self.public_byte = self.public_compressed_byte + else: + self.public_byte = self.public_uncompressed_byte + elif self.is_private and self.key_format == 'decimal': self.secret = int(import_key) self.private_hex = change_base(self.secret, 10, 16, 64) @@ -1422,7 +1439,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger :param import_key: HD Key to import in WIF format or as byte with key (32 bytes) and chain (32 bytes) - :type import_key: str, bytes, int + :type import_key: str, bytes, int, tuple :param key: Private or public key (length 32) :type key: bytes :param chain: A chain code (length 32) diff --git a/tests/test_keys.py b/tests/test_keys.py index 61e98dc2..b39fbcba 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -74,6 +74,13 @@ def test_keys_inverse(self): self.assertEqual(k_inv.x, inv_x) self.assertEqual(k_inv.y, inv_y) + def test_keys_inverse2(self): + k = HDKey() + pub_k = k.public() + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k.inverse().address()) + self.assertEqual((-k).address(), k.inverse().address()) + def test_dict_and_json_outputs(self): k = HDKey() k.address(script_type='p2wsh', encoding='bech32') @@ -99,12 +106,13 @@ def test_path_expand(self): self.assertRaisesRegexp(BKeyError, "Please provide path as list with at least 1 item", path_expand, 5) - def test_key_inverse(self): + def test_keys_create_public_point(self): k = HDKey() - pub_k = k.public() - self.assertEqual(k.address(), pub_k.address()) - self.assertEqual((-k).address(), pub_k.inverse().address()) - self.assertEqual((-k).address(), k.inverse().address()) + p = (k.x, k.y) + k2 = HDKey(p) + self.assertEqual(k, k2) + self.assertEqual(k.public(), k2) + self.assertEqual(k.address(), k2.address()) class TestGetKeyFormat(unittest.TestCase): From 507046fa6dde35fee5d2b95803d67a01c602acf6 Mon Sep 17 00:00:00 2001 From: Lennart Date: Mon, 11 Dec 2023 10:25:20 +0100 Subject: [PATCH 038/207] [UPD] Test with bitcoind regtest network and update example --- examples/bitcoind_regtest.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/bitcoind_regtest.py b/examples/bitcoind_regtest.py index 439b7fb1..e2c0df3f 100644 --- a/examples/bitcoind_regtest.py +++ b/examples/bitcoind_regtest.py @@ -4,24 +4,54 @@ # # EXAMPLES - Bitcoind regtest network example # -# © 2022 Februari - 1200 Web Development +# © 2023 December - 1200 Web Development # from bitcoinlib.services.bitcoind import * +from bitcoinlib.wallets import wallet_create_or_open +from bitcoinlib.services.services import Service from pprint import pprint bdc = BitcoindClient(base_url='http://rpcuser:pwd@localhost:18444') +walletname = 'regtesttestwallet' +walletbcl = 'regtestwallet' + print("Current blockheight is %d" % bdc.proxy.getblockcount()) + +print("Open or create a new wallet") +wallets = bdc.proxy.listwallets() +if walletname not in wallets: + wallet = bdc.proxy.createwallet(walletname) + address = bdc.proxy.getnewaddress() print("Mine 50 blocks and generate regtest coins to address %s" % address) bdc.proxy.generatetoaddress(50, address) + print("Current blockheight is %d" % bdc.proxy.getblockcount()) +print("Current balance is %d" % bdc.proxy.getbalance()) -address2 = bdc.proxy.getnewaddress() -print("Send 10 rBTC to address %s" % address2) +w = wallet_create_or_open(walletname, network='regtest') +address2 = w.get_key().address +print("\nSend 10 rBTC to address %s" % address2) bdc.proxy.settxfee(0.00002500) txid = bdc.proxy.sendtoaddress(address2, 10) print("Resulting txid: %s" % txid) tx = bdc.proxy.gettransaction(txid) pprint(tx) + + +print("\n\nConnect to bitcoind regtest with Service class and retrieve new transaction and utxo info") +srv = Service(network='regtest', providers=['bitcoind'], cache_uri='') +print("Blockcount %d" % srv.blockcount()) +b = srv.getblock(500) +pprint(b.as_dict()) +b.transactions[0].info() + +t = srv.gettransaction(txid) +t.info() + +utxos = srv.getutxos(address) +print(srv.getbalance(address)) +for utxo in utxos: + print(utxo['txid'], utxo['value'], utxo['confirmations'], utxo['block_height']) \ No newline at end of file From 46b3fe4c4ed08674d6b2dd145174c26ebf2f16f2 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sun, 31 Dec 2023 08:56:31 +0100 Subject: [PATCH 039/207] Rewrite commandline wallet part 1 --- bitcoinlib/tools/clw.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 84279944..13b32b20 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -56,7 +56,7 @@ def parse_args(): " passphrase, WIF and public account key. Can be used to create a multisig wallet") group_wallet.add_argument('--export-private', '-e', action='store_true', help="Export private key for this wallet and exit") - group_wallet.add_argument('--import-private', '-k', + group_wallet.add_argument('--import-private', '-v', help="Import private key in this wallet") group_wallet2 = parser.add_argument_group("Wallet Setup") @@ -83,13 +83,22 @@ def parse_args(): 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') group_wallet2.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, help='Witness type of wallet: lecacy (default), p2sh-segwit or segwit') - group_wallet2.add_argument('--cosigner-id', '-s', type=int, default=0, + group_wallet2.add_argument('--cosigner-id', '-o', type=int, default=0, help='Set this if wallet contains only public keys, more then one private key or if ' 'you would like to create keys for other cosigners.') group_transaction = parser.add_argument_group("Transactions") + # TODO: Add simple send address + amount + group_transaction.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, + help="Create transaction to send amount to specified address") group_transaction.add_argument('--create-transaction', '-t', metavar=('ADDRESS_1', 'AMOUNT_1'), - help="Create transaction. Specify address followed by amount in satoshis. Repeat for multiple " - "outputs", nargs='*') + help="Create transaction and send to multiple addresses. " + "Specify address followed by amount in satoshis. Repeat for multiple " + "outputs. Use -p to push to network, otherwise returns a dictionary which" + "can be used to export", + nargs='*') + # TDDO: Add --create-raw-transaction + # TODO: Add -k use specific key for inputs / scan + # TODO: Add number_of_change_outputs group_transaction.add_argument('--sweep', metavar="ADDRESS", help="Sweep wallet, transfer all funds to specified address") group_transaction.add_argument('--fee', '-f', type=int, help="Transaction fee") From 4273ba110273c2c257042f68c319c83a264958c0 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Fri, 12 Jan 2024 19:13:09 +0100 Subject: [PATCH 040/207] Rewrite commandlinewallet part 2 --- bitcoinlib/tools/clw.py | 134 +++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 13b32b20..f87a4142 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -29,17 +29,58 @@ def parse_args(): parser = argparse.ArgumentParser(description='BitcoinLib command line wallet') - parser.add_argument('wallet_name', nargs='?', default='', - help="Name of wallet to create or open. Used to store your all your wallet keys " - "and will be printed on each paper wallet") + parser.add_argument('--list-wallets', '-l', action='store_true', + help="List all known wallets in database") + parser.add_argument('--generate-key', '-g', type=int, metavar="STRENGTH", help="Generate a new masterkey, and " + "show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet") + # parser_new.add_argument('--passphrase-strength', type=int, default=128, + # help="Number of bits for passphrase key. Default is 128, lower is not adviced but can " + # "be used for testing. Set to 256 bits for more future proof passphrases") + parser.add_argument('--database', '-d', + help="URI of the database to use",) + # TODO: use first wallet if only 1 wallet exists and no argument is provided + parser.add_argument('--wallet_name', '-w', nargs='?', default='', + help="Name of wallet to create or open. Provide wallet name or number when running wallet " + "actions") + parser.add_argument('--password', help='Password to protect private key, use to open and close wallet') + parser.add_argument('--network', '-n', + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + + subparsers = parser.add_subparsers(required=False) + parser_new = subparsers.add_parser('new', description="Create new wallet") + parser_new.add_argument('--wallet_name', '-w', nargs='?', default='', required=True, + help="Name of wallet to create or open. Provide wallet name or number when running wallet " + "actions") + parser_new.add_argument('--password', help='Password to protect private key, use to open and close wallet') + parser_new.add_argument('--network', '-n', + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + parser_new.add_argument('--passphrase', nargs="*", default=None, + help="Passphrase to recover or create a wallet. Usually 12 or 24 words") + parser_new.add_argument('--create-from-key', '-c', metavar='KEY', + help="Create a new wallet from specified key") + parser_new.add_argument('--create-multisig', '-m', nargs='*', + metavar='.', + help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, [KEY1, KEY2, ... KEY3]]' + 'Specify number of signatures followed by the number of signatures required and ' + 'then a list of public or private keys for this wallet. Private keys will be ' + 'created if not provided in key list.' + '\nExample, create a 2-of-2 multisig wallet and provide 1 key and create another ' + 'key: -m 2 2 tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQ' + 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' + 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') + parser_new.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, + help='Witness type of wallet: lecacy (default), p2sh-segwit or segwit') + parser_new.add_argument('--cosigner-id', '-o', type=int, default=0, + help='Set this if wallet contains only public keys, more then one private key or if ' + 'you would like to create keys for other cosigners.') + parser_new.add_argument('--database', '-d', + help="URI of the database to use",) group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', help="Name or ID of wallet to remove, all keys and transactions will be deleted") - group_wallet.add_argument('--list-wallets', '-l', action='store_true', - help="List all known wallets in BitcoinLib database") - group_wallet.add_argument('--wallet-info', '-w', action='store_true', - help="Show wallet information") + # group_wallet.add_argument('--wallet-info', '-w', action='store_true', + # help="Show wallet information") group_wallet.add_argument('--update-utxos', '-x', action='store_true', help="Update unspent transaction outputs (UTXO's) for this wallet") group_wallet.add_argument('--update-transactions', '-u', action='store_true', @@ -52,45 +93,22 @@ def parse_args(): help="Show unused address to receive funds. Specify cosigner-id to generate address for " "specific cosigner. Default is -1 for own wallet", const=-1, metavar='COSIGNER_ID') - group_wallet.add_argument('--generate-key', '-g', action='store_true', help="Generate a new masterkey, and show" - " passphrase, WIF and public account key. Can be used to create a multisig wallet") group_wallet.add_argument('--export-private', '-e', action='store_true', help="Export private key for this wallet and exit") group_wallet.add_argument('--import-private', '-v', help="Import private key in this wallet") - group_wallet2 = parser.add_argument_group("Wallet Setup") - group_wallet2.add_argument('--passphrase', nargs="*", default=None, - help="Passphrase to recover or create a wallet. Usually 12 or 24 words") - group_wallet2.add_argument('--passphrase-strength', type=int, default=128, - help="Number of bits for passphrase key. Default is 128, lower is not adviced but can " - "be used for testing. Set to 256 bits for more future proof passphrases") - group_wallet2.add_argument('--network', '-n', - help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") - group_wallet2.add_argument('--database', '-d', - help="URI of the database to use",) - group_wallet2.add_argument('--create-from-key', '-c', metavar='KEY', - help="Create a new wallet from specified key") - group_wallet2.add_argument('--create-multisig', '-m', nargs='*', - metavar='.', - help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, [KEY1, KEY2, ... KEY3]]' - 'Specificy number of signatures followed by the number of signatures required and ' - 'then a list of public or private keys for this wallet. Private keys will be ' - 'created if not provided in key list.' - '\nExample, create a 2-of-2 multisig wallet and provide 1 key and create another ' - 'key: -m 2 2 tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQ' - 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' - 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') - group_wallet2.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, - help='Witness type of wallet: lecacy (default), p2sh-segwit or segwit') - group_wallet2.add_argument('--cosigner-id', '-o', type=int, default=0, - help='Set this if wallet contains only public keys, more then one private key or if ' - 'you would like to create keys for other cosigners.') - group_transaction = parser.add_argument_group("Transactions") - # TODO: Add simple send address + amount - group_transaction.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, + # group_transaction = parser.add_argument_group("Transactions") + parser_tx = subparsers.add_parser('tx', description="Create transactions / send coins") + parser_tx.add_argument('--wallet_name', '-w', nargs='?', default='', + help="Name of wallet to create or open. Provide wallet name or number when running wallet " + "actions") + parser_tx.add_argument('--password', help='Password to protect private key, use to open and close wallet') + parser_tx.add_argument('--network', '-n', + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + parser_tx.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, help="Create transaction to send amount to specified address") - group_transaction.add_argument('--create-transaction', '-t', metavar=('ADDRESS_1', 'AMOUNT_1'), + parser_tx.add_argument('--create-transaction', '-t', metavar=('ADDRESS_1', 'AMOUNT_1'), help="Create transaction and send to multiple addresses. " "Specify address followed by amount in satoshis. Repeat for multiple " "outputs. Use -p to push to network, otherwise returns a dictionary which" @@ -99,32 +117,32 @@ def parse_args(): # TDDO: Add --create-raw-transaction # TODO: Add -k use specific key for inputs / scan # TODO: Add number_of_change_outputs - group_transaction.add_argument('--sweep', metavar="ADDRESS", + parser_tx.add_argument('--sweep', metavar="ADDRESS", help="Sweep wallet, transfer all funds to specified address") - group_transaction.add_argument('--fee', '-f', type=int, help="Transaction fee") - group_transaction.add_argument('--fee-per-kb', type=int, + parser_tx.add_argument('--fee', '-f', type=int, help="Transaction fee") + parser_tx.add_argument('--fee-per-kb', type=int, help="Transaction fee in sathosis (or smallest denominator) per kilobyte") - group_transaction.add_argument('--push', '-p', action='store_true', help="Push created transaction to the network") - group_transaction.add_argument('--import-tx', '-i', metavar="TRANSACTION", + parser_tx.add_argument('--push', '-p', action='store_true', help="Push created transaction to the network") + parser_tx.add_argument('--import-tx', '-i', metavar="TRANSACTION", help="Import raw transaction hash or transaction dictionary in wallet and sign " "it with available key(s)") - group_transaction.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", + parser_tx.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", help="Import transaction dictionary or raw transaction string from specified " "filename and sign it with available key(s)") pa = parser.parse_args() if pa.receive and pa.create_transaction: parser.error("Please select receive or create transaction option not both") - if pa.wallet_name: - pa.wallet_info = True - else: - pa.list_wallets = True + # if pa.wallet_name: + # pa.wallet_info = True + # else: + # pa.list_wallets = True return pa def get_passphrase(args): - inp_passphrase = Mnemonic('english').generate(args.passphrase_strength) - print("\nYour mnemonic private key sentence is: %s" % inp_passphrase) + inp_passphrase = Mnemonic('english').generate(args.generate_key) + print("\nPassphrase: %s" % inp_passphrase) print("\nPlease write down on paper and backup. With this key you can restore your wallet and all keys") passphrase = inp_passphrase.split(' ') inp = input("\nType 'yes' if you understood and wrote down your key: ") @@ -246,7 +264,7 @@ def main(): clw_exit() # List wallets, then exit - if args.list_wallets: + elif args.list_wallets: print("BitcoinLib wallets:") for w in wallets_list(db_uri=db_uri): if 'parent_id' in w and w['parent_id']: @@ -279,10 +297,10 @@ def main(): else: try: wlt = Wallet(args.wallet_name, db_uri=db_uri) - if args.passphrase is not None: - print("WARNING: Using passphrase option for existing wallet ignored") - if args.create_from_key is not None: - print("WARNING: Using create_from_key option for existing wallet ignored") + # if args.passphrase is not None: + # print("WARNING: Using passphrase option for existing wallet ignored") + # if args.create_from_key is not None: + # print("WARNING: Using create_from_key option for existing wallet ignored") except WalletError as e: clw_exit("Error: %s" % e.msg) @@ -364,7 +382,7 @@ def main(): clw_exit() if args.create_transaction == []: clw_exit("Missing arguments for --create-transaction/-t option") - if args.create_transaction: + if args.create_transaction or args.send: if args.fee_per_kb: clw_exit("Fee-per-kb option not allowed with --create-transaction") try: From 29ce79b1445cd631e71c2a1a687387dcb249b8cc Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 19 Jan 2024 09:53:24 +0100 Subject: [PATCH 041/207] Rewrite commandline wallet and fix unittests --- bitcoinlib/services/mempool.py | 7 +- bitcoinlib/tools/clw.py | 466 ++++++++++++++++----------------- dbde | Bin 0 -> 139264 bytes dbdel | Bin 0 -> 147456 bytes tests/test_tools.py | 263 +++++++++---------- 5 files changed, 361 insertions(+), 375 deletions(-) create mode 100644 dbde create mode 100644 dbdel diff --git a/bitcoinlib/services/mempool.py b/bitcoinlib/services/mempool.py index 5c588205..31615524 100644 --- a/bitcoinlib/services/mempool.py +++ b/bitcoinlib/services/mempool.py @@ -153,7 +153,12 @@ def getrawtransaction(self, txid): return self.compose_request('tx', txid, 'hex') def sendrawtransaction(self, rawtx): - return self.compose_request('tx', post_data=rawtx, method='post') + res = self.compose_request('tx', post_data=rawtx, method='post') + _logger.debug('mempool response: %s', res) + return { + 'txid': res, + 'response_dict': {} + } def estimatefee(self, blocks): estimates = self.compose_request('v1/fees', 'recommended') diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index f87a4142..9c791d74 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -16,7 +16,7 @@ from bitcoinlib.mnemonic import Mnemonic from bitcoinlib.keys import HDKey from bitcoinlib.main import BITCOINLIB_VERSION - +from bitcoinlib.config.config import DEFAULT_NETWORK try: import pyqrcode QRCODES_AVAILABLE = True @@ -24,43 +24,49 @@ QRCODES_AVAILABLE = False -DEFAULT_NETWORK = 'bitcoin' +# Show all errors in simple format without tracelog +def exception_handler(exception_type, exception, traceback): + print("%s: %s" % (exception_type.__name__, exception)) + +sys.excepthook = exception_handler def parse_args(): parser = argparse.ArgumentParser(description='BitcoinLib command line wallet') parser.add_argument('--list-wallets', '-l', action='store_true', help="List all known wallets in database") - parser.add_argument('--generate-key', '-g', type=int, metavar="STRENGTH", help="Generate a new masterkey, and " - "show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet") - # parser_new.add_argument('--passphrase-strength', type=int, default=128, - # help="Number of bits for passphrase key. Default is 128, lower is not adviced but can " - # "be used for testing. Set to 256 bits for more future proof passphrases") + parser.add_argument('--generate-key', '-g', action='store_true', help="Generate a new masterkey, and" + " show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet") + parser.add_argument('--passphrase-strength', type=int, default=128, + help="Number of bits for passphrase key. Default is 128, lower is not advised but can " + "be used for testing. Set to 256 bits for more future-proof passphrases") parser.add_argument('--database', '-d', help="URI of the database to use",) - # TODO: use first wallet if only 1 wallet exists and no argument is provided parser.add_argument('--wallet_name', '-w', nargs='?', default='', help="Name of wallet to create or open. Provide wallet name or number when running wallet " "actions") - parser.add_argument('--password', help='Password to protect private key, use to open and close wallet') + parser.add_argument('--password', + help='Password to protect private key, use to open and close wallet') parser.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + parser.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, + help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') - subparsers = parser.add_subparsers(required=False) + subparsers = parser.add_subparsers(required=False, dest='subparser_name') parser_new = subparsers.add_parser('new', description="Create new wallet") parser_new.add_argument('--wallet_name', '-w', nargs='?', default='', required=True, help="Name of wallet to create or open. Provide wallet name or number when running wallet " "actions") - parser_new.add_argument('--password', help='Password to protect private key, use to open and close wallet') + parser_new.add_argument('--password', + help='Password to protect private key, use to open and close wallet') parser_new.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") - parser_new.add_argument('--passphrase', nargs="*", default=None, + parser_new.add_argument('--passphrase', default=None, metavar="PASSPHRASE", help="Passphrase to recover or create a wallet. Usually 12 or 24 words") parser_new.add_argument('--create-from-key', '-c', metavar='KEY', help="Create a new wallet from specified key") - parser_new.add_argument('--create-multisig', '-m', nargs='*', - metavar='.', - help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, [KEY1, KEY2, ... KEY3]]' + parser_new.add_argument('--create-multisig', '-m', nargs='*', metavar='.', + help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, KEY-1, KEY-2, ... KEY-N]' 'Specify number of signatures followed by the number of signatures required and ' 'then a list of public or private keys for this wallet. Private keys will be ' 'created if not provided in key list.' @@ -69,18 +75,22 @@ def parse_args(): 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') parser_new.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, - help='Witness type of wallet: lecacy (default), p2sh-segwit or segwit') - parser_new.add_argument('--cosigner-id', '-o', type=int, default=0, + help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') + parser_new.add_argument('--cosigner-id', type=int, default=0, help='Set this if wallet contains only public keys, more then one private key or if ' 'you would like to create keys for other cosigners.') parser_new.add_argument('--database', '-d', help="URI of the database to use",) + parser_new.add_argument('--receive', '-r', nargs='?', type=int, + help="Show unused address to receive funds. Specify cosigner-id to generate address for " + "specific cosigner. Default is -1 for own wallet", + const=-1, metavar='COSIGNER_ID') group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', help="Name or ID of wallet to remove, all keys and transactions will be deleted") - # group_wallet.add_argument('--wallet-info', '-w', action='store_true', - # help="Show wallet information") + group_wallet.add_argument('--wallet-info', '-i', action='store_true', + help="Show wallet information") group_wallet.add_argument('--update-utxos', '-x', action='store_true', help="Update unspent transaction outputs (UTXO's) for this wallet") group_wallet.add_argument('--update-transactions', '-u', action='store_true', @@ -98,56 +108,45 @@ def parse_args(): group_wallet.add_argument('--import-private', '-v', help="Import private key in this wallet") - # group_transaction = parser.add_argument_group("Transactions") - parser_tx = subparsers.add_parser('tx', description="Create transactions / send coins") - parser_tx.add_argument('--wallet_name', '-w', nargs='?', default='', - help="Name of wallet to create or open. Provide wallet name or number when running wallet " - "actions") - parser_tx.add_argument('--password', help='Password to protect private key, use to open and close wallet') - parser_tx.add_argument('--network', '-n', - help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") - parser_tx.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, - help="Create transaction to send amount to specified address") - parser_tx.add_argument('--create-transaction', '-t', metavar=('ADDRESS_1', 'AMOUNT_1'), - help="Create transaction and send to multiple addresses. " - "Specify address followed by amount in satoshis. Repeat for multiple " - "outputs. Use -p to push to network, otherwise returns a dictionary which" - "can be used to export", - nargs='*') - # TDDO: Add --create-raw-transaction - # TODO: Add -k use specific key for inputs / scan - # TODO: Add number_of_change_outputs - parser_tx.add_argument('--sweep', metavar="ADDRESS", + group_transaction = parser.add_argument_group("Transactions") + group_transaction.add_argument('--send', '-s', metavar=('ADDRESS', 'AMOUNT'), nargs=2, + action='append', + help="Create transaction to send amount to specified address. To send to " + "multiple addresses, argument can be used multiple times.") + group_transaction.add_argument('--number-of-change-outputs', type=int, default=1, + help="Number of change outputs. Default is 1, increase for more privacy or " + "to split funds") + group_transaction.add_argument('--input-key-id', type=int, default=None, + help="Use to create transaction with 1 specific key ID") + group_transaction.add_argument('--sweep', metavar="ADDRESS", help="Sweep wallet, transfer all funds to specified address") - parser_tx.add_argument('--fee', '-f', type=int, help="Transaction fee") - parser_tx.add_argument('--fee-per-kb', type=int, - help="Transaction fee in sathosis (or smallest denominator) per kilobyte") - parser_tx.add_argument('--push', '-p', action='store_true', help="Push created transaction to the network") - parser_tx.add_argument('--import-tx', '-i', metavar="TRANSACTION", + group_transaction.add_argument('--fee', '-f', type=int, help="Transaction fee") + group_transaction.add_argument('--fee-per-kb', type=int, + help="Transaction fee in satoshi per kilobyte") + group_transaction.add_argument('--push', '-p', action='store_true', + help="Push created transaction to the network") + group_transaction.add_argument('--import-tx', metavar="TRANSACTION", help="Import raw transaction hash or transaction dictionary in wallet and sign " "it with available key(s)") - parser_tx.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", + group_transaction.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", help="Import transaction dictionary or raw transaction string from specified " "filename and sign it with available key(s)") pa = parser.parse_args() - if pa.receive and pa.create_transaction: - parser.error("Please select receive or create transaction option not both") - # if pa.wallet_name: - # pa.wallet_info = True - # else: - # pa.list_wallets = True + + if not pa.wallet_name: + pa.list_wallets = True return pa -def get_passphrase(args): - inp_passphrase = Mnemonic('english').generate(args.generate_key) - print("\nPassphrase: %s" % inp_passphrase) +def get_passphrase(strength): + passphrase = Mnemonic().generate(strength) + print("\nPassphrase: %s" % passphrase) print("\nPlease write down on paper and backup. With this key you can restore your wallet and all keys") - passphrase = inp_passphrase.split(' ') inp = input("\nType 'yes' if you understood and wrote down your key: ") if inp not in ['yes', 'Yes', 'YES']: - clw_exit("Exiting...") + print("Exiting...") + sys.exit() return passphrase @@ -157,18 +156,18 @@ def create_wallet(wallet_name, args, db_uri): print("\nCREATE wallet '%s' (%s network)" % (wallet_name, args.network)) if args.create_multisig: if not isinstance(args.create_multisig, list) or len(args.create_multisig) < 2: - clw_exit("Please enter multisig creation parameter in the following format: " + raise WalletError("Please enter multisig creation parameter in the following format: " " " " [ ... ]") try: sigs_total = int(args.create_multisig[0]) except ValueError: - clw_exit("Number of total signatures (first argument) must be a numeric value. %s" % + raise WalletError("Number of total signatures (first argument) must be a numeric value. %s" % args.create_multisig[0]) try: sigs_required = int(args.create_multisig[1]) except ValueError: - clw_exit("Number of signatures required (second argument) must be a numeric value. %s" % + raise WalletError("Number of signatures required (second argument) must be a numeric value. %s" % args.create_multisig[1]) key_list = args.create_multisig[2:] keys_missing = sigs_total - len(key_list) @@ -176,10 +175,8 @@ def create_wallet(wallet_name, args, db_uri): if keys_missing: print("Not all keys provided, creating %d additional key(s)" % keys_missing) for _ in range(keys_missing): - passphrase = get_passphrase(args) - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - key_list.append(HDKey.from_seed(seed, network=args.network)) + passphrase = get_passphrase(args.passphrase_strength) + key_list.append(HDKey.from_passphrase(passphrase, network=args.network)) return Wallet.create(wallet_name, key_list, sigs_required=sigs_required, network=args.network, cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type) elif args.create_from_key: @@ -194,44 +191,41 @@ def create_wallet(wallet_name, args, db_uri): else: passphrase = args.passphrase if passphrase is None: - passphrase = get_passphrase(args) + passphrase = get_passphrase(args.passphrase_strength) elif not passphrase: passphrase = input("Enter Passphrase: ") - if not isinstance(passphrase, list): - passphrase = passphrase.split(' ') - elif len(passphrase) == 1: - passphrase = passphrase[0].split(' ') - if len(passphrase) < 12: - clw_exit("Please specify passphrase with 12 words or more") - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - hdkey = HDKey.from_seed(seed, network=args.network) + if len(passphrase.split(' ')) < 3: + raise WalletError("Please specify passphrase with 3 words or more. However less than 12 words is insecure!") + hdkey = HDKey.from_passphrase(passphrase, network=args.network) return Wallet.create(wallet_name, hdkey, network=args.network, witness_type=args.witness_type, - db_uri=db_uri) + password=args.password, db_uri=db_uri) def create_transaction(wlt, send_args, args): - output_arr = [] - while send_args: - if len(send_args) == 1: - raise ValueError("Invalid number of transaction inputs. Use ... ") - try: - amount = int(send_args[1]) - except ValueError: - clw_exit("Amount must be in satoshis, an integer value: %s" % send_args[1]) - output_arr.append((send_args[0], amount)) - send_args = send_args[2:] - return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0) + output_arr = [(address, int(value)) for [address, value] in send_args] + return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0, + input_key_id=args.input_key_id, + number_of_change_outputs=args.number_of_change_outputs) def print_transaction(wt): tx_dict = { - 'network': wt.network.name, 'fee': wt.fee, 'raw': wt.raw_hex(), 'outputs': [{ - 'address': o.address, 'value': o.value - } for o in wt.outputs], 'inputs': [{ - 'prev_hash': i.prev_txid.hex(), 'output_n': int.from_bytes(i.output_n, 'big'), - 'address': i.address, 'signatures': [{ - 'signature': s.hex(), 'sig_der': s.as_der_encoded(as_hex=True), + 'txid': wt.txid, + 'network': wt.network.name, + 'fee': wt.fee, + 'raw': wt.raw_hex(), + 'witness_type': wt.witness_type, + 'outputs': [{ + 'address': o.address, + 'value': o.value + } for o in wt.outputs], + 'inputs': [{ + 'prev_hash': i.prev_txid.hex(), + 'output_n': int.from_bytes(i.output_n, 'big'), + 'address': i.address, + 'signatures': [{ + 'signature': s.hex(), + 'sig_der': s.as_der_encoded(as_hex=True), 'pub_key': s.public_key.public_hex, } for s in i.signatures], 'value': i.value } for i in wt.inputs] @@ -239,136 +233,172 @@ def print_transaction(wt): pprint(tx_dict) -def clw_exit(msg=None): - if msg: - print(msg) - sys.exit() - - def main(): print("Command Line Wallet - BitcoinLib %s\n" % BITCOINLIB_VERSION) - # --- Parse commandline arguments --- args = parse_args() db_uri = args.database + wlt = None + # --- General arguments --- + # Generate key if args.generate_key: - passphrase = get_passphrase(args) - passphrase = ' '.join(passphrase) - seed = Mnemonic().to_seed(passphrase).hex() - hdkey = HDKey.from_seed(seed, network=args.network) + passphrase = get_passphrase(args.passphrase_strength) + hdkey = HDKey.from_passphrase(passphrase, witness_type=args.witness_type, network=args.network) print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) print("Public Master key, to share with other cosigner multisig wallets:\n%s" % hdkey.public_master(witness_type=args.witness_type, multisig=True).wif()) print("Network: %s" % hdkey.network.name) - clw_exit() - # List wallets, then exit + # List wallets elif args.list_wallets: print("BitcoinLib wallets:") - for w in wallets_list(db_uri=db_uri): + wallets = wallets_list(db_uri=db_uri) + if not wallets: + print("No wallets defined yet, use 'new' argument to create a new wallet. See clw new --help " + "for more info.") + for w in wallets: if 'parent_id' in w and w['parent_id']: continue print("[%d] %s (%s) %s" % (w['id'], w['name'], w['network'], w['owner'])) - clw_exit() - # Delete specified wallet, then exit - if args.wallet_remove: + # Delete specified wallet + elif args.wallet_remove: if not wallet_exists(args.wallet_name, db_uri=db_uri): - clw_exit("Wallet '%s' not found" % args.wallet_name) - inp = input("\nWallet '%s' with all keys and will be removed, without private key it cannot be restored." - "\nPlease retype exact name of wallet to proceed: " % args.wallet_name) - if inp == args.wallet_name: - if wallet_delete(args.wallet_name, force=True, db_uri=db_uri): - clw_exit("\nWallet %s has been removed" % args.wallet_name) - else: - clw_exit("\nError when deleting wallet") + print("Wallet '%s' not found" % args.wallet_name) else: - clw_exit("\nSpecified wallet name incorrect") + inp = input("\nWallet '%s' with all keys and will be removed, without private key it cannot be restored." + "\nPlease retype exact name of wallet to proceed: " % args.wallet_name) + if inp == args.wallet_name: + if wallet_delete(args.wallet_name, force=True, db_uri=db_uri): + print("\nWallet %s has been removed" % args.wallet_name) + else: + print("\nError when deleting wallet") + else: + print("\nSpecified wallet name incorrect") - wlt = None - if args.wallet_name and not args.wallet_name.isdigit() and not wallet_exists(args.wallet_name, - db_uri=db_uri): - if not args.create_from_key and input( - "Wallet %s does not exist, create new wallet [yN]? " % args.wallet_name).lower() != 'y': - clw_exit('Aborted') - wlt = create_wallet(args.wallet_name, args, db_uri) - args.wallet_info = True - else: - try: - wlt = Wallet(args.wallet_name, db_uri=db_uri) - # if args.passphrase is not None: - # print("WARNING: Using passphrase option for existing wallet ignored") - # if args.create_from_key is not None: - # print("WARNING: Using create_from_key option for existing wallet ignored") - except WalletError as e: - clw_exit("Error: %s" % e.msg) + # Create or open wallet + elif args.wallet_name: + if args.subparser_name == 'new': + if wallet_exists(args.wallet_name, db_uri=db_uri): + print("Wallet with name '%s' already exists" % args.wallet_name) + else: + wlt = create_wallet(args.wallet_name, args, db_uri) + args.wallet_info = True + else: + try: + wlt = Wallet(args.wallet_name, db_uri=db_uri) + except WalletError as e: + print("Error: %s" % e.msg) if wlt is None: - clw_exit("Could not open wallet %s" % args.wallet_name) - - if args.import_private: - if wlt.import_key(args.import_private): - clw_exit("Private key imported") - else: - clw_exit("Failed to import key") - - if args.wallet_recreate: - wallet_empty(args.wallet_name) - print("Removed transactions and generated keys from this wallet") - if args.update_utxos: - wlt.utxos_update() - if args.update_transactions: - wlt.scan(scan_gap_limit=5) - - if args.export_private: - if wlt.scheme == 'multisig': - for w in wlt.cosigner: - if w.main_key and w.main_key.is_private: - print(w.main_key.wif) - elif not wlt.main_key or not wlt.main_key.is_private: - print("No private key available for this wallet") - else: - print(wlt.main_key.wif) - clw_exit() + sys.exit() if args.network is None: args.network = wlt.network.name tx_import = None - if args.import_tx_file: - try: - fn = args.import_tx_file - f = open(fn, "r") - except FileNotFoundError: - clw_exit("File %s not found" % args.import_tx_file) - try: - tx_import = ast.literal_eval(f.read()) - except (ValueError, SyntaxError): - tx_import = f.read() - if args.import_tx: - try: - tx_import = ast.literal_eval(args.import_tx) - except (ValueError, SyntaxError): - tx_import = args.import_tx - if tx_import: - if isinstance(tx_import, dict): - wt = wlt.transaction_import(tx_import) - else: - wt = wlt.transaction_import_raw(tx_import, network=args.network) - wt.sign() - if args.push: - res = wt.send() - if res: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + if not args.subparser_name: + + if args.import_private: + if wlt.import_key(args.import_private): + print("Private key imported") + else: + print("Failed to import key") + + elif args.wallet_recreate: + wallet_empty(args.wallet_name) + print("Removed transactions and emptied wallet. Use --update-wallet option to update again.") + elif args.update_utxos: + wlt.utxos_update() + elif args.update_transactions: + wlt.scan(scan_gap_limit=3) + elif args.export_private: + if wlt.scheme == 'multisig': + for w in wlt.cosigner: + if w.main_key and w.main_key.is_private: + print(w.main_key.wif) + elif not wlt.main_key or not wlt.main_key.is_private: + print("No private key available for this wallet") else: - print("Error creating transaction: %s" % wt.error) - wt.info() - print("Signed transaction:") - print_transaction(wt) - clw_exit() + print(wlt.main_key.wif) + elif args.import_tx_file or args.import_tx: + if args.import_tx_file: + try: + fn = args.import_tx_file + f = open(fn, "r") + except FileNotFoundError: + print("File %s not found" % args.import_tx_file) + sys.exit() + try: + tx_import = ast.literal_eval(f.read()) + except (ValueError, SyntaxError): + tx_import = f.read() + elif args.import_tx: + try: + tx_import = ast.literal_eval(args.import_tx) + except (ValueError, SyntaxError): + tx_import = args.import_tx + if tx_import: + if isinstance(tx_import, dict): + wt = wlt.transaction_import(tx_import) + else: + wt = wlt.transaction_import_raw(tx_import, network=args.network) + wt.sign() + if args.push: + res = wt.send() + if res: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + else: + print("Error creating transaction: %s" % wt.error) + wt.info() + print("Signed transaction:") + print_transaction(wt) + elif args.send: + # if args.fee_per_kb: + # raise WalletError("Fee-per-kb option not allowed with --create-transaction") + try: + wt = create_transaction(wlt, args.send, args) + except WalletError as e: + raise WalletError("Cannot create transaction: %s" % e.msg) + wt.sign() + print("Transaction created") + wt.info() + if args.push: + wt.send() + if wt.pushed: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + else: + print("Error creating transaction: %s" % wt.error) + else: + print("\nTransaction created but not sent yet. Transaction dictionary for export: ") + print_transaction(wt) + elif args.sweep: + if args.fee: + raise WalletError("Fee option not allowed with --sweep") + offline = True + print("Sweep wallet. Send all funds to %s" % args.sweep) + if args.push: + offline = False + wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb) + if not wt: + raise WalletError("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) + wt.info() + if args.push: + if wt.pushed: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + elif not wt: + print("Cannot sweep wallet, are UTXO's updated and available?") + else: + print("Error sweeping wallet: %s" % wt.error) + else: + print("\nTransaction created but not sent yet. Transaction dictionary for export: ") + print_transaction(wt) + else: + print("Please provide an argument. Use -h or --help for more information") - if args.receive: + + if args.receive and not (args.send or args.sweep): cosigner_id = args.receive if args.receive == -1: cosigner_id = None @@ -379,57 +409,9 @@ def main(): print(qrcode.terminal()) else: print("Install qr code module to show QR codes: pip install pyqrcode") - clw_exit() - if args.create_transaction == []: - clw_exit("Missing arguments for --create-transaction/-t option") - if args.create_transaction or args.send: - if args.fee_per_kb: - clw_exit("Fee-per-kb option not allowed with --create-transaction") - try: - wt = create_transaction(wlt, args.create_transaction, args) - except WalletError as e: - clw_exit("Cannot create transaction: %s" % e.msg) - wt.sign() - print("Transaction created") - wt.info() - if args.push: - wt.send() - if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) - else: - print("Error creating transaction: %s" % wt.error) - else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") - print_transaction(wt) - clw_exit() - if args.sweep: - if args.fee: - clw_exit("Fee option not allowed with --sweep") - offline = True - print("Sweep wallet. Send all funds to %s" % args.sweep) - if args.push: - offline = False - wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb) - if not wt: - clw_exit("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) - wt.info() - if args.push: - if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) - elif not wt: - print("Cannot sweep wallet, are UTXO's updated and available?") - else: - print("Error sweeping wallet: %s" % wt.error) - else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") - print_transaction(wt) - clw_exit() - - # print("Updating wallet") - if args.network == 'bitcoinlib_test': - wlt.utxos_update() - print("Wallet info for %s" % wlt.name) - wlt.info() + elif args.wallet_info: + print("Wallet info for %s" % wlt.name) + wlt.info() if __name__ == '__main__': diff --git a/dbde b/dbde new file mode 100644 index 0000000000000000000000000000000000000000..3f6bb64d9bf0d3737291c90d9bad0cd937b1460c GIT binary patch literal 139264 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 literal 0 HcmV?d00001 diff --git a/dbdel b/dbdel new file mode 100644 index 0000000000000000000000000000000000000000..9dab582511eb0ed29db3807a503f554e5934b3ab GIT binary patch literal 147456 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 Pbh@NrXAo0PYs3CO3uP$x literal 0 HcmV?d00001 diff --git a/tests/test_tools.py b/tests/test_tools.py index e10db4f7..b8370acc 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -73,138 +73,137 @@ def setUp(self): self.clw_executable = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../bitcoinlib/tools/clw.py')) - # TODO: Re-enable when CLW rewrite is finished - # def test_tools_clw_create_wallet(self): - # cmd_wlt_create = '%s %s test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ - # 'actual chicken obscure spray" -r -d %s' % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # cmd_wlt_delete = "%s %s test --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "bc1qdv5tuzrluh4lzhnu59je9n83w4hkqjhgg44d5g" - # output_wlt_delete = "Wallet test has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'test') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) - # - # def test_tools_clw_create_multisig_wallet(self): - # key_list = [ - # 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' - # '5zNYeiX8', - # 'tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJ' - # 'MeQHdWDp' - # ] - # cmd_wlt_create = "%s %s testms -m 2 2 %s -r -n testnet -d %s" % \ - # (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - # cmd_wlt_delete = "%s %s testms --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "2NBrLTapyFqU4Wo29xG4QeEt8kn38KVWRR" - # output_wlt_delete = "Wallet testms has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'testms') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) - # - # def test_tools_clw_create_multisig_wallet_one_key(self): - # key_list = [ - # 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' - # '5zNYeiX8' - # ] - # cmd_wlt_create = "%s %s testms1 -m 2 2 %s -r -n testnet -d %s" % \ - # (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - # cmd_wlt_delete = "%s %s testms1 --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "if you understood and wrote down your key: Receive address:" - # output_wlt_delete = "Wallet testms1 has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y\nyes') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'testms1') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) - # - # def test_tools_clw_create_multisig_wallet_error(self): - # cmd_wlt_create = "%s %s testms2 -m 2 a -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "Number of signatures required (second argument) must be a numeric value" - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # - # def test_tools_clw_transaction_with_script(self): - # cmd_wlt_create = '%s %s test2 --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ - # 'actual chicken obscure spray" -r -n bitcoinlib_test -d %s' % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # cmd_wlt_update = "%s %s test2 -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # cmd_wlt_transaction = "%s %s test2 -d %s -t 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 50000000 -f 100000 -p" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # cmd_wlt_delete = "%s %s test2 --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "blt1qj0mgwyhxuw9p0ngj5kqnxhlrx8ypecqekm2gr7" - # output_wlt_transaction = 'Transaction pushed to network' - # output_wlt_delete = "Wallet test2 has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # - # process = Popen(cmd_wlt_update, stdout=PIPE, shell=True) - # process.communicate() - # - # process = Popen(cmd_wlt_transaction, stdout=PIPE, shell=True) - # poutput = process.communicate() - # self.assertIn(output_wlt_transaction, normalize_string(poutput[0])) - # - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'test2') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) - # - # def test_tools_clw_create_litecoin_segwit_wallet(self): - # cmd_wlt_create = '%s %s ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ - # 'sword order share" -r -d %s -y segwit -n litecoin' % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # cmd_wlt_delete = "%s %s ltcsw --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "ltc1qgc7c2z56rr4lftg0fr8tgh2vknqc3yuydedu6m" - # output_wlt_delete = "Wallet ltcsw has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'ltcsw') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) - # - # def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): - # key_list = [ - # 'YprvANkMzkodih9AKnvFGXTm8Fid3b6wDWoRq5GxmoFb8Rwoa4YsJvoHtbd6jFhCiCzG8Da3bFbkBeQq7Lz1YDAqufAZB5paBaZTEv8' - # 'A1Yxfi5R', - # 'YprvANkMzkodih9AJ6UamjW9rTWqBDMm5Be3M2cKybivd6V1MSMnKnGDkUXsVkz1hPKKNPFRZS9fFchRGKTgKdyTsppMuHjQQMVFBLY' - # 'Ghp5MTsC', - # 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' - # '7FW6wpKW' - # ] - # cmd_wlt_create = "%s %s testms-p2sh-segwit -m 3 2 %s -r -y p2sh-segwit -d %s" % \ - # (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) - # cmd_wlt_delete = "%s %s testms-p2sh-segwit --wallet-remove -d %s" % \ - # (self.python_executable, self.clw_executable, self.DATABASE_URI) - # output_wlt_create = "3MtNi5U2cjs3EcPizzjarSz87pU9DTANge" - # output_wlt_delete = "Wallet testms-p2sh-segwit has been removed" - # - # process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'y') - # self.assertIn(output_wlt_create, normalize_string(poutput[0])) - # process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) - # poutput = process.communicate(input=b'testms-p2sh-segwit') - # self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + def test_tools_clw_create_wallet(self): + cmd_wlt_create = '%s %s new -w test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ + 'actual chicken obscure spray" -r -d %s' % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w test --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "bc1qdv5tuzrluh4lzhnu59je9n83w4hkqjhgg44d5g" + output_wlt_delete = "Wallet test has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'test') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + + def test_tools_clw_create_multisig_wallet(self): + key_list = [ + 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' + '5zNYeiX8', + 'tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJ' + 'MeQHdWDp' + ] + cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w testms --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "2NBrLTapyFqU4Wo29xG4QeEt8kn38KVWRR" + output_wlt_delete = "Wallet testms has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'testms') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + + def test_tools_clw_create_multisig_wallet_one_key(self): + key_list = [ + 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' + '5zNYeiX8' + ] + cmd_wlt_create = "%s %s new -w testms1 -m 2 2 %s -r -n testnet -d %s" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w testms1 --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "if you understood and wrote down your key: Receive address:" + output_wlt_delete = "Wallet testms1 has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'yes') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'testms1') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + + def test_tools_clw_create_multisig_wallet_error(self): + cmd_wlt_create = "%s %s new -w testms2 -m 2 a -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "Number of signatures required (second argument) must be a numeric value" + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + def test_tools_clw_transaction_with_script(self): + cmd_wlt_create = '%s %s new -w test2 --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ + 'actual chicken obscure spray" -r -n bitcoinlib_test -d %s' % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_update = "%s %s -w test2 -x -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_transaction = "%s %s -w test2 -d %s -s 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 50000000 -f 100000 -p" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w test2 --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "blt1qj0mgwyhxuw9p0ngj5kqnxhlrx8ypecqekm2gr7" + output_wlt_transaction = 'Transaction pushed to network' + output_wlt_delete = "Wallet test2 has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + process = Popen(cmd_wlt_update, stdout=PIPE, shell=True) + process.communicate() + + process = Popen(cmd_wlt_transaction, stdout=PIPE, shell=True) + poutput = process.communicate() + self.assertIn(output_wlt_transaction, normalize_string(poutput[0])) + + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'test2') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + + def test_tools_clw_create_litecoin_segwit_wallet(self): + cmd_wlt_create = '%s %s new -w ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ + 'sword order share" -d %s -y segwit -n litecoin -r' % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w ltcsw --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "ltc1qgc7c2z56rr4lftg0fr8tgh2vknqc3yuydedu6m" + output_wlt_delete = "Wallet ltcsw has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'ltcsw') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + + def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): + key_list = [ + 'YprvANkMzkodih9AKnvFGXTm8Fid3b6wDWoRq5GxmoFb8Rwoa4YsJvoHtbd6jFhCiCzG8Da3bFbkBeQq7Lz1YDAqufAZB5paBaZTEv8' + 'A1Yxfi5R', + 'YprvANkMzkodih9AJ6UamjW9rTWqBDMm5Be3M2cKybivd6V1MSMnKnGDkUXsVkz1hPKKNPFRZS9fFchRGKTgKdyTsppMuHjQQMVFBLY' + 'Ghp5MTsC', + 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' + '7FW6wpKW' + ] + cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -y p2sh-segwit -d %s" % \ + (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output_wlt_create = "3MtNi5U2cjs3EcPizzjarSz87pU9DTANge" + output_wlt_delete = "Wallet testms-p2sh-segwit has been removed" + + process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'y') + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + process = Popen(cmd_wlt_delete, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate(input=b'testms-p2sh-segwit') + self.assertIn(output_wlt_delete, normalize_string(poutput[0])) if __name__ == '__main__': From 56e6c825754a6fcb4c4b583173ffc7be35570476 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 19 Jan 2024 09:54:11 +0100 Subject: [PATCH 042/207] Remove testdbs --- dbde | Bin 139264 -> 0 bytes dbdel | Bin 147456 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dbde delete mode 100644 dbdel diff --git a/dbde b/dbde deleted file mode 100644 index 3f6bb64d9bf0d3737291c90d9bad0cd937b1460c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139264 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 diff --git a/dbdel b/dbdel deleted file mode 100644 index 9dab582511eb0ed29db3807a503f554e5934b3ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147456 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 Pbh@NrXAo0PYs3CO3uP$x From 79d9448165d03e8c02bd98db3e0c7d3f2e96d7ef Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 19 Jan 2024 14:49:46 +0100 Subject: [PATCH 043/207] Add interactive mode to commandline wallet. Change shortcut options --- bitcoinlib/tools/clw.py | 76 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 9c791d74..46d33365 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -49,8 +49,12 @@ def parse_args(): help='Password to protect private key, use to open and close wallet') parser.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") - parser.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, + parser.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') + parser.add_argument('--yes', '-y', action='store_true', default=False, + help='Non-interactive mode, does not prompt for confirmation') + parser.add_argument('--quiet', '-q', action='store_true', + help='Quit mode, no output writen to console.') subparsers = parser.add_subparsers(required=False, dest='subparser_name') parser_new = subparsers.add_parser('new', description="Create new wallet") @@ -74,7 +78,7 @@ def parse_args(): 'key: -m 2 2 tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQ' 'EAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsPeUbMS6kswJc11zgV' 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') - parser_new.add_argument('--witness-type', '-y', metavar='WITNESS_TYPE', default=None, + parser_new.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') parser_new.add_argument('--cosigner-id', type=int, default=0, help='Set this if wallet contains only public keys, more then one private key or if ' @@ -85,6 +89,10 @@ def parse_args(): help="Show unused address to receive funds. Specify cosigner-id to generate address for " "specific cosigner. Default is -1 for own wallet", const=-1, metavar='COSIGNER_ID') + parser_new.add_argument('--yes', '-y', action='store_true', default=False, + help='Non-interactive mode, does not prompt for confirmation') + parser_new.add_argument('--quiet', '-q', action='store_true', + help='Quit mode, no output writen to console.') group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', @@ -95,10 +103,10 @@ def parse_args(): help="Update unspent transaction outputs (UTXO's) for this wallet") group_wallet.add_argument('--update-transactions', '-u', action='store_true', help="Update all transactions and UTXO's for this wallet") - group_wallet.add_argument('--wallet-recreate', '-z', action='store_true', - help="Delete all keys and transactions and recreate wallet, except for the masterkey(s)." - " Use when updating fails or other errors occur. Please backup your database and " - "masterkeys first.") + group_wallet.add_argument('--wallet-empty', '-z', action='store_true', + help="Delete all keys and transactions from wallet, except for the masterkey(s). " + "Use when updating fails or other errors occur. Please backup your database and " + "masterkeys first. Update empty wallet again to restore your wallet.") group_wallet.add_argument('--receive', '-r', nargs='?', type=int, help="Show unused address to receive funds. Specify cosigner-id to generate address for " "specific cosigner. Default is -1 for own wallet", @@ -116,12 +124,12 @@ def parse_args(): group_transaction.add_argument('--number-of-change-outputs', type=int, default=1, help="Number of change outputs. Default is 1, increase for more privacy or " "to split funds") - group_transaction.add_argument('--input-key-id', type=int, default=None, + group_transaction.add_argument('--input-key-id', '-k', type=int, default=None, help="Use to create transaction with 1 specific key ID") group_transaction.add_argument('--sweep', metavar="ADDRESS", help="Sweep wallet, transfer all funds to specified address") group_transaction.add_argument('--fee', '-f', type=int, help="Transaction fee") - group_transaction.add_argument('--fee-per-kb', type=int, + group_transaction.add_argument('--fee-per-kb', '-b', type=int, help="Transaction fee in satoshi per kilobyte") group_transaction.add_argument('--push', '-p', action='store_true', help="Push created transaction to the network") @@ -139,12 +147,11 @@ def parse_args(): return pa -def get_passphrase(strength): +def get_passphrase(strength, interactive=False): passphrase = Mnemonic().generate(strength) - print("\nPassphrase: %s" % passphrase) - print("\nPlease write down on paper and backup. With this key you can restore your wallet and all keys") - inp = input("\nType 'yes' if you understood and wrote down your key: ") - if inp not in ['yes', 'Yes', 'YES']: + print("Passphrase: %s" % passphrase) + print("Please write down on paper and backup. With this key you can restore your wallet and all keys") + if not interactive and input("\nType 'yes' if you understood and wrote down your key: ") not in ['yes', 'Yes', 'YES']: print("Exiting...") sys.exit() return passphrase @@ -175,7 +182,7 @@ def create_wallet(wallet_name, args, db_uri): if keys_missing: print("Not all keys provided, creating %d additional key(s)" % keys_missing) for _ in range(keys_missing): - passphrase = get_passphrase(args.passphrase_strength) + passphrase = get_passphrase(args.passphrase_strength, args.yes) key_list.append(HDKey.from_passphrase(passphrase, network=args.network)) return Wallet.create(wallet_name, key_list, sigs_required=sigs_required, network=args.network, cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type) @@ -184,16 +191,15 @@ def create_wallet(wallet_name, args, db_uri): import_key = args.create_from_key kf = get_key_format(import_key) if kf['format'] == 'wif_protected': - password = input('Key password? ') - import_key, _ = HDKey._bip38_decrypt(import_key, password) + if not args.password: + raise WalletError("This is a WIF protected key, please provide a password with the --password argument.") + import_key, _ = HDKey._bip38_decrypt(import_key, args.password) return Wallet.create(wallet_name, import_key, network=args.network, db_uri=db_uri, witness_type=args.witness_type) else: passphrase = args.passphrase if passphrase is None: - passphrase = get_passphrase(args.passphrase_strength) - elif not passphrase: - passphrase = input("Enter Passphrase: ") + passphrase = get_passphrase(args.passphrase_strength, args.yes) if len(passphrase.split(' ')) < 3: raise WalletError("Please specify passphrase with 3 words or more. However less than 12 words is insecure!") hdkey = HDKey.from_passphrase(passphrase, network=args.network) @@ -243,7 +249,7 @@ def main(): # --- General arguments --- # Generate key if args.generate_key: - passphrase = get_passphrase(args.passphrase_strength) + passphrase = get_passphrase(args.passphrase_strength, args.yes) hdkey = HDKey.from_passphrase(passphrase, witness_type=args.witness_type, network=args.network) print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) print("Public Master key, to share with other cosigner multisig wallets:\n%s" % @@ -264,18 +270,22 @@ def main(): # Delete specified wallet elif args.wallet_remove: - if not wallet_exists(args.wallet_name, db_uri=db_uri): + wallet_name = args.wallet_name + if args.wallet_name.isdigit(): + wallet_name = int(args.wallet_name) + if not wallet_exists(wallet_name, db_uri=db_uri): print("Wallet '%s' not found" % args.wallet_name) else: - inp = input("\nWallet '%s' with all keys and will be removed, without private key it cannot be restored." - "\nPlease retype exact name of wallet to proceed: " % args.wallet_name) - if inp == args.wallet_name: - if wallet_delete(args.wallet_name, force=True, db_uri=db_uri): - print("\nWallet %s has been removed" % args.wallet_name) + inp = wallet_name if args.yes else ( + input("Wallet '%s' with all keys and will be removed, without private key it cannot be restored." + "\nPlease retype exact name of wallet to proceed: " % args.wallet_name)) + if str(inp) == str(wallet_name): + if wallet_delete(wallet_name, force=True, db_uri=db_uri): + print("Wallet %s has been removed" % wallet_name) else: - print("\nError when deleting wallet") + print("Error when deleting wallet") else: - print("\nSpecified wallet name incorrect") + print("Specified wallet name incorrect") # Create or open wallet elif args.wallet_name: @@ -306,7 +316,7 @@ def main(): else: print("Failed to import key") - elif args.wallet_recreate: + elif args.wallet_empty: wallet_empty(args.wallet_name) print("Removed transactions and emptied wallet. Use --update-wallet option to update again.") elif args.update_utxos: @@ -355,8 +365,8 @@ def main(): print("Signed transaction:") print_transaction(wt) elif args.send: - # if args.fee_per_kb: - # raise WalletError("Fee-per-kb option not allowed with --create-transaction") + if args.fee_per_kb: + raise WalletError("Fee-per-kb option not allowed with --send") try: wt = create_transaction(wlt, args.send, args) except WalletError as e: @@ -374,13 +384,11 @@ def main(): print("\nTransaction created but not sent yet. Transaction dictionary for export: ") print_transaction(wt) elif args.sweep: - if args.fee: - raise WalletError("Fee option not allowed with --sweep") offline = True print("Sweep wallet. Send all funds to %s" % args.sweep) if args.push: offline = False - wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb) + wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee) if not wt: raise WalletError("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) wt.info() From c105918597adbc8e14104ac3e0ddf37e502f289a Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Fri, 19 Jan 2024 20:58:07 +0100 Subject: [PATCH 044/207] Fix argument shortcuts in unittests --- tests/test_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index b8370acc..cd64fb30 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -168,7 +168,7 @@ def test_tools_clw_transaction_with_script(self): def test_tools_clw_create_litecoin_segwit_wallet(self): cmd_wlt_create = '%s %s new -w ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ - 'sword order share" -d %s -y segwit -n litecoin -r' % \ + 'sword order share" -d %s -j segwit -n litecoin -r' % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) cmd_wlt_delete = "%s %s -w ltcsw --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) @@ -191,7 +191,7 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' '7FW6wpKW' ] - cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -y p2sh-segwit -d %s" % \ + cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -j p2sh-segwit -d %s" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) From bfadb98b062272a5941e13b302cf6a9c0ef76645 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sat, 20 Jan 2024 10:47:31 +0100 Subject: [PATCH 045/207] Add quiet en interactive mode --- bitcoinlib/tools/clw.py | 167 +++++++++++++++++++++++----------------- tests/test_tools.py | 6 +- 2 files changed, 98 insertions(+), 75 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 46d33365..4f186946 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -5,10 +5,11 @@ # CLW - Command Line Wallet manager. # Create and manage BitcoinLib legacy/segwit single and multisignatures wallets from the commandline # -# © 2019 November - 1200 Web Development +# © 2019 - 2024 January - 1200 Web Development # import sys +import os import argparse import ast from pprint import pprint @@ -28,6 +29,7 @@ def exception_handler(exception_type, exception, traceback): print("%s: %s" % (exception_type.__name__, exception)) + sys.excepthook = exception_handler @@ -38,8 +40,8 @@ def parse_args(): parser.add_argument('--generate-key', '-g', action='store_true', help="Generate a new masterkey, and" " show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet") parser.add_argument('--passphrase-strength', type=int, default=128, - help="Number of bits for passphrase key. Default is 128, lower is not advised but can " - "be used for testing. Set to 256 bits for more future-proof passphrases") + help="Number of bits for passphrase key. Default is 128, lower is not advised but can " + "be used for testing. Set to 256 bits for more future-proof passphrases") parser.add_argument('--database', '-d', help="URI of the database to use",) parser.add_argument('--wallet_name', '-w', nargs='?', default='', @@ -80,19 +82,17 @@ def parse_args(): 'EXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp') parser_new.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, help='Witness type of wallet: legacy, p2sh-segwit or segwit (default)') - parser_new.add_argument('--cosigner-id', type=int, default=0, + parser_new.add_argument('--cosigner-id', '-o', type=int, default=None, help='Set this if wallet contains only public keys, more then one private key or if ' 'you would like to create keys for other cosigners.') parser_new.add_argument('--database', '-d', - help="URI of the database to use",) - parser_new.add_argument('--receive', '-r', nargs='?', type=int, - help="Show unused address to receive funds. Specify cosigner-id to generate address for " - "specific cosigner. Default is -1 for own wallet", - const=-1, metavar='COSIGNER_ID') + help="URI of the database to use",) + parser_new.add_argument('--receive', '-r', action='store_true', + help="Show unused address to receive funds.") parser_new.add_argument('--yes', '-y', action='store_true', default=False, - help='Non-interactive mode, does not prompt for confirmation') + help='Non-interactive mode, does not prompt for confirmation') parser_new.add_argument('--quiet', '-q', action='store_true', - help='Quit mode, no output writen to console.') + help='Quit mode, no output writen to console.') group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', @@ -107,10 +107,11 @@ def parse_args(): help="Delete all keys and transactions from wallet, except for the masterkey(s). " "Use when updating fails or other errors occur. Please backup your database and " "masterkeys first. Update empty wallet again to restore your wallet.") - group_wallet.add_argument('--receive', '-r', nargs='?', type=int, - help="Show unused address to receive funds. Specify cosigner-id to generate address for " - "specific cosigner. Default is -1 for own wallet", - const=-1, metavar='COSIGNER_ID') + group_wallet.add_argument('--receive', '-r', action='store_true', + help="Show unused address to receive funds.") + group_wallet.add_argument('--cosigner-id', '-o', type=int, default=None, + help='Set this if wallet contains only public keys, more then one private key or if ' + 'you would like to create keys for other cosigners.') group_wallet.add_argument('--export-private', '-e', action='store_true', help="Export private key for this wallet and exit") group_wallet.add_argument('--import-private', '-v', @@ -147,25 +148,26 @@ def parse_args(): return pa -def get_passphrase(strength, interactive=False): +def get_passphrase(strength, interactive=False, quiet=False): passphrase = Mnemonic().generate(strength) - print("Passphrase: %s" % passphrase) - print("Please write down on paper and backup. With this key you can restore your wallet and all keys") - if not interactive and input("\nType 'yes' if you understood and wrote down your key: ") not in ['yes', 'Yes', 'YES']: - print("Exiting...") - sys.exit() + if not quiet: + print("Passphrase: %s" % passphrase) + print("Please write down on paper and backup. With this key you can restore your wallet and all keys") + if not interactive and input("\nType 'yes' if you understood and wrote down your key: ") not in ['yes', 'Yes', 'YES']: + print("Exiting...") + sys.exit() return passphrase -def create_wallet(wallet_name, args, db_uri): +def create_wallet(wallet_name, args, db_uri, output_to): if args.network is None: args.network = DEFAULT_NETWORK - print("\nCREATE wallet '%s' (%s network)" % (wallet_name, args.network)) + print("\nCREATE wallet '%s' (%s network)" % (wallet_name, args.network), file=output_to) if args.create_multisig: if not isinstance(args.create_multisig, list) or len(args.create_multisig) < 2: raise WalletError("Please enter multisig creation parameter in the following format: " - " " - " [ ... ]") + " " + " [ ... ]") try: sigs_total = int(args.create_multisig[0]) except ValueError: @@ -180,9 +182,9 @@ def create_wallet(wallet_name, args, db_uri): keys_missing = sigs_total - len(key_list) assert(keys_missing >= 0) if keys_missing: - print("Not all keys provided, creating %d additional key(s)" % keys_missing) + print("Not all keys provided, creating %d additional key(s)" % keys_missing, file=output_to) for _ in range(keys_missing): - passphrase = get_passphrase(args.passphrase_strength, args.yes) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) key_list.append(HDKey.from_passphrase(passphrase, network=args.network)) return Wallet.create(wallet_name, key_list, sigs_required=sigs_required, network=args.network, cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type) @@ -240,25 +242,30 @@ def print_transaction(wt): def main(): - print("Command Line Wallet - BitcoinLib %s\n" % BITCOINLIB_VERSION) args = parse_args() db_uri = args.database + output_to = open(os.devnull, 'w') if args.quiet else sys.stdout wlt = None + print("Command Line Wallet - BitcoinLib %s\n" % BITCOINLIB_VERSION, file=output_to) + # --- General arguments --- # Generate key if args.generate_key: - passphrase = get_passphrase(args.passphrase_strength, args.yes) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) hdkey = HDKey.from_passphrase(passphrase, witness_type=args.witness_type, network=args.network) - print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) + if args.quiet: + print(passphrase) + else: + print("Private Master key, to create multisig wallet on this machine:\n%s" % hdkey.wif_private()) print("Public Master key, to share with other cosigner multisig wallets:\n%s" % - hdkey.public_master(witness_type=args.witness_type, multisig=True).wif()) - print("Network: %s" % hdkey.network.name) + hdkey.public_master(witness_type=args.witness_type, multisig=True).wif(), file=output_to) + print("Network: %s" % hdkey.network.name, file=output_to) # List wallets elif args.list_wallets: - print("BitcoinLib wallets:") + print("BitcoinLib wallets:", file=sys.stdout) wallets = wallets_list(db_uri=db_uri) if not wallets: print("No wallets defined yet, use 'new' argument to create a new wallet. See clw new --help " @@ -274,32 +281,32 @@ def main(): if args.wallet_name.isdigit(): wallet_name = int(args.wallet_name) if not wallet_exists(wallet_name, db_uri=db_uri): - print("Wallet '%s' not found" % args.wallet_name) + print("Wallet '%s' not found" % args.wallet_name, file=output_to) else: - inp = wallet_name if args.yes else ( + inp = wallet_name if (args.quiet or args.yes) else ( input("Wallet '%s' with all keys and will be removed, without private key it cannot be restored." - "\nPlease retype exact name of wallet to proceed: " % args.wallet_name)) + "\nPlease retype exact name of wallet to proceed: " % args.wallet_name)) if str(inp) == str(wallet_name): if wallet_delete(wallet_name, force=True, db_uri=db_uri): - print("Wallet %s has been removed" % wallet_name) + print("Wallet %s has been removed" % wallet_name, file=output_to) else: - print("Error when deleting wallet") + print("Error when deleting wallet", file=output_to) else: - print("Specified wallet name incorrect") + print("Specified wallet name incorrect", file=output_to) # Create or open wallet elif args.wallet_name: if args.subparser_name == 'new': if wallet_exists(args.wallet_name, db_uri=db_uri): - print("Wallet with name '%s' already exists" % args.wallet_name) + print("Wallet with name '%s' already exists" % args.wallet_name, file=output_to) else: - wlt = create_wallet(args.wallet_name, args, db_uri) + wlt = create_wallet(args.wallet_name, args, db_uri, output_to) args.wallet_info = True else: try: wlt = Wallet(args.wallet_name, db_uri=db_uri) except WalletError as e: - print("Error: %s" % e.msg) + print("Error: %s" % e.msg, file=output_to) if wlt is None: sys.exit() @@ -312,16 +319,19 @@ def main(): if args.import_private: if wlt.import_key(args.import_private): - print("Private key imported") + print("Private key imported", file=output_to) else: - print("Failed to import key") + print("Failed to import key", file=output_to) elif args.wallet_empty: wallet_empty(args.wallet_name) - print("Removed transactions and emptied wallet. Use --update-wallet option to update again.") + print("Removed transactions and emptied wallet. Use --update-wallet option to update again.", + file=output_to) elif args.update_utxos: + print("Updating wallet utxo's", file=output_to) wlt.utxos_update() elif args.update_transactions: + print("Updating wallet transactions", file=output_to) wlt.scan(scan_gap_limit=3) elif args.export_private: if wlt.scheme == 'multisig': @@ -329,7 +339,7 @@ def main(): if w.main_key and w.main_key.is_private: print(w.main_key.wif) elif not wlt.main_key or not wlt.main_key.is_private: - print("No private key available for this wallet") + print("No private key available for this wallet", file=output_to) else: print(wlt.main_key.wif) elif args.import_tx_file or args.import_tx: @@ -338,7 +348,7 @@ def main(): fn = args.import_tx_file f = open(fn, "r") except FileNotFoundError: - print("File %s not found" % args.import_tx_file) + print("File %s not found" % args.import_tx_file, file=output_to) sys.exit() try: tx_import = ast.literal_eval(f.read()) @@ -358,12 +368,16 @@ def main(): if args.push: res = wt.send() if res: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) else: - print("Error creating transaction: %s" % wt.error) + print("Error creating transaction: %s" % wt.error, file=output_to) wt.info() - print("Signed transaction:") - print_transaction(wt) + print("Signed transaction:", file=output_to) + if not args.quiet: + print_transaction(wt) elif args.send: if args.fee_per_kb: raise WalletError("Fee-per-kb option not allowed with --send") @@ -372,20 +386,24 @@ def main(): except WalletError as e: raise WalletError("Cannot create transaction: %s" % e.msg) wt.sign() - print("Transaction created") + print("Transaction created", file=output_to) wt.info() if args.push: wt.send() if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) else: - print("Error creating transaction: %s" % wt.error) + print("Error creating transaction: %s" % wt.error, file=output_to) else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") - print_transaction(wt) + print("\nTransaction created but not sent yet. Transaction dictionary for export: ", file=output_to) + if not args.quiet: + print_transaction(wt) elif args.sweep: offline = True - print("Sweep wallet. Send all funds to %s" % args.sweep) + print("Sweep wallet. Send all funds to %s" % args.sweep, file=output_to) if args.push: offline = False wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee) @@ -394,32 +412,37 @@ def main(): wt.info() if args.push: if wt.pushed: - print("Transaction pushed to network. Transaction ID: %s" % wt.txid) + if args.quiet: + print(wt.txid) + else: + print("Transaction pushed to network. Transaction ID: %s" % wt.txid) elif not wt: - print("Cannot sweep wallet, are UTXO's updated and available?") + print("Cannot sweep wallet, are UTXO's updated and available?", file=output_to) else: - print("Error sweeping wallet: %s" % wt.error) + print("Error sweeping wallet: %s" % wt.error, file=output_to) else: - print("\nTransaction created but not sent yet. Transaction dictionary for export: ") + print("\nTransaction created but not sent yet. Transaction dictionary for export: ", file=output_to) print_transaction(wt) else: - print("Please provide an argument. Use -h or --help for more information") - + print("Please provide an argument. Use -h or --help for more information", file=output_to) if args.receive and not (args.send or args.sweep): - cosigner_id = args.receive - if args.receive == -1: - cosigner_id = None - key = wlt.get_key(network=args.network, cosigner_id=cosigner_id) - print("Receive address: %s" % key.address) + key = wlt.get_key(network=args.network, cosigner_id=args.cosigner_id) + if args.quiet: + print(key.address) + else: + print("Receive address: %s" % key.address) if QRCODES_AVAILABLE: qrcode = pyqrcode.create(key.address) - print(qrcode.terminal()) + print(qrcode.terminal(), file=output_to) else: - print("Install qr code module to show QR codes: pip install pyqrcode") + print("Install qr code module to show QR codes: pip install pyqrcode", file=output_to) elif args.wallet_info: - print("Wallet info for %s" % wlt.name) - wlt.info() + print("Wallet info for %s" % wlt.name, file=output_to) + if not args.quiet: + wlt.info() + else: + pprint(wlt.as_dict()) if __name__ == '__main__': diff --git a/tests/test_tools.py b/tests/test_tools.py index cd64fb30..75b978f6 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -96,7 +96,7 @@ def test_tools_clw_create_multisig_wallet(self): 'tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJ' 'MeQHdWDp' ] - cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s" % \ + cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s -o 0" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) cmd_wlt_delete = "%s %s -w testms --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) @@ -115,7 +115,7 @@ def test_tools_clw_create_multisig_wallet_one_key(self): 'tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK' '5zNYeiX8' ] - cmd_wlt_create = "%s %s new -w testms1 -m 2 2 %s -r -n testnet -d %s" % \ + cmd_wlt_create = "%s %s new -w testms1 -m 2 2 %s -r -n testnet -d %s -o 0" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) cmd_wlt_delete = "%s %s -w testms1 --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) @@ -191,7 +191,7 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' '7FW6wpKW' ] - cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -j p2sh-segwit -d %s" % \ + cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -j p2sh-segwit -d %s -o 0" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) From 38ecfa4e264a30413eaa8fb9012304ed2d4c292d Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sat, 20 Jan 2024 15:44:55 +0100 Subject: [PATCH 046/207] Pass network and witness_type arguments to bip38 decrypt --- bitcoinlib/tools/clw.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 4f186946..76f7d9b5 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -2,8 +2,8 @@ # # BitcoinLib - Python Cryptocurrency Library # -# CLW - Command Line Wallet manager. -# Create and manage BitcoinLib legacy/segwit single and multisignatures wallets from the commandline +# CLW - Command Line Wallet manager +# Create and manage BitcoinLib legacy, segwit single and multi-signature wallets from the commandline # # © 2019 - 2024 January - 1200 Web Development # @@ -47,8 +47,6 @@ def parse_args(): parser.add_argument('--wallet_name', '-w', nargs='?', default='', help="Name of wallet to create or open. Provide wallet name or number when running wallet " "actions") - parser.add_argument('--password', - help='Password to protect private key, use to open and close wallet') parser.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") parser.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, @@ -61,16 +59,16 @@ def parse_args(): subparsers = parser.add_subparsers(required=False, dest='subparser_name') parser_new = subparsers.add_parser('new', description="Create new wallet") parser_new.add_argument('--wallet_name', '-w', nargs='?', default='', required=True, - help="Name of wallet to create or open. Provide wallet name or number when running wallet " - "actions") + help="Name of wallet to create or open. Provide wallet name or number when running wallet " + "actions") parser_new.add_argument('--password', - help='Password to protect private key, use to open and close wallet') + help='Password to protect private key, use to create a wallet with a protected key') parser_new.add_argument('--network', '-n', - help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") + help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") parser_new.add_argument('--passphrase', default=None, metavar="PASSPHRASE", help="Passphrase to recover or create a wallet. Usually 12 or 24 words") parser_new.add_argument('--create-from-key', '-c', metavar='KEY', - help="Create a new wallet from specified key") + help="Create a new wallet from specified key") parser_new.add_argument('--create-multisig', '-m', nargs='*', metavar='.', help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, KEY-1, KEY-2, ... KEY-N]' 'Specify number of signatures followed by the number of signatures required and ' @@ -195,7 +193,7 @@ def create_wallet(wallet_name, args, db_uri, output_to): if kf['format'] == 'wif_protected': if not args.password: raise WalletError("This is a WIF protected key, please provide a password with the --password argument.") - import_key, _ = HDKey._bip38_decrypt(import_key, args.password) + import_key, _ = HDKey._bip38_decrypt(import_key, args.password, args.network, args.witness_type) return Wallet.create(wallet_name, import_key, network=args.network, db_uri=db_uri, witness_type=args.witness_type) else: From aaa51fc2565deb78301602f2a8178b080018b90e Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sat, 20 Jan 2024 17:35:39 +0100 Subject: [PATCH 047/207] Specify values in Value object format, ie '0.1 mBTC' --- bitcoinlib/tools/clw.py | 2 +- bitcoinlib/values.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 76f7d9b5..61e2e6ae 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -208,7 +208,7 @@ def create_wallet(wallet_name, args, db_uri, output_to): def create_transaction(wlt, send_args, args): - output_arr = [(address, int(value)) for [address, value] in send_args] + output_arr = [(address, value) for [address, value] in send_args] return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0, input_key_id=args.input_key_id, number_of_change_outputs=args.number_of_change_outputs) diff --git a/bitcoinlib/values.py b/bitcoinlib/values.py index e1a507d3..4f59ac7d 100644 --- a/bitcoinlib/values.py +++ b/bitcoinlib/values.py @@ -34,7 +34,10 @@ def value_to_satoshi(value, network=None): :return int: """ if isinstance(value, str): - value = Value(value) + if network: + value = Value(value, network=network) + else: + value = Value(value) if isinstance(value, Value): if network and value.network != network: raise ValueError("Value uses different network (%s) then supplied network: %s" % (value.network.name, network)) From b1c80a28a94b2c64926b82715554713bde039a93 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sun, 21 Jan 2024 19:24:38 +0100 Subject: [PATCH 048/207] Add commandline wallet unittest to send and empty wallet --- bitcoinlib/tools/clw.py | 6 ++-- tests/test_tools.py | 74 +++++++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 61e2e6ae..35a87ae8 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -199,7 +199,7 @@ def create_wallet(wallet_name, args, db_uri, output_to): else: passphrase = args.passphrase if passphrase is None: - passphrase = get_passphrase(args.passphrase_strength, args.yes) + passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) if len(passphrase.split(' ')) < 3: raise WalletError("Please specify passphrase with 3 words or more. However less than 12 words is insecure!") hdkey = HDKey.from_passphrase(passphrase, network=args.network) @@ -322,7 +322,7 @@ def main(): print("Failed to import key", file=output_to) elif args.wallet_empty: - wallet_empty(args.wallet_name) + wallet_empty(args.wallet_name, args.database) print("Removed transactions and emptied wallet. Use --update-wallet option to update again.", file=output_to) elif args.update_utxos: @@ -439,8 +439,6 @@ def main(): print("Wallet info for %s" % wlt.name, file=output_to) if not args.quiet: wlt.info() - else: - pprint(wlt.as_dict()) if __name__ == '__main__': diff --git a/tests/test_tools.py b/tests/test_tools.py index 75b978f6..ed228095 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Bitcoinlib Tools -# © 2018 May - 1200 Web Development +# © 2018 - 2024 January - 1200 Web Development # import os @@ -56,22 +56,24 @@ def init_mysql(_): con.close() -db_uris = (('sqlite:///' + SQLITE_DATABASE_FILE, init_sqlite),) -if UNITTESTS_FULL_DATABASE_TEST: - db_uris += ( - # ('mysql://root@localhost:3306/' + DATABASE_NAME, init_mysql), - ('postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, init_postgresql), - ) +# db_uris = (('sqlite:///' + SQLITE_DATABASE_FILE, init_sqlite),) +# if UNITTESTS_FULL_DATABASE_TEST: +# db_uris += ( +# ('mysql://root@localhost:3306/' + DATABASE_NAME, init_mysql), +# ('postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, init_postgresql), +# ) -@parameterized_class(('DATABASE_URI', 'init_fn'), db_uris) +# @parameterized_class(('DATABASE_URI', 'init_fn'), db_uris) class TestToolsCommandLineWallet(unittest.TestCase): def setUp(self): - self.init_fn() + # self.init_fn() + init_sqlite(self) self.python_executable = sys.executable self.clw_executable = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../bitcoinlib/tools/clw.py')) + self.DATABASE_URI = SQLITE_DATABASE_FILE def test_tools_clw_create_wallet(self): cmd_wlt_create = '%s %s new -w test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ @@ -143,7 +145,7 @@ def test_tools_clw_transaction_with_script(self): (self.python_executable, self.clw_executable, self.DATABASE_URI) cmd_wlt_update = "%s %s -w test2 -x -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) - cmd_wlt_transaction = "%s %s -w test2 -d %s -s 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 50000000 -f 100000 -p" % \ + cmd_wlt_transaction = "%s %s -w test2 -d %s -s 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 0.5 -f 100000 -p" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) cmd_wlt_delete = "%s %s -w test2 --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) @@ -205,6 +207,58 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): poutput = process.communicate(input=b'testms-p2sh-segwit') self.assertIn(output_wlt_delete, normalize_string(poutput[0])) + def test_tools_generate_key_quiet(self): + cmd_generate_passphrase = "%s %s -gq --passphrase-strength 256" % \ + (self.python_executable, self.clw_executable) + process = Popen(cmd_generate_passphrase, stdin=PIPE, stdout=PIPE, shell=True) + poutput = process.communicate()[0] + self.assertEqual(len(poutput.split(b' ')), 24) + + def test_tools_wallet_create_from_key(self): + phrase = ("hover rescue clock ocean strategy post melt banner anxiety phone pink paper enhance more " + "copy gate bag brass raise logic stone duck muffin conduct") + cmd_wlt_create = "%s %s new -w wlt_from_key -c \"%s\" -d %s -y" % \ + (self.python_executable, self.clw_executable, phrase, self.DATABASE_URI) + output_wlt_create = "bc1qpylcrcyqa5wkwe2stzc6h7q0mhs5skxuas44w2" + + poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + def test_tools_wallet_send_to_multi(self): + send_str = ("-s blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz 0.1 " + "-s blt1qu825hm0a6ajg66j79x4tzkn56qmljjms97c5tp 1") + cmd_wlt_create = "%s %s new -w wallet_send_to_multi -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_update = "%s %s -w wallet_send_to_multi -d %s -x" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_send = "%s %s -w wallet_send_to_multi -d %s %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI, send_str) + + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn(b"Transaction created", process.communicate()[0]) + + def test_tools_wallet_empty(self): + pk = ("zprvAWgYBBk7JR8GiejuVoZaVXtWf5zNawFbTH88uKao9qnZxBypJQNvh1tGHZghpfjUfSUiS7G7MmNw3cyakkNcNis3MjD4ic54n" + "FY5LQxMszQ") + cmd_wlt_create = "%s %s new -w wlt_create_and_empty -c %s -d %s -y" % \ + (self.python_executable, self.clw_executable, pk, self.DATABASE_URI) + output_wlt_create = "bc1qqnqkjpnmr5zsxar76wxqcntp28ltly0fz6crdg" + poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn(output_wlt_create, normalize_string(poutput[0])) + + cmd_wlt_empty = "%s %s -w wlt_create_and_empty -d %s --wallet-empty" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + poutput = Popen(cmd_wlt_empty, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn("Removed transactions and emptied wallet", normalize_string(poutput[0])) + + cmd_wlt_info = "%s %s -w wlt_create_and_empty -d %s -i" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + poutput = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertIn("- - Transactions Account 0 (0)", normalize_string(poutput[0])) + self.assertNotIn(output_wlt_create, normalize_string(poutput[0])) + if __name__ == '__main__': unittest.main() From aee0f3ffcacc1923e7f13131808460c6b99d34b6 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 13:57:31 +0100 Subject: [PATCH 049/207] Add clw unittest for more advanced transactions --- bitcoinlib/tools/clw.py | 38 +++---------- bitcoinlib/transactions.py | 2 +- docs/index.rst | 35 ++++++++++-- tests/import_test.tx | 70 ++++++++++++++++++++++++ tests/test_tools.py | 109 ++++++++++++++++++++++++++++++++++++- 5 files changed, 215 insertions(+), 39 deletions(-) create mode 100644 tests/import_test.tx diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 35a87ae8..4559a2fa 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -70,7 +70,7 @@ def parse_args(): parser_new.add_argument('--create-from-key', '-c', metavar='KEY', help="Create a new wallet from specified key") parser_new.add_argument('--create-multisig', '-m', nargs='*', metavar='.', - help='[NUMBER_OF_SIGNATURES, NUMBER_OF_SIGNATURES_REQUIRED, KEY-1, KEY-2, ... KEY-N]' + help='[NUMBER_OF_SIGNATURES_REQUIRED, NUMBER_OF_SIGNATURES, KEY-1, KEY-2, ... KEY-N]' 'Specify number of signatures followed by the number of signatures required and ' 'then a list of public or private keys for this wallet. Private keys will be ' 'created if not provided in key list.' @@ -160,25 +160,26 @@ def get_passphrase(strength, interactive=False, quiet=False): def create_wallet(wallet_name, args, db_uri, output_to): if args.network is None: args.network = DEFAULT_NETWORK - print("\nCREATE wallet '%s' (%s network)" % (wallet_name, args.network), file=output_to) + print("CREATE wallet '%s' (%s network)" % (wallet_name, args.network), file=output_to) if args.create_multisig: if not isinstance(args.create_multisig, list) or len(args.create_multisig) < 2: raise WalletError("Please enter multisig creation parameter in the following format: " " " " [ ... ]") try: - sigs_total = int(args.create_multisig[0]) + sigs_required = int(args.create_multisig[0]) except ValueError: - raise WalletError("Number of total signatures (first argument) must be a numeric value. %s" % + raise WalletError("Number of signatures required (first argument) must be a numeric value. %s" % args.create_multisig[0]) try: - sigs_required = int(args.create_multisig[1]) + sigs_total = int(args.create_multisig[1]) except ValueError: - raise WalletError("Number of signatures required (second argument) must be a numeric value. %s" % + raise WalletError("Number of total signatures (second argument) must be a numeric value. %s" % args.create_multisig[1]) key_list = args.create_multisig[2:] keys_missing = sigs_total - len(key_list) - assert(keys_missing >= 0) + if keys_missing < 0: + raise WalletError("Invalid number of keys (%d required)" % sigs_total) if keys_missing: print("Not all keys provided, creating %d additional key(s)" % keys_missing, file=output_to) for _ in range(keys_missing): @@ -215,28 +216,7 @@ def create_transaction(wlt, send_args, args): def print_transaction(wt): - tx_dict = { - 'txid': wt.txid, - 'network': wt.network.name, - 'fee': wt.fee, - 'raw': wt.raw_hex(), - 'witness_type': wt.witness_type, - 'outputs': [{ - 'address': o.address, - 'value': o.value - } for o in wt.outputs], - 'inputs': [{ - 'prev_hash': i.prev_txid.hex(), - 'output_n': int.from_bytes(i.output_n, 'big'), - 'address': i.address, - 'signatures': [{ - 'signature': s.hex(), - 'sig_der': s.as_der_encoded(as_hex=True), - 'pub_key': s.public_key.public_hex, - } for s in i.signatures], 'value': i.value - } for i in wt.inputs] - } - pprint(tx_dict) + pprint(wt.as_dict()) def main(): diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index b9b6b752..c5f013d0 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1786,7 +1786,7 @@ def as_dict(self): def as_json(self): """ - Get current key as json formatted string + Get current transaction as json formatted string :return str: """ diff --git a/docs/index.rst b/docs/index.rst index 6fc68ca1..a8ba2359 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,16 +149,39 @@ To create a new Bitcoin wallet .. code-block:: bash - $ clw newwallet - Command Line Wallet for BitcoinLib + $ python bitcoinlib/tools/clw.py new -w newwallet + Command Line Wallet - BitcoinLib 0.6.14 - Wallet newwallet does not exist, create new wallet [yN]? y + CREATE wallet 'newwallet' (bitcoin network) + Passphrase: sibling undo gift cat garage survey taxi index admit odor surface waste + Please write down on paper and backup. With this key you can restore your wallet and all keys - CREATE wallet 'newwallet' (bitcoin network) + Type 'yes' if you understood and wrote down your key: yes + Wallet info for newwallet + === WALLET === + ID 21 + Name newwallet + Owner + Scheme bip32 + Multisig False + Witness type segwit + Main network bitcoin + Latest update None + + = Wallet Master Key = + ID 177 + Private True + Depth 0 + + - NETWORK: bitcoin - + - - Keys + 182 m/84'/0'/0'/0/0 bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv address index 0 0.00000000 ₿ + + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = - Your mnemonic private key sentence is: force humble chair kiss season ready elbow cool awake divorce famous tunnel - Please write down on paper and backup. With this key you can restore your wallet and all keys You can use the command line wallet 'clw' to create simple or multisig wallets for various networks, manage public and private keys and managing transactions. diff --git a/tests/import_test.tx b/tests/import_test.tx new file mode 100644 index 00000000..c0de7cbb --- /dev/null +++ b/tests/import_test.tx @@ -0,0 +1,70 @@ +{'block_hash': None, + 'block_height': None, + 'coinbase': False, + 'confirmations': None, + 'date': None, + 'fee': 12366, + 'fee_per_kb': 33333, + 'flag': None, + 'input_total': 100000000, + 'inputs': [{'address': '23K38iGiHEHFfHh7SSUd46RWGQNHwha9kAt', + 'compressed': True, + 'double_spend': False, + 'encoding': 'base58', + 'index_n': 0, + 'locktime_cltv': None, + 'locktime_csv': None, + 'output_n': 0, + 'prev_txid': '12821f8ac330e4eddb9f87ea29456b31ec300e232d2c63880f669a9b15e3741f', + 'public_hash': 'e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a', + 'public_keys': ['0289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e', + '0331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd3', + '03547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc89'], + 'redeemscript': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', + 'script': '', + 'script_code': '', + 'script_type': 'p2sh_multisig', + 'sequence': 4294967295, + 'signatures': ['1862f7a0b7d161954431662fb63db86247cead6dfc6b6c8b9b1c2479297ad3b50a35dd0ec056a43da44d3d04e22181ea59ad9225d2fc4541424d464622a0a6f2'], + 'sigs_required': 2, + 'sort': True, + 'unlocking_script': '', + 'unlocking_script_unsigned': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', + 'valid': None, + 'value': 100000000, + 'witness': '', + 'witness_type': 'legacy'}], + 'locktime': 0, + 'network': 'bitcoinlib_test', + 'output_total': 99987634, + 'outputs': [{'address': '23K38iGiHEHFfHh7SSUd46RWGQNHwha9kAt', + 'output_n': 0, + 'public_hash': 'e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a', + 'public_key': '', + 'script': 'a914e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a87', + 'script_type': 'p2sh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 50000000}, + {'address': '239M1DxQuxJcMHtYBdG6A81bfXQrrCNa2rr', + 'output_n': 1, + 'public_hash': '787f3d509665fd64ea9c7f2670ef9f60133290fe', + 'public_key': '', + 'script': 'a914787f3d509665fd64ea9c7f2670ef9f60133290fe87', + 'script_type': 'p2sh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 49987634}], + 'raw': '01000000011f74e3159b9a660f88632c2d230e30ec316b4529ea879fdbede430c38a1f82120000000000ffffffff0280f0fa020000000017a914e2cf42c85bb53cff8d3b75bdabb31e2c8a00cb8a8732c0fa020000000017a914787f3d509665fd64ea9c7f2670ef9f60133290fe8700000000', + 'size': 371, + 'status': 'new', + 'txhash': '', + 'txid': '2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', + 'verified': False, + 'version': 1, + 'vsize': 371, + 'witness_type': 'legacy' +} + diff --git a/tests/test_tools.py b/tests/test_tools.py index ed228095..ae0da653 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -4,7 +4,7 @@ # Unit Tests for Bitcoinlib Tools # © 2018 - 2024 January - 1200 Web Development # - +import ast import os import sys import unittest @@ -134,7 +134,7 @@ def test_tools_clw_create_multisig_wallet_one_key(self): def test_tools_clw_create_multisig_wallet_error(self): cmd_wlt_create = "%s %s new -w testms2 -m 2 a -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) - output_wlt_create = "Number of signatures required (second argument) must be a numeric value" + output_wlt_create = "Number of total signatures (second argument) must be a numeric value" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) poutput = process.communicate(input=b'y') self.assertIn(output_wlt_create, normalize_string(poutput[0])) @@ -193,7 +193,7 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): 'YprvANkMzkodih9AKQ8evAkiDWCzpQsU6N1uasNtWznNj44Y2X6FJqkv9wcfavxVEkz9qru7VKRhzmQXqy562b9Tk4JGdsaVazByzmX' '7FW6wpKW' ] - cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 3 2 %s -r -j p2sh-segwit -d %s -o 0" % \ + cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 2 3 %s -r -j p2sh-segwit -d %s -o 0" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) @@ -259,6 +259,109 @@ def test_tools_wallet_empty(self): self.assertIn("- - Transactions Account 0 (0)", normalize_string(poutput[0])) self.assertNotIn(output_wlt_create, normalize_string(poutput[0])) + def test_tools_wallet_sweep(self): + cmd_wlt_create = "%s %s new -w wlt_sweep -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_update = "%s %s -w wlt_sweep -d %s -x" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_send = "%s %s -w wlt_sweep -d %s --sweep blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz -p" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_info = "%s %s -w wlt_sweep -d %s -i" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn(b"Transaction pushed to network", process.communicate()[0]) + process = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True) + self.assertIn(b"-1.00000000 T \n\n= Balance Totals (includes unconfirmed) =\n\n\n", + process.communicate()[0]) + + def test_tools_wallet_multisig_cosigners(self): + pk1 = ('BC12Se7KL1uS2bA6QNjPAjFirwyoB8bDA3EPLMwDex7D3fZrWG4pP2zUcyEPKpgXfcoxxhZQqWX7b57MBWVxjjioNvsfvnpJVT9' + 'XWVvHtmdyowDz') + pk2 = ('BC12Se7KL1uS2bA6QQH1M6YkFGbNXoFSUavaE6EfMEmTrtSERw1JRCWf6Jj5tfoLhZopA4s2FSzqZqYTMpChvUvV9KdgtnJ1sFi' + 'B7SZVyHC31ybq') + pk3 = ('BC12Se7KL1uS2bA6QNjZ8T9CzaubwGjTH3WTaZdDB45GVwNMt26ixhgk4L8zus4NxhKWez5xj6xiT7DkpsSnD363h8WEoR7b5d2' + 'u64ec4KeCXQKg') + pub_key1 = ('BC11mYr7gRWJM1oBUFSkW8tPWVeb8bVv9kzjkjH7emfNnsSWVKLo24vopvN8vxud7VvFjYBvhCrEECC6mVTtE7imyytvkLT' + '9URKHJ3Crs1dSecKa') + pub_key2 = ('BC11mYrAhSZGc4JJYubuRSJDjbeoi2BueBjggutvkC8AMv8v2vdKT9T1Tq5VmXgnmzdb2maK5VF5fnbpZR1yt5bJRNBAgJb' + 'ZYXRnhWiS3jjHqgeZ') + pub_key3 = ('BC11mYrL5yBtMgaYxHEUg3anvLX3gcLi8hbtwbjymReCgGiP6hYifVMi96M3ejtvZpZbDvetBfbzgRxmu22ZkqP2i7yhFge' + 'mSkHp7BRhoDubrQvs') + cmd_wlt_create1 = "%s %s new -w wlt_multisig_2_3_A -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ + (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.DATABASE_URI) + Popen(cmd_wlt_create1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_create2 = "%s %s new -w wlt_multisig_2_3_B -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ + (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.DATABASE_URI) + Popen(cmd_wlt_create2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + + cmd_wlt_receive1 = "%s %s -w wlt_multisig_2_3_A -d %s -r -o 1 -q" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output1 = Popen(cmd_wlt_receive1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_receive2 = "%s %s -w wlt_multisig_2_3_B -d %s -r -o 1 -q" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output2 = Popen(cmd_wlt_receive2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + self.assertEqual(output1[0], output2[0]) + address = normalize_string(output1[0].strip(b'\n')) + + cmd_wlt_update1 = "%s %s -w wlt_multisig_2_3_A -d %s -x -o 1" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + Popen(cmd_wlt_update1, stdin=PIPE, stdout=PIPE, shell=True).communicate() + cmd_wlt_update2 = "%s %s -w wlt_multisig_2_3_B -d %s -x -o 1" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate() + + create_tx = "%s %s -w wlt_multisig_2_3_A -d %s -s %s 0.5 -o 1" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI, address) + output = Popen(create_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() + tx_dict_str = '{' + normalize_string(output[0]).split('{', 1)[1] + sign_tx = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx \"%s\"" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI, tx_dict_str) + output = Popen(sign_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() + response = normalize_string(output[0]) + self.assertIn('12821f8ac330e4eddb9f87ea29456b31ec300e232d2c63880f669a9b15e3741f', response) + self.assertIn('Signed transaction', response) + self.assertIn("'verified': True,", response) + + sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file import_test.tx" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output = Popen(sign_import_tx_file, stdin=PIPE, stdout=PIPE, shell=True).communicate() + response2 = normalize_string(output[0]) + self.assertIn('2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', response2) + self.assertIn('239M1DxQuxJcMHtYBdG6A81bfXQrrCNa2rr', response2) + self.assertIn('Signed transaction', response2) + self.assertIn("'verified': True,", response2) + + def test_tools_transaction_options(self): + cmd_wlt_create = "%s %s new -w test_tools_transaction_options -d %s -n bitcoinlib_test -yq" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_update = "%s %s -w test_tools_transaction_options -d %s -x" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + cmd_wlt_send = ("%s %s -w test_tools_transaction_options -d %s -s blt1qg7du8cs0scxccmfly7x252qurv7kwsy6rm4xr7 0.001 " + "--number-of-change-outputs 5") % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() + Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() + output = normalize_string(Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + tx_dict_str = '{' + output.split('{', 1)[1] + tx_dict = ast.literal_eval(tx_dict_str) + self.assertEqual(len(tx_dict['outputs']), 6) + self.assertTrue(tx_dict['verified']) + + cmd_wlt_update2 = "%s %s -w test_tools_transaction_options -d %s -ix" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI) + output = Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0] + output_list = [i for i in output.split(b'Keys\n')[1].split(b' ') if i != b''] + first_key_id = int(output_list[0]) + address = normalize_string(output_list[2]) + cmd_wlt_send2 = ("%s %s -w test_tools_transaction_options -d %s " + "-s blt1qdjre3yw9hnt53entkp6tflhg34y4sp999emjnk 0.5 -k %d") % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI, first_key_id) + output = normalize_string(Popen(cmd_wlt_send2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + self.assertIn(address, output) + self.assertIn("Transaction created", output) + if __name__ == '__main__': unittest.main() From e486c8e49340b4fb5fc4eab87119ce3be51f0269 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 14:41:30 +0100 Subject: [PATCH 050/207] Update commandline wallet manual --- bitcoinlib/tools/clw.py | 8 +- docs/_static/manuals.command-line-wallet.rst | 243 ++++++++++--------- tests/test_tools.py | 5 +- 3 files changed, 136 insertions(+), 120 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 4559a2fa..79a81721 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -54,7 +54,7 @@ def parse_args(): parser.add_argument('--yes', '-y', action='store_true', default=False, help='Non-interactive mode, does not prompt for confirmation') parser.add_argument('--quiet', '-q', action='store_true', - help='Quit mode, no output writen to console.') + help='Quiet mode, no output writen to console') subparsers = parser.add_subparsers(required=False, dest='subparser_name') parser_new = subparsers.add_parser('new', description="Create new wallet") @@ -62,7 +62,7 @@ def parse_args(): help="Name of wallet to create or open. Provide wallet name or number when running wallet " "actions") parser_new.add_argument('--password', - help='Password to protect private key, use to create a wallet with a protected key') + help='Password for BIP38 encrypted key. Use to create a wallet with a protected key') parser_new.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") parser_new.add_argument('--passphrase', default=None, metavar="PASSPHRASE", @@ -294,13 +294,11 @@ def main(): tx_import = None if not args.subparser_name: - if args.import_private: if wlt.import_key(args.import_private): print("Private key imported", file=output_to) else: print("Failed to import key", file=output_to) - elif args.wallet_empty: wallet_empty(args.wallet_name, args.database) print("Removed transactions and emptied wallet. Use --update-wallet option to update again.", @@ -401,8 +399,6 @@ def main(): else: print("\nTransaction created but not sent yet. Transaction dictionary for export: ", file=output_to) print_transaction(wt) - else: - print("Please provide an argument. Use -h or --help for more information", file=output_to) if args.receive and not (args.send or args.sweep): key = wlt.get_key(network=args.network, cosigner_id=args.cosigner_id) diff --git a/docs/_static/manuals.command-line-wallet.rst b/docs/_static/manuals.command-line-wallet.rst index 6f5d8643..dbae8cc9 100644 --- a/docs/_static/manuals.command-line-wallet.rst +++ b/docs/_static/manuals.command-line-wallet.rst @@ -1,16 +1,7 @@ Command Line Wallet =================== -Manage wallets from commandline. Allows you to - -* Show wallets and wallet info -* Create single and multi signature wallets -* Delete wallets -* Generate receive addresses -* Create transactions -* Import and export transactions -* Sign transactions with available private keys -* Broadcast transaction to the network +Manage Bitcoin wallets from commandline The Command Line wallet Script can be found in the tools directory. If you call the script without arguments it will show all available wallets. @@ -26,19 +17,36 @@ To create a wallet just specify an unused wallet name: .. code-block:: none - $ clw mywallet - Command Line Wallet for BitcoinLib + $ clw new -w mywallet + CREATE wallet 'newwallet' (bitcoin network) + Passphrase: sibling undo gift cat garage survey taxi index admit odor surface waste + Please write down on paper and backup. With this key you can restore your wallet and all keys - Wallet mywallet does not exist, create new wallet [yN]? y + Type 'yes' if you understood and wrote down your key: yes + Wallet info for newwallet + === WALLET === + ID 21 + Name newwallet + Owner + Scheme bip32 + Multisig False + Witness type segwit + Main network bitcoin + Latest update None - CREATE wallet 'mywallet' (bitcoin network) + = Wallet Master Key = + ID 177 + Private True + Depth 0 - Your mnemonic private key sentence is: mutual run dynamic armed brown meadow height elbow citizen put industry work + - NETWORK: bitcoin - + - - Keys + 182 m/84'/0'/0'/0/0 bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv address index 0 0.00000000 ₿ - Please write down on paper and backup. With this key you can restore your wallet and all keys + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = - Type 'yes' if you understood and wrote down your key: yes - Updating wallet Generate / show receive addresses @@ -49,10 +57,10 @@ codes on the commandline install the pyqrcode module. .. code-block:: none - $ clw mywallet -r + $ clw -w mywallet -r Command Line Wallet for BitcoinLib - Receive address is 1JMKBiiDMdjTx6rfqGumALvcRMX6DQNeG1 + Receive address is bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv Send funds / create transaction @@ -69,21 +77,22 @@ network. .. code-block:: none - $ clw -d dbtest mywallet -t 1FpBBJ2E9w9nqxHUAtQME8X4wGeAKBsKwZ 10000 + $ clw -w mywallet -d dbtest -t bc1qza24j7snqlmx7603z8qplm4rzfkr0p0mneraqv 10000 Restore wallet with passphrase ------------------------------ To restore or create a wallet with a passphrase use new wallet name and the --passphrase option. -If it's an old wallet you can recreate and scan it with the -s option. This will create new -addresses and update unspend outputs. +If it's an old wallet you can recreate and scan it with the -u / --update-transactions option. This will create new +addresses and update unspent outputs. .. code-block:: none - $ clw mywallet --passphrase "mutual run dynamic armed brown meadow height elbow citizen put industry work" - $ clw mywallet -s + $ clw new -w mywallet --passphrase "mutual run dynamic armed brown meadow height elbow citizen put industry work" + $ clw mywallet -ui +The -i / --wallet-info shows the contents of the updated wallet. Options Overview ---------------- @@ -92,90 +101,100 @@ Command Line Wallet for BitcoinLib .. code-block:: none - usage: clw.py [-h] [--wallet-remove] [--list-wallets] [--wallet-info] - [--update-utxos] [--update-transactions] - [--wallet-recreate] [--receive [NUMBER_OF_ADDRESSES]] - [--generate-key] [--export-private] - [--passphrase [PASSPHRASE [PASSPHRASE ...]]] - [--passphrase-strength PASSPHRASE_STRENGTH] - [--network NETWORK] [--database DATABASE] - [--create-from-key KEY] - [--create-multisig [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]]] - [--create-transaction [ADDRESS_1 [AMOUNT_1 ...]]] - [--sweep ADDRESS] [--fee FEE] [--fee-per-kb FEE_PER_KB] - [--push] [--import-tx TRANSACTION] - [--import-tx-file FILENAME_TRANSACTION] - [wallet_name] - - BitcoinLib CLI - - positional arguments: - wallet_name Name of wallet to create or open. Used to store your - all your wallet keys and will be printed on each paper - wallet - - optional arguments: - -h, --help show this help message and exit - - Wallet Actions: - --wallet-remove Name or ID of wallet to remove, all keys and - transactions will be deleted - --list-wallets, -l List all known wallets in BitcoinLib database - --wallet-info, -w Show wallet information - --update-utxos, -x Update unspent transaction outputs (UTXO's) for this - wallet - --update-transactions, -u - Update all transactions and UTXO's for this wallet - --wallet-recreate, -z - Delete all keys and transactions and recreate wallet, - except for the masterkey(s). Use when updating fails - or other errors occur. Please backup your database and - masterkeys first. - --receive [COSIGNER_ID], -r [COSIGNER_ID] - Show unused address to receive funds. Generate new - payment and change addresses if no unused addresses are - available. - --generate-key, -k Generate a new masterkey, and show passphrase, WIF and - public account key. Use to create multisig wallet - --export-private, -e Export private key for this wallet and exit - - Wallet Setup: - --passphrase [PASSPHRASE [PASSPHRASE ...]] - Passphrase to recover or create a wallet. Usually 12 - or 24 words - --passphrase-strength PASSPHRASE_STRENGTH - Number of bits for passphrase key. Default is 128, - lower is not adviced but can be used for testing. Set - to 256 bits for more future proof passphrases - --network NETWORK, -n NETWORK - Specify 'bitcoin', 'litecoin', 'testnet' or other - supported network - --database DATABASE, -d DATABASE - Name of specific database file to use - --create-from-key KEY, -c KEY - Create a new wallet from specified key - --create-multisig [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]], -m [NUMBER_OF_SIGNATURES_REQUIRED [KEYS ...]] - Specificy number of signatures required followed by a - list of signatures. Example: -m 2 tprv8ZgxMBicQKsPd1Q4 - 4tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5M - Cj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 tprv8ZgxMBicQKsP - eUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXi - zThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp - - Transactions: - --create-transaction [ADDRESS_1 [AMOUNT_1 ...]], -t [ADDRESS_1 [AMOUNT_1 ...]] - Create transaction. Specify address followed by - amount. Repeat for multiple outputs - --sweep ADDRESS Sweep wallet, transfer all funds to specified address - --fee FEE, -f FEE Transaction fee - --fee-per-kb FEE_PER_KB - Transaction fee in sathosis (or smallest denominator) - per kilobyte - --push, -p Push created transaction to the network - --import-tx TRANSACTION, -i TRANSACTION - Import raw transaction hash or transaction dictionary - in wallet and sign it with available key(s) - --import-tx-file FILENAME_TRANSACTION, -a FILENAME_TRANSACTION - Import transaction dictionary or raw transaction - string from specified filename and sign it with - available key(s) +usage: clw.py [-h] [--list-wallets] [--generate-key] [--passphrase-strength PASSPHRASE_STRENGTH] [--database DATABASE] [--wallet_name [WALLET_NAME]] [--network NETWORK] [--witness-type WITNESS_TYPE] [--yes] + [--quiet] [--wallet-remove] [--wallet-info] [--update-utxos] [--update-transactions] [--wallet-empty] [--receive] [--cosigner-id COSIGNER_ID] [--export-private] + [--import-private IMPORT_PRIVATE] [--send ADDRESS AMOUNT] [--number-of-change-outputs NUMBER_OF_CHANGE_OUTPUTS] [--input-key-id INPUT_KEY_ID] [--sweep ADDRESS] [--fee FEE] + [--fee-per-kb FEE_PER_KB] [--push] [--import-tx TRANSACTION] [--import-tx-file FILENAME_TRANSACTION] + {new} ... + +BitcoinLib command line wallet + +positional arguments: + {new} + +options: + -h, --help show this help message and exit + --list-wallets, -l List all known wallets in database + --generate-key, -g Generate a new masterkey, and show passphrase, WIF and public account key. Can be used to create a new (multisig) wallet + --passphrase-strength PASSPHRASE_STRENGTH + Number of bits for passphrase key. Default is 128, lower is not advised but can be used for testing. Set to 256 bits for more future-proof passphrases + --database DATABASE, -d DATABASE + URI of the database to use + --wallet_name [WALLET_NAME], -w [WALLET_NAME] + Name of wallet to create or open. Provide wallet name or number when running wallet actions + --network NETWORK, -n NETWORK + Specify 'bitcoin', 'litecoin', 'testnet' or other supported network + --witness-type WITNESS_TYPE, -j WITNESS_TYPE + Witness type of wallet: legacy, p2sh-segwit or segwit (default) + --yes, -y Non-interactive mode, does not prompt for confirmation + --quiet, -q Quiet mode, no output writen to console + +Wallet Actions: + --wallet-remove Name or ID of wallet to remove, all keys and transactions will be deleted + --wallet-info, -i Show wallet information + --update-utxos, -x Update unspent transaction outputs (UTXO's) for this wallet + --update-transactions, -u + Update all transactions and UTXO's for this wallet + --wallet-empty, -z Delete all keys and transactions from wallet, except for the masterkey(s). Use when updating fails or other errors occur. Please backup your database and masterkeys first. Update + empty wallet again to restore your wallet. + --receive, -r Show unused address to receive funds. + --cosigner-id COSIGNER_ID, -o COSIGNER_ID + Set this if wallet contains only public keys, more then one private key or if you would like to create keys for other cosigners. + --export-private, -e Export private key for this wallet and exit + --import-private IMPORT_PRIVATE, -v IMPORT_PRIVATE + Import private key in this wallet + +Transactions: + --send ADDRESS AMOUNT, -s ADDRESS AMOUNT + Create transaction to send amount to specified address. To send to multiple addresses, argument can be used multiple times. + --number-of-change-outputs NUMBER_OF_CHANGE_OUTPUTS + Number of change outputs. Default is 1, increase for more privacy or to split funds + --input-key-id INPUT_KEY_ID, -k INPUT_KEY_ID + Use to create transaction with 1 specific key ID + --sweep ADDRESS Sweep wallet, transfer all funds to specified address + --fee FEE, -f FEE Transaction fee + --fee-per-kb FEE_PER_KB, -b FEE_PER_KB + Transaction fee in satoshi per kilobyte + --push, -p Push created transaction to the network + --import-tx TRANSACTION + Import raw transaction hash or transaction dictionary in wallet and sign it with available key(s) + --import-tx-file FILENAME_TRANSACTION, -a FILENAME_TRANSACTION + Import transaction dictionary or raw transaction string from specified filename and sign it with available key(s) + + +And create new wallet options: + +.. code-block:: none + +usage: clw.py new [-h] --wallet_name [WALLET_NAME] [--password PASSWORD] [--network NETWORK] [--passphrase PASSPHRASE] [--create-from-key KEY] [--create-multisig [. ...]] [--witness-type WITNESS_TYPE] + [--cosigner-id COSIGNER_ID] [--database DATABASE] [--receive] [--yes] [--quiet] + +Create new wallet + +options: + -h, --help show this help message and exit + --wallet_name [WALLET_NAME], -w [WALLET_NAME] + Name of wallet to create or open. Provide wallet name or number when running wallet actions + --password PASSWORD Password for BIP38 encrypted key. Use to create a wallet with a protected key + --network NETWORK, -n NETWORK + Specify 'bitcoin', 'litecoin', 'testnet' or other supported network + --passphrase PASSPHRASE + Passphrase to recover or create a wallet. Usually 12 or 24 words + --create-from-key KEY, -c KEY + Create a new wallet from specified key + --create-multisig [. ...], -m [. ...] + [NUMBER_OF_SIGNATURES_REQUIRED, NUMBER_OF_SIGNATURES, KEY-1, KEY-2, ... KEY-N]Specify number of signatures followed by the number of signatures required and then a list of public or + private keys for this wallet. Private keys will be created if not provided in key list. Example, create a 2-of-2 multisig wallet and provide 1 key and create another key: -m 2 2 + tprv8ZgxMBicQKsPd1Q44tfDiZC98iYouKRC2CzjT3HGt1yYw2zuX2awTotzGAZQEAU9bi2M5MCj8iedP9MREPjUgpDEBwBgGi2C8eK5zNYeiX8 + tprv8ZgxMBicQKsPeUbMS6kswJc11zgVEXUnUZuGo3bF6bBrAg1ieFfUdPc9UHqbD5HcXizThrcKike1c4z6xHrz6MWGwy8L6YKVbgJMeQHdWDp + --witness-type WITNESS_TYPE, -j WITNESS_TYPE + Witness type of wallet: legacy, p2sh-segwit or segwit (default) + --cosigner-id COSIGNER_ID, -o COSIGNER_ID + Set this if wallet contains only public keys, more then one private key or if you would like to create keys for other cosigners. + --database DATABASE, -d DATABASE + URI of the database to use + --receive, -r Show unused address to receive funds. + --yes, -y Non-interactive mode, does not prompt for confirmation + --quiet, -q Quit mode, no output writen to console. + + diff --git a/tests/test_tools.py b/tests/test_tools.py index ae0da653..36221028 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -324,8 +324,9 @@ def test_tools_wallet_multisig_cosigners(self): self.assertIn('Signed transaction', response) self.assertIn("'verified': True,", response) - sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file import_test.tx" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'import_test.tx') + sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file %s" % \ + (self.python_executable, self.clw_executable, self.DATABASE_URI, filename) output = Popen(sign_import_tx_file, stdin=PIPE, stdout=PIPE, shell=True).communicate() response2 = normalize_string(output[0]) self.assertIn('2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', response2) From 705734d3c978b7e748466dfd907ca70ab50b6d20 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 15:50:09 +0100 Subject: [PATCH 051/207] Add example usage of multisig commandline wallet --- docs/_static/manuals.command-line-wallet.rst | 202 ++++++++++++++++++- tx.tx | 67 ++++++ 2 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 tx.tx diff --git a/docs/_static/manuals.command-line-wallet.rst b/docs/_static/manuals.command-line-wallet.rst index dbae8cc9..256aabc6 100644 --- a/docs/_static/manuals.command-line-wallet.rst +++ b/docs/_static/manuals.command-line-wallet.rst @@ -94,13 +94,204 @@ addresses and update unspent outputs. The -i / --wallet-info shows the contents of the updated wallet. + +Example: Multi-signature Bitcoinlib test wallet +----------------------------------------------- + +First we generate 2 private keys to create a 2-of-2 multisig wallet: + +.. code-block:: bash + + $ clw -g -n bitcoinlib_test -y + Command Line Wallet - BitcoinLib 0.6.14 + + Passphrase: marine kiwi great try know scan rigid indicate place gossip fault liquid + Please write down on paper and backup. With this key you can restore your wallet and all keys + + Type 'yes' if you understood and wrote down your key: yes + Private Master key, to create multisig wallet on this machine: + BC19UtECk2r9PVQYhY4yboRf92XKEnKZf9hQEd1qBqCgQ98HkBeysLPqYewcWDUuaBRSSVXCShDfmhpbtgZ33sWeGPqfwoLwamzPEcnfwLoeqfQM + Public Master key, to share with other cosigner multisig wallets: + BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM + Network: bitcoinlib_test + + $ clw -g -n bitcoinlib_test -y + Command Line Wallet - BitcoinLib 0.6.14 + + Passphrase: trumpet utility cotton couch hard shadow ivory alpha glance pear snow emerge + Please write down on paper and backup. With this key you can restore your wallet and all keys + Private Master key, to create multisig wallet on this machine: + BC19UtECk2r9PVQYhaAa8kEgBMPWHC4fJVJD48zBMMb9gSpY9LQVvQ1HhzB3Xmkm2BpiH5SyWoboiewpbeexPLsw8QBfAqMbDfet6kLhedtfQF8r + Public Master key, to share with other cosigner multisig wallets: + BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH + Network: bitcoinlib_test + +The -g / --generate-key is used to generate a private key passphrase. +With -n / --network we specify the bitcoinlib_test network. This isn't actually a network but allows us to create and +verify transactions. +The -y / --yes options, skips the required user input. +We now use 1 private and 1 public key to create a wallet. + +.. code-block:: bash + + $ clw new -w multisig-2-2 -n bitcoinlib_test -m 2 2 BC19UtECk2r9PVQYhY4yboRf92XKEnKZf9hQEd1qBqCgQ98HkBeysLPqYewcWDUuaBRSSVXCShDfmhpbtgZ33sWeGPqfwoLwamzPEcnfwLoeqfQM BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH + + Command Line Wallet - BitcoinLib 0.6.14 + + CREATE wallet 'ms22' (bitcoinlib_test network) + Wallet info for ms22 + === WALLET === + ID 22 + Name ms22 + Owner + Scheme bip32 + Multisig True + Multisig Wallet IDs 23, 24 + Cosigner ID 1 + Witness type segwit + Main network bitcoinlib_test + Latest update None + + = Multisig Public Master Keys = + 0 183 BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH bip32 cosigner + 1 186 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM bip32 main * + For main keys a private master key is available in this wallet to sign transactions. * cosigner key for this wallet + + - NETWORK: bitcoinlib_test - + - - Keys + + - - Transactions Account 0 (0) + + = Balance Totals (includes unconfirmed) = + +The multisig wallet has been created, you can view the wallet info by using the -i / --wallet-info option. Now we +generate a new receiving address with the -r / --receive option and update the unspent outputs with the +-x / --update-utxos option. + +.. code-block:: bash + + $ clw -w ms22 -r + Command Line Wallet - BitcoinLib 0.6.14 + + Receive address: blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p + Install qr code module to show QR codes: pip install pyqrcode + + $ clw -w ms22 -x + Command Line Wallet - BitcoinLib 0.6.14 + + Updating wallet utxo's + $ clw -w ms22 -i + Command Line Wallet - BitcoinLib 0.6.14 + + Wallet info for ms22 + === WALLET === + ID 22 + Name ms22 + Owner + Scheme bip32 + Multisig True + Multisig Wallet IDs 23, 24 + Cosigner ID 1 + Witness type segwit + Main network bitcoinlib_test + Latest update None + + = Multisig Public Master Keys = + 0 183 BC18rEEvE8begagfJs7kdxx1yW9tFsz7879c9vQQ2mnGbF6WSeKuBEGtmxJYLEy8rpVV9wXffbBtnL1LPKZqujPtEKzHqQeERiRybKB3DRBBoSFH bip32 cosigner + 1 186 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM bip32 main * + For main keys a private master key is available in this wallet to sign transactions. * cosigner key for this wallet + + - NETWORK: bitcoinlib_test - + - - Keys + 193 m/48'/9999999'/0'/2'/0/0 blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p Multisig Key 185/192 2.00000000 T + + - - Transactions Account 0 (2) + 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 10 1.00000000 T U + 5d0f176259ab4bc596363aa3653c44858ebeb2fd8361311966776192968e545d blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 10 1.00000000 T U + + = Balance Totals (includes unconfirmed) = + bitcoinlib_test (Account 0) 2.00000000 T + +We now have some utxo's in our wallet so we can create a transaction + +.. code-block:: bash + + $ clw -w ms22 -s blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.1 + Connected to pydev debugger (build 233.13135.95) + Command Line Wallet - BitcoinLib 0.6.14 + + Transaction created + Transaction 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613 + Date: None + Network: bitcoinlib_test + Version: 1 + Witness type: segwit + Status: new + Verified: False + Inputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 1.00000000 TST 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d 0 + segwit p2sh_multisig; sigs: 1 (2-of-2) not validated + Outputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.10000000 TST p2wsh U + - blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge 0.89993601 TST p2wsh U + Size: 192 + Vsize: 192 + Fee: 6399 + Confirmations: None + Block: None + Pushed to network: False + Wallet: ms22 + + Transaction created but not sent yet. Transaction dictionary for export: + {} + +Copy the contents of the dictionary and save it as 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613.tx + +The transaction has been created, but cannot be verified because the wallet contains only 1 private key. So we need to +create another wallet with the other private key, in real life situations this would be on another (offiline) machine. + +Below we create a new wallet, generate a receive address and update the utxo's. Finally we can import the transaction +dictionary which we be signed once imported. And as you can see the transaction has been verified now! + +.. code-block:: bash + + $ clw new -w multisig-2-2-signer2 -n bitcoinlib_test -m 2 2 BC18rEEZrakM87qWbSSUv19vnRkEFL7ZtNtGx3exB886VbeFZp6aq9JLZucYAj1EtsHKUB2mkjvafCCGaeYeUVtdFcz5xTxTTgEPCE8fDC8LcahM BC19UtECk2r9PVQYhaAa8kEgBMPWHC4fJVJD48zBMMb9gSpY9LQVvQ1HhzB3Xmkm2BpiH5SyWoboiewpbeexPLsw8QBfAqMbDfet6kLhedtfQF8r + $ clw -w multisig-2-2-signer2 -r + $ clw -w multisig-2-2-signer2 -x + $ clw -w multisig-2-2-signer2 -a tx.tx + Command Line Wallet - BitcoinLib 0.6.14 + + Transaction 3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613 + Date: None + Network: bitcoinlib_test + Version: 1 + Witness type: segwit + Status: new + Verified: True + Inputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 1.00000000 TST 7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d 0 + segwit p2sh_multisig; sigs: 2 (2-of-2) valid + Outputs + - blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p 0.10000000 TST p2wsh U + - blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge 0.89993601 TST p2wsh U + Size: 192 + Vsize: 192 + Fee: 6399 + Confirmations: None + Block: None + Pushed to network: False + Wallet: multisig-2-2-signer2 + + + Signed transaction: + {} + + Options Overview ---------------- Command Line Wallet for BitcoinLib -.. code-block:: none - usage: clw.py [-h] [--list-wallets] [--generate-key] [--passphrase-strength PASSPHRASE_STRENGTH] [--database DATABASE] [--wallet_name [WALLET_NAME]] [--network NETWORK] [--witness-type WITNESS_TYPE] [--yes] [--quiet] [--wallet-remove] [--wallet-info] [--update-utxos] [--update-transactions] [--wallet-empty] [--receive] [--cosigner-id COSIGNER_ID] [--export-private] [--import-private IMPORT_PRIVATE] [--send ADDRESS AMOUNT] [--number-of-change-outputs NUMBER_OF_CHANGE_OUTPUTS] [--input-key-id INPUT_KEY_ID] [--sweep ADDRESS] [--fee FEE] @@ -162,9 +353,8 @@ Transactions: Import transaction dictionary or raw transaction string from specified filename and sign it with available key(s) -And create new wallet options: - -.. code-block:: none +Options overview: New Wallet +---------------------------- usage: clw.py new [-h] --wallet_name [WALLET_NAME] [--password PASSWORD] [--network NETWORK] [--passphrase PASSPHRASE] [--create-from-key KEY] [--create-multisig [. ...]] [--witness-type WITNESS_TYPE] [--cosigner-id COSIGNER_ID] [--database DATABASE] [--receive] [--yes] [--quiet] @@ -195,6 +385,6 @@ options: URI of the database to use --receive, -r Show unused address to receive funds. --yes, -y Non-interactive mode, does not prompt for confirmation - --quiet, -q Quit mode, no output writen to console. + --quiet, -q Quiet mode, no output writen to console diff --git a/tx.tx b/tx.tx new file mode 100644 index 00000000..3929919d --- /dev/null +++ b/tx.tx @@ -0,0 +1,67 @@ +{'block_hash': None, + 'block_height': None, + 'coinbase': False, + 'confirmations': None, + 'date': None, + 'fee': 6399, + 'fee_per_kb': 33333, + 'flag': None, + 'input_total': 100000000, + 'inputs': [{'address': 'blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p', + 'compressed': True, + 'double_spend': False, + 'encoding': 'bech32', + 'index_n': 0, + 'locktime_cltv': None, + 'locktime_csv': None, + 'output_n': 0, + 'prev_txid': '7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d', + 'public_hash': '37342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', + 'public_keys': ['02116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd6', + '03f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa'], + 'redeemscript': '522102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd62103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa52ae', + 'script': '', + 'script_code': '2102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd6adab2103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aaac', + 'script_type': 'p2sh_multisig', + 'sequence': 4294967295, + 'signatures': ['b6418e051ccb55ec7da09f7dd132bbb0059ecd258eb96923cdfb183481c859f31fe1c8ea1c92f2ec63085a13d29b85bcd29db77d4287f3574292e09ccd3f4155'], + 'sigs_required': 2, + 'sort': True, + 'unlocking_script': '', + 'unlocking_script_unsigned': '522102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd62103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa52ae', + 'valid': None, + 'value': 100000000, + 'witness': '', + 'witness_type': 'segwit'}], + 'locktime': 0, + 'network': 'bitcoinlib_test', + 'output_total': 99993601, + 'outputs': [{'address': 'blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p', + 'output_n': 0, + 'public_hash': '37342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', + 'public_key': '', + 'script': '002037342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', + 'script_type': 'p2wsh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 10000000}, + {'address': 'blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge', + 'output_n': 1, + 'public_hash': 'cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb', + 'public_key': '', + 'script': '0020cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb', + 'script_type': 'p2wsh', + 'spending_index_n': None, + 'spending_txid': None, + 'spent': False, + 'value': 89993601}], + 'raw': '010000000001017da79c474fe078120a796f31254f5aaf95612ee36a13a5a584baf8c7e90a027b0000000000ffffffff02809698000000000022002037342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c97055625581315d0500000000220020cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb0000000000', + 'size': 192, + 'status': 'new', + 'txhash': '', + 'txid': '3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613', + 'verified': False, + 'version': 1, + 'vsize': 192, + 'witness_type': 'segwit'} From e61f66b89f9b48f0934974a180dd439fd82b43d1 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 16:13:00 +0100 Subject: [PATCH 052/207] Fix clw unittests for Windows --- tests/test_tools.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 36221028..7127a8a4 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -273,8 +273,8 @@ def test_tools_wallet_sweep(self): process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) self.assertIn(b"Transaction pushed to network", process.communicate()[0]) process = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True) - self.assertIn(b"-1.00000000 T \n\n= Balance Totals (includes unconfirmed) =\n\n\n", - process.communicate()[0]) + self.assertIn("-1.00000000 T = Balance Totals (includes unconfirmed) =", + normalize_string(process.communicate()[0]).replace('\n', '').replace('\r', '')) def test_tools_wallet_multisig_cosigners(self): pk1 = ('BC12Se7KL1uS2bA6QNjPAjFirwyoB8bDA3EPLMwDex7D3fZrWG4pP2zUcyEPKpgXfcoxxhZQqWX7b57MBWVxjjioNvsfvnpJVT9' @@ -317,7 +317,8 @@ def test_tools_wallet_multisig_cosigners(self): output = Popen(create_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() tx_dict_str = '{' + normalize_string(output[0]).split('{', 1)[1] sign_tx = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx \"%s\"" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, tx_dict_str) + (self.python_executable, self.clw_executable, self.DATABASE_URI, + tx_dict_str.replace('\r', '').replace('\n', '')) output = Popen(sign_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() response = normalize_string(output[0]) self.assertIn('12821f8ac330e4eddb9f87ea29456b31ec300e232d2c63880f669a9b15e3741f', response) @@ -346,7 +347,7 @@ def test_tools_transaction_options(self): Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() output = normalize_string(Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) tx_dict_str = '{' + output.split('{', 1)[1] - tx_dict = ast.literal_eval(tx_dict_str) + tx_dict = ast.literal_eval(tx_dict_str.replace('\r', '').replace('\n', '')) self.assertEqual(len(tx_dict['outputs']), 6) self.assertTrue(tx_dict['verified']) From 20f2bbb501e073e49ca191302e8893387364de86 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 16:35:46 +0100 Subject: [PATCH 053/207] Fix clw unittests for Windows 2 --- tests/test_tools.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 7127a8a4..733ae64b 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -353,10 +353,12 @@ def test_tools_transaction_options(self): cmd_wlt_update2 = "%s %s -w test_tools_transaction_options -d %s -ix" % \ (self.python_executable, self.clw_executable, self.DATABASE_URI) - output = Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0] - output_list = [i for i in output.split(b'Keys\n')[1].split(b' ') if i != b''] + output = normalize_string(Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) + output_list = [i for i in output.split('Keys')[1].split(' ') if i != ''] + if output_list[0] == '\n': + output_list = output_list[1:] first_key_id = int(output_list[0]) - address = normalize_string(output_list[2]) + address = output_list[2] cmd_wlt_send2 = ("%s %s -w test_tools_transaction_options -d %s " "-s blt1qdjre3yw9hnt53entkp6tflhg34y4sp999emjnk 0.5 -k %d") % \ (self.python_executable, self.clw_executable, self.DATABASE_URI, first_key_id) From 3eb37905506884a2dc3bb3e740e6b05a58ca0866 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 17:20:28 +0100 Subject: [PATCH 054/207] Remove support for Dash and Dash testnet --- bitcoinlib/data/networks.json | 50 -------------------- tests/test_keys.py | 44 +---------------- tests/test_networks.py | 9 +--- tests/test_services.py | 76 ------------------------------ tests/test_tools.py | 6 +-- tests/test_transactions.py | 55 +++++++++------------- tests/test_values.py | 6 +-- tests/test_wallets.py | 89 +++++++---------------------------- tx.tx | 67 -------------------------- 9 files changed, 47 insertions(+), 355 deletions(-) delete mode 100644 tx.tx diff --git a/bitcoinlib/data/networks.json b/bitcoinlib/data/networks.json index 0644f8ed..0948aea9 100644 --- a/bitcoinlib/data/networks.json +++ b/bitcoinlib/data/networks.json @@ -230,56 +230,6 @@ "fee_max": 1000000, "priority": 6 }, - "dash": - { - "description": "Dash Network", - "currency_name": "dash", - "currency_name_plural": "dash coins", - "currency_symbol": "DASH", - "currency_code": "DASH", - "prefix_address": "4C", - "prefix_address_p2sh": "10", - "prefix_bech32": "dash", - "prefix_wif": "CC", - "prefixes_wif": [ - ["0488B21E", "xpub", "public", false, "legacy", "p2pkh"], - ["0488B21E", "xpub", "public", true, "legacy", "p2sh"], - ["0488ADE4", "xprv", "private", false, "legacy", "p2pkh"], - ["0488ADE4", "xprv", "private", true, "legacy", "p2sh"] - ], - "bip44_cointype": 5, - "denominator": 0.00000001, - "dust_amount": 1000, - "fee_default": 2000, - "fee_min": 1000, - "fee_max": 50000, - "priority": 10 - }, - "dash_testnet": - { - "description": "Dash Testnet Network", - "currency_name": "test-dash coins", - "currency_name_plural": "test-dash", - "currency_symbol": "TDASH", - "currency_code": "tDASH", - "prefix_address": "8C", - "prefix_address_p2sh": "13", - "prefix_bech32": "tdash", - "prefix_wif": "EF", - "prefixes_wif": [ - ["043587CF", "tpub", "public", false, "legacy", "p2pkh"], - ["043587CF", "tpub", "public", true, "legacy", "p2sh"], - ["04358394", "tprv", "private", false, "legacy", "p2pkh"], - ["04358394", "tprv", "private", true, "legacy", "p2sh"] - ], - "bip44_cointype": 1, - "denominator": 0.00000001, - "dust_amount": 1000, - "fee_default": 10000, - "fee_min": 1000, - "fee_max": 50000, - "priority": 6 - }, "dogecoin": { "description": "Dogecoin", diff --git a/tests/test_keys.py b/tests/test_keys.py index 6bee6a73..68c9f1fb 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -534,7 +534,7 @@ def test_hdkey_wif_prefixes(self): k = HDKey(network=network) for witness_type in ['legacy', 'p2sh-segwit', 'segwit']: for multisig in [False, True]: - if (network[:4] == 'dash' or network[:4] == 'doge') and witness_type != 'legacy': + if (network[:4] == 'doge') and witness_type != 'legacy': break kwif = k.wif_private(witness_type=witness_type, multisig=multisig) hdkey = wif_prefix_search(kwif, witness_type=witness_type, multisig=multisig, network=network) @@ -587,7 +587,7 @@ def test_bip38_invalid_keys(self): def test_bip38_other_networks(self): if not USING_MODULE_SCRYPT: return - networks = ['testnet', 'litecoin', 'dash'] + networks = ['testnet', 'litecoin', 'dogecoin'] for network in networks: k = Key(network=network) enc_key = k.encrypt('password') @@ -747,46 +747,6 @@ def test_keys_address_p2tr_bcrt(self): encoding='bech32').address self.assertEqual(addr, 'bcrt1pq77c6jeemv8wxlsh5h5pfdq6323naua8yapte3juw9hyec83mr8sw2eggg') -class TestKeysDash(unittest.TestCase): - def test_format_wif_compressed_private_dash(self): - key = 'XH2Yndjv6Ks3XEHGaSMDhUMTAMZTTWv5nEN958Y7VMyQXBCJVQmM' - self.assertEqual('wif_compressed', get_key_format(key)['format']) - self.assertEqual(['dash'], get_key_format(key)['networks']) - - def test_format_wif_private_dash(self): - key = '7rrHic4Nzr8iMSfaSFMSXvKgTb7Sw3FHwevGsnD2vYwU5btpXRT' - self.assertEqual('wif', get_key_format(key)['format']) - self.assertEqual(['dash'], get_key_format(key)['networks']) - - def test_format_hdkey_private_dash(self): - key = 'xprv9s21ZrQH143K3D4pKs8hj46ixU3T2vPsdmfMsoYjytd15C84SoRRkXebFFb3o4j6R5srg7btramafwcfdiibf2CWqMJLEX6jL2' \ - 'YUrLR7VfS' - self.assertEqual('hdkey_private', get_key_format(key)['format']) - self.assertIn('dash', get_key_format(key)['networks']) - - def test_dash_private_key(self): - KC_DASH = Key('000ece5e695793773007ac225a21fd570aa10f64d4da7ba29e6eabb0e34aae6b', network='dash_testnet') - self.assertEqual(KC_DASH.wif(), 'cMapAmsnHr2UZ2ZCjZZfRru8dS9PLjYjTVjbnrR7suqducfQNYnX') - self.assertEqual(KC_DASH.address(), 'ya3XLrAqfHFTFEZvDno9kv3MHREzHQzQMq') - self.assertEqual(KC_DASH.public_hex, '02d092ed110b2d127c160ef1d72dc158fa96a3d32b41b9680ea6ef35e194bbc83e') - - def test_hdkey_bip44_account_dash(self): - pk = 'xprv9s21ZrQH143K3cq8ueA8GV9uv7cHqkyQGBQu8YZkAU2EXG5oSKVFeQnYK25zhHEEqqjfyTFEcV5enh6vh4tFA3FvdGuWAqPqvY' \ - 'ECNLB78mV' - k = HDKey(pk, network='dash') - self.assertEqual(k.public_master().wif(), - 'xpub6CRdroJethC3Y46tb4XuouSCiRDUBhecDR96NXUvRUysD2XgZy4AWB8mAsvMmcw9GgXvmu4BRSFj1yAdiN1K7f' - 'w9o96T41hLJRLpLGLJrxY') - - def test_hdkey_dash(self): - k = HDKey('xprv9s21ZrQH143K4EGnYMHVxNp8JgqXCyywC3CGTrSzSudH3iRgC1gPTYgce4xamXMnyDAX8Qv8tvuW1LEgkZSrXiC25LqTJN' - '8RpCKS5ixcQWD', network='dash') - self.assertEqual('XkQ9Vudjgq62pvuG9K7pknVbiViZzZjWkJ', k.child_public(0).address()) - self.assertEqual('XtqfKEcdtn1QioGRie41uP79gGC6yPzmnz', k.child_public(100).address()) - self.assertEqual('XEYoxQJvhuXCXMpUFjf9knkJrFeE3mYp9mbFXG6mR3EK2Vvzi8vA', k.child_private(6).wif_key()) - self.assertEqual('xprv9wZJLyzHEFzD3w3uazhGhbytbsVbrHQ5Spc7qkuwsPqUQo2VTxhpyoYRGD7o1T4AKZkfjGrWHtHrS4GUkBxzUH' - 'ozuqu8c2n3d7sjbmyPdFC', str(k.subkey_for_path('3H/1').wif(is_private=True))) - class TestKeysSignatures(unittest.TestCase): diff --git a/tests/test_networks.py b/tests/test_networks.py index 31be4f1c..5fe3a247 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -25,13 +25,8 @@ def test_networks_prefix_hdkey_wif(self): network = Network('bitcoin') self.assertEqual(network.wif_prefix(is_private=True), b'\x04\x88\xad\xe4') self.assertEqual(network.wif_prefix(is_private=False), b'\x04\x88\xb2\x1e') - self.assertRaisesRegex(NetworkError, "WIF Prefix for script type p2wpkh not found", Network('dash').wif_prefix, - witness_type='segwit') def test_networks_print_value(self): - network = Network('dash') - self.assertEqual(network.print_value(10000), '0.00010000 DASH') - self.assertEqual(print_value(123, rep='symbol', denominator=0.001), '0.00123 m₿') self.assertEqual(print_value(123, denominator=1e-6), '1.23 µBTC') self.assertEqual(print_value(1e+14, network='dogecoin', denominator=1e+6, decimals=0), '1 MDOGE') @@ -40,7 +35,7 @@ def test_networks_print_value(self): def test_networks_network_value_for(self): prefixes = network_values_for('prefix_wif') - expected_prefixes = [b'\xb0', b'\xef', b'\x99', b'\x80', b'\xcc'] + expected_prefixes = [b'\xb0', b'\xef', b'\x99', b'\x80'] for expected in expected_prefixes: self.assertIn(expected, prefixes) self.assertEqual(network_values_for('denominator')[0], 1e-8) @@ -69,7 +64,7 @@ def test_network_dunders(self): self.assertFalse(n1 == n2) self.assertTrue(n1 == 'bitcoin') self.assertFalse(n2 == 'bitcoin') - self.assertTrue(n1 != 'dash') + self.assertTrue(n1 != 'dogecoin') self.assertEqual(str(n1), "") self.assertTrue(hash(n1)) diff --git a/tests/test_services.py b/tests/test_services.py index edd09284..cfc6409f 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -94,15 +94,6 @@ def test_service_transaction_get_raw_litecoin(self): '1976a914c1b1668730f13dd1772977e8ce96e3f5f78d290388ac00000000' self.assertEqual(raw_tx, ServiceTest(network='litecoin').getrawtransaction(tx_id)) - # FIXME: Disabled for now, too many broken dash service providers - # def test_service_transaction_get_raw_dash(self): - # tx_id = '885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf' - # raw_tx = '0100000001edfbcd24cd10350844061d62d03be6f3ed9c28b26b0b8082539c5d29454f7cb3010000006b483045022100e' \ - # '87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b022062f1cc0f33d036c1c60a7d561de060' \ - # '67528fffca52292d803b75e53f7dfbf63d0121028bd465d7eb03bbee946c3a277ad1b331f78add78c6723eed00097520e' \ - # 'dc21ed2ffffffff0200f90295000000001976a914de4b569d39f05bfc43f56a1b22d7783a7d0661d488aca0fc7c040000' \ - # '00001976a9141495ac5ca428a17197c7cb5065614d8eabfcf8cb88ac00000000' - # self.assertEqual(raw_tx, ServiceTest(network='dash').getrawtransaction(tx_id)) def test_service_sendrawtransaction(self): raw_tx = \ @@ -430,46 +421,6 @@ def test_service_gettransaction(self): self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, ['block_hash', 'block_height', 'spent', 'value']) - # FIXME: Disabled, not enough working providers - # def test_service_gettransaction_dash(self): - # expected_dict = {'block_hash': '000000000000002eddff510f4f6c61243e350102c58bdf8c986430b405ce7a22', - # 'network': 'dash', 'input_total': 2575500000, 'fee_per_kb': None, 'outputs': [ - # {'public_key_hash': 'de4b569d39f05bfc43f56a1b22d7783a7d0661d4', 'output_n': 0, 'spent': True, - # 'public_key': '', 'address': 'XvxE6SRkZMbhBW34QfrgxPqcNmgTsRvyeJ', 'script_type': 'p2pkh', - # 'script': '76a914de4b569d39f05bfc43f56a1b22d7783a7d0661d488ac', 'value': 2500000000}, - # {'public_key_hash': '1495ac5ca428a17197c7cb5065614d8eabfcf8cb', 'output_n': 1, 'spent': True, - # 'public_key': '', 'address': 'XcZgeaA4cwUqBqtKUPfZHUme8a5G3gA8LC', 'script_type': 'p2pkh', - # 'script': '76a9141495ac5ca428a17197c7cb5065614d8eabfcf8cb88ac', 'value': 75300000}], - # 'output_total': 2575300000, 'block_height': 900147, 'locktime': 0, 'flag': None, - # 'coinbase': False, - # 'status': 'confirmed', 'version': 1, - # 'hash': '885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf', 'size': 226, - # 'fee': 200000, 'inputs': [ - # {'redeemscript': '', 'address': 'XczHdW9k4Kg9mu6AdJayJ1PJtfX3Z9wYxm', 'double_spend': False, - # 'sequence': 4294967295, - # 'prev_txid': 'b37c4f45295d9c5382800b6bb2289cedf3e63bd0621d0644083510cd24cdfbed', 'output_n': 1, - # 'signatures': [ - # 'e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b62f1cc0f33d036c1c60a7d561de0' - # '6067528fffca52292d803b75e53f7dfbf63d', - # 'e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b62f1cc0f33d036c1c60a7d561de0' - # '6067528fffca52292d803b75e53f7dfbf63d'], - # 'public_key': '028bd465d7eb03bbee946c3a277ad1b331f78add78c6723eed00097520edc21ed2', 'index_n': 0, - # 'script_type': 'sig_pubkey', - # 'script': '483045022100e87b6a6dff07d1b91d12f530992cf8fa9f26a541af525337bbbc5c954cbf072b022062f1cc' - # '0f33d036c1c60a7d561de06067528fffca52292d803b75e53f7dfbf63d0121028bd465d7eb03bbee946c3a' - # '277ad1b331f78add78c6723eed00097520edc21ed2', - # 'value': 2575500000}], 'date': datetime(2018, 7, 8, 21, 35, 58)} - # - # srv = ServiceTest(network='dash', min_providers=3) - # - # # Get transactions by hash - # srv.gettransaction('885042c885dc0d44167ce71ce82bb28b09bdd8445b7639ea96a5f5be8ceba4cf') - # for provider in srv.results: - # print("Comparing provider %s" % provider) - # self.assertTrue(srv.results[provider].verify()) - # self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, - # ['block_hash', 'block_height', 'spent', 'value']) - def test_service_gettransactions_litecoin(self): txid = '832518d58e9678bcdb9fe0e417a138daeb880c3a2ee1fb1659f1179efc383c25' address = 'Lct7CEpiN7e72rUXmYucuhqnCy5F5Vc6Vg' @@ -672,16 +623,6 @@ def test_service_blockcount(self): msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) n_blocks = srv.results[provider] - # FIXME: Disabled, not enough working providers - # # Test Dash network - # srv = ServiceTest(min_providers=3, network='dash') - # n_blocks = None - # for provider in srv.results: - # if n_blocks is not None: - # self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - # msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - # n_blocks = srv.results[provider] - def test_service_max_providers(self): srv = ServiceTest(max_providers=1, cache_uri='') srv._blockcount = None @@ -707,23 +648,6 @@ def test_service_mempool(self): # print("Mempool: Comparing ltc provider %s" % provider) self.assertListEqual(srv.results[provider], []) - # FIXME: Disabled, not enough working providers - # txid = '15641a37e21a0cf7611a1633954be645512f1ab725a0d5077a9ad0aa0ca20bed' - # srv = ServiceTest(min_providers=3, network='dash') - # srv.mempool(txid) - # for provider in srv.results: - # # print("Mempool: Comparing dash provider %s" % provider) - # self.assertListEqual(srv.results[provider], []) - - # FIXME: Disabled, not enough working providers - # def test_service_dash(self): - # srv = ServiceTest(network='dash') - # address = 'XoLTipv6ryWECYu94vbkmDjntAXqNgouTW' - # txid = 'f770f05d2b1c63b71b2650227252da06ef226661982c4ee9b136b64f77bbbd0c' - # self.assertGreaterEqual(srv.getbalance(address), 50000000000) - # self.assertEqual(srv.getutxos(address)[0]['txid'], txid) - # self.assertEqual(srv.gettransactions(address)[0].txid, txid) - def test_service_getblock_id(self): srv = ServiceTest(min_providers=3, timeout=TIMEOUT_TEST, cache_uri='') srv.getblock('0000000000000a3290f20e75860d505ce0e948a1d1d846bec7e39015d242884b', parse_transactions=False) diff --git a/tests/test_tools.py b/tests/test_tools.py index 733ae64b..6eb62a22 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -355,10 +355,8 @@ def test_tools_transaction_options(self): (self.python_executable, self.clw_executable, self.DATABASE_URI) output = normalize_string(Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) output_list = [i for i in output.split('Keys')[1].split(' ') if i != ''] - if output_list[0] == '\n': - output_list = output_list[1:] - first_key_id = int(output_list[0]) - address = output_list[2] + first_key_id = int(output_list[1]) + address = output_list[3] cmd_wlt_send2 = ("%s %s -w test_tools_transaction_options -d %s " "-s blt1qdjre3yw9hnt53entkp6tflhg34y4sp999emjnk 0.5 -k %d") % \ (self.python_executable, self.clw_executable, self.DATABASE_URI, first_key_id) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 17eba5ef..2a278c87 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -71,15 +71,6 @@ def test_transaction_input_add_public_key(self): ti = Input(prev_txid=ph, output_n=1, keys=k.public(), compressed=k.compressed) self.assertEqual('16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM', ti.keys[0].address()) - def test_transaction_input_with_pkh(self): - ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='dash_testnet', compressed=False) - prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" - output_n = 0 - ki_public_hash = ki.hash160 - ti = Input(prev_txid=prev_tx, output_n=output_n, public_hash=ki_public_hash, network='dash_testnet', - compressed=False) - self.assertEqual(ti.address, 'yWut2kHY6nXbpgqatMCNkwsxoYHcpWeF6Q') - def test_transaction_input_locking_script(self): ph = "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48" ti = Input(ph, 0, unlocking_script_unsigned='76a91423e102597c4a99516f851406f935a6e634dbccec88ac') @@ -103,6 +94,16 @@ def test_transaction_hash_type(self): self.assertTrue(t.verify()) self.assertEqual(t.inputs[0].hash_type, 0x81) + def test_transaction_input_with_pkh(self): + ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='bitcoin', + compressed=False) + prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" + output_n = 0 + ki_public_hash = ki.hash160 + ti = Input(prev_txid=prev_tx, output_n=output_n, public_hash=ki_public_hash, network='bitcoin', + compressed = False) + self.assertEqual(ti.address, '1BbSBYZChXewL1KTTcZksPmpgvDZH93wtt') + # TODO: Move and rewrite # def test_transaction_input_locktime(self): # rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044' \ @@ -1085,16 +1086,6 @@ def test_transaction_script_type_string(self): '028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd63676d0e OP_3 OP_CHECKMULTISIG' self.assertEqual(script_to_string(script), script_string) - def test_transaction_sign_uncompressed(self): - ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', network='dash_testnet', compressed=False) - prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" - output_n = 0 - t = Transaction(network='dash_testnet') - t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False) - t.add_output(99900000, 'yUV8W2RmEbKZD8oD7YMeBNiydHWmormCDj') - t.sign(ki.private_byte) - self.assertTrue(t.verify()) - def test_transaction_p2pk_script(self): rawtx = '0100000001db1a1774240cb1bd39d6cd6df0c57d5624fd2bd25b8b1be471714ab00e1a8b5d00000000484730440220592ce8' \ '5d3b79509499c9832699c591fc0fd92208bfe20c67d655497c388b3cc50220134e367276b285c35692bcfc832afdc5c27729' \ @@ -1105,6 +1096,17 @@ def test_transaction_p2pk_script(self): self.assertEqual(t.inputs[0].script_type, 'signature') self.assertEqual(t.outputs[0].script_type, 'p2pk') + def test_transaction_sign_uncompressed(self): + ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', + compressed=False) + prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" + output_n = 0 + t = Transaction() + t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False) + t.add_output(99900000, '1EHmhQH4HjJF7e4tyX61PVzzVevRJfsPMg') + t.sign(ki.private_byte) + self.assertTrue(t.verify()) + def test_transaction_sign_p2pk(self): wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ 'nQETYvZu3j' @@ -1448,21 +1450,6 @@ def test_transaction_multisig_litecoin(self): t.sign(pk3) self.assertTrue(t.verify()) - def test_transaction_multisig_dash(self): - network = 'dash' - pk1 = HDKey(network=network) - pk2 = HDKey(network=network) - pk3 = HDKey(network=network) - t = Transaction(network=network) - t.add_input(self.utxo_prev_tx, self.utxo_output_n, - [pk1.public_byte, pk2.public_byte, pk3.public_byte], - script_type='p2sh_multisig', sigs_required=2) - t.add_output(100000, 'XwZcTpBnRRURenL7Jh9Z52XGTx1jhvecUt') - t.sign(pk1) - self.assertFalse(t.verify()) - t.sign(pk3) - self.assertTrue(t.verify()) - def test_transaction_multisig_same_sigs_for_keys(self): traw = '0100000001b4397ffe208657210147a452ca85f9a2c934f6be09a81fb19b6eb9b10310053501000000fdfe0000483045022' \ '100acff5e244831a294909567601e8851533c17cc8692201f4ee056920a522dbc050220730cdc85757564e0bacbe9ecf3f2' \ diff --git a/tests/test_values.py b/tests/test_values.py index b7b4a7fa..b0d50430 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -32,8 +32,6 @@ def test_value_class(self): self.assertEqual(str(Value('10')), '10.00000000 BTC') self.assertEqual(str(Value('10 ltc')), '10.00000000 LTC') self.assertEqual(str(Value('10', network='litecoin')), '10.00000000 LTC') - self.assertEqual(str(Value('10', network='dash_testnet')), '10.00000000 tDASH') - self.assertEqual(str(Value('10 tDASH')), '10.00000000 tDASH') self.assertEqual(float(Value('0.001 BTC')), 0.001) self.assertEqual(float(Value('1 msat')), 0.00000000001) self.assertEqual(int(Value('1 BTC')), 1) @@ -102,7 +100,7 @@ def test_value_class_str(self): self.assertEqual(Value('0.00021 YBTC').str(1), '210000000000000000000.00000000 BTC') self.assertEqual(Value('127127504620 Doge').str('TDoge'), '0.12712750 TDOGE') self.assertRaisesRegex(ValueError, "Denominator not found in NETWORK_DENOMINATORS definition", - Value('123 Dash').str, 'DD') + Value('123 Doge').str, 'DD') def test_value_class_str_auto(self): self.assertEqual(Value('1000000 sat').str('auto'), '0.01000000 BTC') @@ -139,7 +137,7 @@ def test_value_operators_comparison(self): self.assertTrue(v3 == '1000.00000 mBTC') self.assertTrue(v3 == '1 BTC') self.assertTrue(v3 == '100000000 sat') - self.assertFalse(v3 == '1 dash') + self.assertFalse(v3 == '1 doge') def test_value_operators_arithmetic(self): value1 = Value('3 BTC') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index acf40a42..7a0a6563 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -273,9 +273,9 @@ def test_wallet_create_errors(self): Wallet.create, 'test_wallet_create_errors_multisig3', keys=[HDKey(), HDKey()], sigs_required=3, db_uri=self.DATABASE_URI) self.assertRaisesRegex(WalletError, - "Network from key \(dash\) is different then specified network \(bitcoin\)", + "Network from key \(litecoin\) is different then specified network \(bitcoin\)", Wallet.create, 'test_wallet_create_errors_multisig4', - keys=[HDKey(), HDKey(network='dash')], db_uri=self.DATABASE_URI) + keys=[HDKey(), HDKey(network='litecoin')], db_uri=self.DATABASE_URI) self.assertRaisesRegex(WalletError, "Invalid key or address: zwqrC7h9pRj7SBhLRDG4FnkNBRQgene3y3", Wallet.create, 'test_wallet_create_errors4', keys='zwqrC7h9pRj7SBhLRDG4FnkNBRQgene3y3', db_uri=self.DATABASE_URI) @@ -286,9 +286,6 @@ def test_wallet_create_errors(self): self.assertRaisesRegex(WalletError, "Invalid key or address", Wallet.create, 'test_wallet_create_errors5', keys=k, network='bitcoin', db_uri=self.DATABASE_URI) - self.assertRaisesRegex(WalletError, "Segwit is not supported for Dash wallets", - Wallet.create, 'test_wallet_create_errors6', keys=HDKey(network='dash'), - witness_type='segwit', db_uri=self.DATABASE_URI) k = HDKey().subkey_for_path('m/1/2/3/4/5/6/7') self.assertRaisesRegex(WalletError, "Depth of provided public master key 7 does not correspond with key path", Wallet.create, 'test_wallet_create_errors7', keys=k, @@ -686,17 +683,17 @@ def test_wallet_private_parts(self): self.assertFalse(str(secret2) in w_json) def test_wallet_key_create_from_key(self): - k1 = HDKey(network='dash') - k2 = HDKey(network='dash') + k1 = HDKey(network='testnet') + k2 = HDKey(network='testnet') w1 = Wallet.create('network_mixup_test_wallet', network='litecoin', db_uri=self.DATABASE_URI) wk1 = WalletKey.from_key('key1', w1.wallet_id, w1._session, key=k1.address_obj) - self.assertEqual(wk1.network.name, 'dash') + self.assertEqual(wk1.network.name, 'testnet') self.assertRaisesRegex(WalletError, "Specified network and key network should be the same", WalletKey.from_key, 'key2', w1.wallet_id, w1._session, key=k2.address_obj, network='bitcoin') w2 = Wallet.create('network_mixup_test_wallet2', network='litecoin', db_uri=self.DATABASE_URI) wk2 = WalletKey.from_key('key1', w2.wallet_id, w2._session, key=k1) - self.assertEqual(wk2.network.name, 'dash') + self.assertEqual(wk2.network.name, 'testnet') self.assertRaisesRegex(WalletError, "Specified network and key network should be the same", WalletKey.from_key, 'key2', w2.wallet_id, w2._session, key=k2, network='bitcoin') @@ -789,29 +786,31 @@ def setUpClass(cls): cls.pk = 'xprv9s21ZrQH143K4478MENLXpSXSvJSRYsjD2G3sY7s5sxAGubyujDmt9Qzfd1me5s1HokWGGKW9Uft8eB9dqryybAcFZ5JAs' \ 'rg84jAVYwKJ8c' cls.wallet = Wallet.create( - keys=cls.pk, network='dash', witness_type='legacy', + keys=cls.pk, network='dogecoin', witness_type='legacy', name='test_wallet_multicurrency', db_uri=cls.DATABASE_URI) cls.wallet.new_account(network='litecoin') cls.wallet.new_account(network='bitcoin') cls.wallet.new_account(network='testnet') - cls.wallet.new_account(network='dash') + cls.wallet.new_account(network='dogecoin') cls.wallet.new_key() cls.wallet.new_key() cls.wallet.new_key(network='bitcoin') def test_wallet_multiple_networks_defined(self): - networks_expected = sorted(['litecoin', 'bitcoin', 'dash', 'testnet']) + networks_expected = sorted(['litecoin', 'bitcoin', 'dogecoin', 'testnet']) networks_wlt = sorted([x.name for x in self.wallet.networks()]) self.assertListEqual(networks_wlt, networks_expected, msg="Not all network are defined correctly for this wallet") def test_wallet_multiple_networks_default_addresses(self): - addresses_expected = ['XqTpf6NYrrckvsauJKfHFBzZaD9wRHjQtv', 'Xamqfy4y21pXMUP8x8TeTPWCNzsKrhSfau', - 'XugknDhBtJFvfErjaobizCa8KAEDVU7bCJ', 'Xj6tV9Jc3qJ2AszpNxvEq7KVQKUMcfmBqH', - 'XgkpZbqaRsRb2e2BC1QsWxTDEfW6JVpP6r'] - self.assertListEqual(self.wallet.addresslist(network='dash'), addresses_expected) + addresses_expected = ['D5RuWXkLEWavHvFBanskaP2LFKTYg6J6fU', + 'DHUXe7QJfCo1gewXHsLyBB98zd6quWyxEK', + 'DSaM5oJ7rRbrVcSYuTc5KE21paw9kWqLf7', + 'DToob5uhE3hCMaCZxYd4S5eunFgr5f8XhD', + 'DAPKhNHuidSyzhBypVdnc5fRY3pcvihLgs'] + self.assertListEqual(self.wallet.addresslist(network='dogecoin'), addresses_expected) def test_wallet_multiple_networks_import_key(self): pk_bitcoin = 'xprv9s21ZrQH143K3RBvuNbSwpAHxXuPNWMMPfpjuX6ciwo91HpYq6gDLjZuyrQCPpo4qBDXyvftN7MdX7SBVXeGgHs' \ @@ -827,11 +826,11 @@ def test_wallet_multiple_networks_import_key_network(self): self.assertIn(address_ltc, addresses_ltc_in_wallet) def test_wallet_multiple_networks_import_error(self): - pk_dashtest = ('BC19UtECk2r9PVQYhZYzX3m4arsu6tCL5VMpKPbeGpZdpzd9FHweoSRreTFKo96FAEFsUWBrASfKussgoxTrNQfm' - 'jWFrVraLbiHf4gCkUvwHEocA') + pk_test = ('BC19UtECk2r9PVQYhZYzX3m4arsu6tCL5VMpKPbeGpZdpzd9FHweoSRreTFKo96FAEFsUWBrASfKussgoxTrNQfm' + 'jWFrVraLbiHf4gCkUvwHEocA') error_str = "Network bitcoinlib_test not available in this wallet, please create an account for this network " \ "first." - self.assertRaisesRegex(WalletError, error_str, self.wallet.import_key, pk_dashtest) + self.assertRaisesRegex(WalletError, error_str, self.wallet.import_key, pk_test) def test_wallet_multiple_networks_value(self): pk = 'vprv9DMUxX4ShgxMM1FFB24BgXE3fMYXKicceSdMUtfhyyUzKNkCvPeYrcoZpPezahBEzFc23yHTPj46eqx3jKuQpQFq5kbd2oxDysd' \ @@ -2260,58 +2259,6 @@ def test_wallet_transactions_pagination(self): self.assertEqual(txs_all[10], txs_page[0]) self.assertEqual(txs_all[11], txs_page[1]) - -@parameterized_class(*params) -class TestWalletDash(TestWalletMixin, unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.db_remove() - - def test_wallet_create_with_passphrase_dash(self): - passphrase = "always reward element perfect chunk father margin slab pond suffer episode deposit" - wlt = Wallet.create("wallet-passphrase-dash", keys=passphrase, network='dash', witness_type='legacy', - db_uri=self.DATABASE_URI) - keys = wlt.get_keys(number_of_keys=5) - self.assertEqual(keys[4].address, "XhxXcRvTm4yZZzbH4MYz2udkdHWEMMf9GM") - - def test_wallet_import_dash(self): - accountkey = 'xprv9yQgG6Z38AXWuhkxScDCkLzThWWZgDKHKinMHUAPTH1uihrBWQw99sWBsN2HMpzeTze1YEYb8acT1x7sHKhXX8AbT' \ - 'GNf8tdbycySUi2fRaa' - wallet = Wallet.create( - db_uri=self.DATABASE_URI, - name='test_wallet_import_dash', - keys=accountkey, - network='dash') - newkey = wallet.get_key() - self.assertEqual(wallet.main_key.wif, accountkey) - self.assertEqual(newkey.address, u'XtVa6s1rqo9BNXir1tb6KEXsj5NGogp1QB') - self.assertEqual(newkey.path, "M/0/0") - - def test_wallet_multisig_dash(self): - network = 'dash' - pk1 = HDKey(network=network, witness_type='legacy') - pk2 = HDKey(network=network, witness_type='legacy') - wl1 = Wallet.create('multisig_test_wallet1', [pk1, pk2.public_master(multisig=True)], sigs_required=2, - db_uri=self.DATABASE_URI) - wl2 = Wallet.create('multisig_test_wallet2', [pk1.public_master(multisig=True), pk2], sigs_required=2, - db_uri=self.DATABASE_URI) - wl1_key = wl1.new_key() - wl2_key = wl2.new_key(cosigner_id=wl1.cosigner_id) - self.assertEqual(wl1_key.address, wl2_key.address) - - def test_wallet_import_private_for_known_public_multisig_dash(self): - network = 'dash' - pk1 = HDKey(network=network, witness_type='legacy') - pk2 = HDKey(network=network, witness_type='legacy') - pk3 = HDKey(network=network, witness_type='legacy') - with wallet_create_or_open("mstest_dash", [pk1.public_master(multisig=True), pk2.public_master(multisig=True), - pk3.public_master(multisig=True)], 2, network=network, - sort_keys=False, cosigner_id=0, db_uri=self.DATABASE_URI) as wlt: - self.assertFalse(wlt.cosigner[1].main_key.is_private) - wlt.import_key(pk2) - self.assertTrue(wlt.cosigner[1].main_key.is_private) - def test_wallet_merge_transactions(self): w = wallet_create_or_open('wallet_merge_transactions_tests', network='bitcoinlib_test', db_uri=self.DATABASE_URI) diff --git a/tx.tx b/tx.tx deleted file mode 100644 index 3929919d..00000000 --- a/tx.tx +++ /dev/null @@ -1,67 +0,0 @@ -{'block_hash': None, - 'block_height': None, - 'coinbase': False, - 'confirmations': None, - 'date': None, - 'fee': 6399, - 'fee_per_kb': 33333, - 'flag': None, - 'input_total': 100000000, - 'inputs': [{'address': 'blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p', - 'compressed': True, - 'double_spend': False, - 'encoding': 'bech32', - 'index_n': 0, - 'locktime_cltv': None, - 'locktime_csv': None, - 'output_n': 0, - 'prev_txid': '7b020ae9c7f8ba84a5a5136ae32e6195af5a4f25316f790a1278e04f479ca77d', - 'public_hash': '37342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', - 'public_keys': ['02116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd6', - '03f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa'], - 'redeemscript': '522102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd62103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa52ae', - 'script': '', - 'script_code': '2102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd6adab2103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aaac', - 'script_type': 'p2sh_multisig', - 'sequence': 4294967295, - 'signatures': ['b6418e051ccb55ec7da09f7dd132bbb0059ecd258eb96923cdfb183481c859f31fe1c8ea1c92f2ec63085a13d29b85bcd29db77d4287f3574292e09ccd3f4155'], - 'sigs_required': 2, - 'sort': True, - 'unlocking_script': '', - 'unlocking_script_unsigned': '522102116da7d63ef65d15f9712fa19d88c1e2a9b9e3127bcc877bb53a36f6f7e1bdd62103f0b62e4d24c28fbe2147522d0c3186f161814393369e14717532881c9bd046aa52ae', - 'valid': None, - 'value': 100000000, - 'witness': '', - 'witness_type': 'segwit'}], - 'locktime': 0, - 'network': 'bitcoinlib_test', - 'output_total': 99993601, - 'outputs': [{'address': 'blt1qxu6z7evkrmz5s7sk63dr0u3h9xsf2j2vys88reg75cjvjuz4vf2srkxp7p', - 'output_n': 0, - 'public_hash': '37342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', - 'public_key': '', - 'script': '002037342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c970556255', - 'script_type': 'p2wsh', - 'spending_index_n': None, - 'spending_txid': None, - 'spent': False, - 'value': 10000000}, - {'address': 'blt1qe4tr993nftagprtapclxrm7ahrcvl4w0dnxfnhz2cx6pjaeg989syy9zge', - 'output_n': 1, - 'public_hash': 'cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb', - 'public_key': '', - 'script': '0020cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb', - 'script_type': 'p2wsh', - 'spending_index_n': None, - 'spending_txid': None, - 'spent': False, - 'value': 89993601}], - 'raw': '010000000001017da79c474fe078120a796f31254f5aaf95612ee36a13a5a584baf8c7e90a027b0000000000ffffffff02809698000000000022002037342f65961ec5487a16d45a37f23729a095494c240e71e51ea624c97055625581315d0500000000220020cd563296334afa808d7d0e3e61efddb8f0cfd5cf6ccc99dc4ac1b419772829cb0000000000', - 'size': 192, - 'status': 'new', - 'txhash': '', - 'txid': '3b96f493d189667565271041abbc0efbd8631bb54d76decb90e144bb145fa613', - 'verified': False, - 'version': 1, - 'vsize': 192, - 'witness_type': 'segwit'} From 38f79cac144c0fb97bc786b9d7a681cc80e7a831 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 19:34:11 +0100 Subject: [PATCH 055/207] Remove deprecated methods from Wallet, Key and Transaction class --- bitcoinlib/blocks.py | 75 ------ bitcoinlib/keys.py | 50 ---- bitcoinlib/networks.py | 65 ----- bitcoinlib/transactions.py | 529 ------------------------------------- tests/test_keys.py | 4 +- tests/test_networks.py | 7 - tests/test_transactions.py | 112 ++++---- tests/test_wallets.py | 3 +- 8 files changed, 59 insertions(+), 786 deletions(-) diff --git a/bitcoinlib/blocks.py b/bitcoinlib/blocks.py index bd12d2be..f051e61c 100644 --- a/bitcoinlib/blocks.py +++ b/bitcoinlib/blocks.py @@ -270,81 +270,6 @@ def parse_bytesio(cls, raw, block_hash=None, height=None, parse_transactions=Fal block.tx_count = tx_count return block - @classmethod - @deprecated - def from_raw(cls, raw, block_hash=None, height=None, parse_transactions=False, limit=0, network=DEFAULT_NETWORK): # pragma: no cover - """ - Create Block object from raw serialized block in bytes. - - Get genesis block: - - >>> from bitcoinlib.services.services import Service - >>> srv = Service() - >>> b = srv.getblock(0) - >>> b.block_hash.hex() - '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' - - :param raw: Raw serialize block - :type raw: bytes - :param block_hash: Specify block hash if known to verify raw block. Value error will be raised if calculated block hash is different than specified. - :type block_hash: bytes - :param height: Specify height if known. Will be derived from coinbase transaction if not provided. - :type height: int - :param parse_transactions: Indicate if transactions in raw block need to be parsed and converted to Transaction objects. Default is False - :type parse_transactions: bool - :param limit: Maximum number of transactions to parse. Default is 0: parse all transactions. Only used if parse_transaction is set to True - :type limit: int - :param network: Name of network - :type network: str - - :return Block: - """ - block_hash_calc = double_sha256(raw[:80])[::-1] - if not block_hash: - block_hash = block_hash_calc - elif block_hash != block_hash_calc: - raise ValueError("Provided block hash does not correspond to calculated block hash %s" % - block_hash_calc.hex()) - - version = raw[0:4][::-1] - prev_block = raw[4:36][::-1] - merkle_root = raw[36:68][::-1] - time = raw[68:72][::-1] - bits = raw[72:76][::-1] - nonce = raw[76:80][::-1] - tx_count, size = varbyteint_to_int(raw[80:89]) - txs_data = BytesIO(raw[80+size:]) - - # Parse coinbase transaction so we can extract extra information - # transactions = [Transaction.parse(txs_data, network=network)] - # txs_data = BytesIO(txs_data[transactions[0].size:]) - # block_txs_data = txs_data.read() - txs_data_size = txs_data.seek(0, 2) - txs_data.seek(0) - transactions = [] - - while parse_transactions and txs_data and txs_data.tell() < txs_data_size: - if limit != 0 and len(transactions) >= limit: - break - t = Transaction.parse_bytesio(txs_data, strict=False) - transactions.append(t) - # t = transaction_deserialize(txs_data, network=network, check_size=False) - # transactions.append(t) - # txs_data = txs_data[t.size:] - # TODO: verify transactions, need input value from previous txs - # if verify and not t.verify(): - # raise ValueError("Could not verify transaction %s in block %s" % (t.txid, block_hash)) - - if parse_transactions and limit == 0 and tx_count != len(transactions): - raise ValueError("Number of found transactions %d is not equal to expected number %d" % - (len(transactions), tx_count)) - - block = cls(block_hash, version, prev_block, merkle_root, time, bits, nonce, transactions, height, - network=network) - block.txs_data = txs_data - block.tx_count = tx_count - return block - def parse_transactions(self, limit=0): """ Parse raw transactions from Block, if transaction data is available in txs_data attribute. Creates diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index aa493418..a7d37be0 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -457,35 +457,6 @@ class Address(object): Class to store, convert and analyse various address types as representation of public keys or scripts hashes """ - @classmethod - @deprecated - def import_address(cls, address, compressed=None, encoding=None, depth=None, change=None, - address_index=None, network=None, network_overrides=None): - """ - Import an address to the Address class. Specify network if available, otherwise it will be - derived form the address. - - :param address: Address to import - :type address: str - :param compressed: Is key compressed or not, default is None - :type compressed: bool - :param encoding: Address encoding. Default is base58 encoding, for native segwit addresses specify bech32 encoding. Leave empty to derive from address - :type encoding: str - :param depth: Level of depth in BIP32 key path - :type depth: int - :param change: Use 0 for normal address/key, and 1 for change address (for returned/change payments) - :type change: int - :param address_index: Index of address. Used in BIP32 key paths - :type address_index: int - :param network: Specify network filter, i.e.: bitcoin, testnet, litecoin, etc. Wil trigger check if address is valid for this network - :type network: str - :param network_overrides: Override network settings for specific prefixes, i.e.: {"prefix_address_p2sh": "32"}. Used by settings in providers.json - :type network_overrides: dict - - :return Address: - """ - return cls.parse(address, compressed, encoding, depth, change, address_index, network, network_overrides) - @classmethod def parse(cls, address, compressed=None, encoding=None, depth=None, change=None, address_index=None, network=None, network_overrides=None): @@ -1073,10 +1044,6 @@ def encrypt(self, password): flagbyte = b'\xe0' if self.compressed else b'\xc0' return bip38_encrypt(self.private_hex, self.address(), password, flagbyte) - @deprecated - def bip38_encrypt(self, password): - return self.encrypt(password) - def wif(self, prefix=None): """ Get private Key in Wallet Import Format, steps: @@ -1999,23 +1966,6 @@ def parse_bytes(signature, public_key=None): return Signature(r, s, signature=signature, der_signature=der_signature, public_key=public_key, hash_type=hash_type) - @staticmethod - @deprecated - def from_str(signature, public_key=None): - """ - Create a signature from signature string with r and s part. Signature length must be 64 bytes or 128 - character hexstring - - :param signature: Signature string - :type signature: bytes, str - :param public_key: Public key as HDKey or Key object or any other string accepted by HDKey object - :type public_key: HDKey, Key, str, hexstring, bytes - - :return Signature: - """ - - signature = to_bytes(signature) - return Signature(signature, public_key) @staticmethod def create(txid, private, use_rfc6979=True, k=None): diff --git a/bitcoinlib/networks.py b/bitcoinlib/networks.py index b497559d..9e4214c2 100644 --- a/bitcoinlib/networks.py +++ b/bitcoinlib/networks.py @@ -198,30 +198,6 @@ def wif_prefix_search(wif, witness_type=None, multisig=None, network=None): return matches -# Replaced by Value class -@deprecated -def print_value(value, network=DEFAULT_NETWORK, rep='string', denominator=1, decimals=None): - """ - Return the value as string with currency symbol - - Wrapper for the Network().print_value method. - - :param value: Value in the smallest denominator such as Satoshi - :type value: int, float - :param network: Network name as string, default is 'bitcoin' - :type network: str - :param rep: Currency representation: 'string', 'symbol', 'none' or your own custom name - :type rep: str - :param denominator: Unit to use in representation. Default is 1. I.e. 1 = 1 BTC, 0.001 = milli BTC / mBTC, 1e-8 = Satoshi's - :type denominator: float - :param decimals: Number of digits after the decimal point, leave empty for automatic determination based on value. Use integer value between 0 and 8 - :type decimals: int - - :return str: - """ - return Network(network_name=network).print_value(value, rep, denominator, decimals) - - class Network(object): """ Network class with all network definitions. @@ -269,47 +245,6 @@ def __eq__(self, other): def __hash__(self): return hash(self.name) - # Replaced by Value class - @deprecated - def print_value(self, value, rep='string', denominator=1, decimals=None): - """ - Return the value as string with currency symbol - - Print value for 100000 satoshi as string in human-readable format - - >>> Network('bitcoin').print_value(100000) - '0.00100000 BTC' - - :param value: Value in the smallest denominator such as Satoshi - :type value: int, float - :param rep: Currency representation: 'string', 'symbol', 'none' or your own custom name - :type rep: str - :param denominator: Unit to use in representation. Default is 1. I.e. 1 = 1 BTC, 0.001 = milli BTC / mBTC - :type denominator: float - :param decimals: Number of digits after the decimal point, leave empty for automatic determination based on value. Use integer value between 0 and 8 - :type decimals: int - - :return str: - """ - if denominator not in NETWORK_DENOMINATORS: - raise NetworkError("Denominator not found in definitions, use one of the following values: %s" % - NETWORK_DENOMINATORS.keys()) - if value is None: - return "" - symb = rep - if rep == 'string': - symb = NETWORK_DENOMINATORS[denominator] + self.currency_code - elif rep == 'symbol': - symb = NETWORK_DENOMINATORS[denominator] + self.currency_symbol - elif rep == 'none': - symb = '' - decimals = decimals if decimals is not None else -int(math.log10(self.denominator / denominator)) - decimals = 0 if decimals < 0 else decimals - decimals = 8 if decimals > 8 else decimals - balance = round(float(value) * self.denominator / denominator, decimals) - format_str = "%%.%df %%s" % decimals - return (format_str % (balance, symb)).rstrip() - def wif_prefix(self, is_private=False, witness_type='legacy', multisig=False): """ Get WIF prefix for this network and specifications in arguments diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index c5f013d0..004816dd 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -46,512 +46,6 @@ def __str__(self): return self.msg -@deprecated # Replaced by Transaction.parse() in version 0.6 -def transaction_deserialize(rawtx, network=DEFAULT_NETWORK, check_size=True): # pragma: no cover - """ - Deserialize a raw transaction - - Returns a dictionary with list of input and output objects, locktime and version. - - Will raise an error if wrong number of inputs are found or if there are no output found. - - :param rawtx: Raw transaction as hexadecimal string or bytes - :type rawtx: str, bytes - :param network: Network code, i.e. 'bitcoin', 'testnet', 'litecoin', etc. Leave emtpy for default network - :type network: str, Network - :param check_size: Check if not bytes are left when parsing is finished. Disable when parsing list of transactions, such as the transactions in a raw block. Default is True - :type check_size: bool - - :return Transaction: - """ - - rawtx = to_bytes(rawtx) - coinbase = False - flag = None - witness_type = 'legacy' - - version = rawtx[0:4][::-1] - cursor = 4 - if rawtx[4:5] == b'\0': - flag = rawtx[5:6] - if flag == b'\1': - witness_type = 'segwit' - cursor += 2 - n_inputs, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - inputs = [] - if not isinstance(network, Network): - network = Network(network) - for n in range(0, n_inputs): - inp_hash = rawtx[cursor:cursor + 32][::-1] - if not len(inp_hash): - raise TransactionError("Input transaction hash not found. Probably malformed raw transaction") - if inp_hash == 32 * b'\0': - coinbase = True - output_n = rawtx[cursor + 32:cursor + 36][::-1] - cursor += 36 - unlocking_script_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - unlocking_script = rawtx[cursor:cursor + unlocking_script_size] - inp_type = 'legacy' - if witness_type == 'segwit' and not unlocking_script_size: - inp_type = 'segwit' - cursor += unlocking_script_size - sequence_number = rawtx[cursor:cursor + 4] - cursor += 4 - inputs.append(Input(prev_txid=inp_hash, output_n=output_n, unlocking_script=unlocking_script, - witness_type=inp_type, sequence=sequence_number, index_n=n, network=network)) - if len(inputs) != n_inputs: - raise TransactionError("Error parsing inputs. Number of tx specified %d but %d found" % (n_inputs, len(inputs))) - - outputs = [] - n_outputs, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - output_total = 0 - for n in range(0, n_outputs): - value = int.from_bytes(rawtx[cursor:cursor + 8][::-1], 'big') - cursor += 8 - lock_script_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - lock_script = rawtx[cursor:cursor + lock_script_size] - cursor += lock_script_size - outputs.append(Output(value=value, lock_script=lock_script, network=network, output_n=n)) - output_total += value - if not outputs: - raise TransactionError("Error no outputs found in this transaction") - - if witness_type == 'segwit': - for n in range(0, len(inputs)): - n_items, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - cursor += size - witnesses = [] - for m in range(0, n_items): - witness = b'\0' - item_size, size = varbyteint_to_int(rawtx[cursor:cursor + 9]) - if item_size: - witness = rawtx[cursor + size:cursor + item_size + size] - cursor += item_size + size - witnesses.append(witness) - if witnesses and not coinbase: - script_type = inputs[n].script_type - witness_script_type = 'sig_pubkey' - signatures = [] - keys = [] - sigs_required = 1 - public_hash = b'' - for witness in witnesses: - if witness == b'\0': - continue - if 69 <= len(witness) <= 74 and witness[0:1] == b'\x30': # witness is DER encoded signature - signatures.append(witness) - elif len(witness) == 33 and len(signatures) == 1: # key from sig_pk - keys.append(witness) - else: - rsds = script_deserialize(witness, script_types=['multisig']) - if not rsds['script_type'] == 'multisig': - _logger.warning("Could not parse witnesses in transaction. Multisig redeemscript expected") - witness_script_type = 'unknown' - script_type = 'unknown' - else: - keys = rsds['signatures'] - sigs_required = rsds['number_of_sigs_m'] - witness_script_type = 'p2sh' - script_type = 'p2sh_multisig' - - inp_witness_type = inputs[n].witness_type - usd = script_deserialize(inputs[n].unlocking_script, locking_script=True) - - if usd['script_type'] == "p2wpkh" and witness_script_type == 'sig_pubkey': - inp_witness_type = 'p2sh-segwit' - script_type = 'p2sh_p2wpkh' - elif usd['script_type'] == "p2wsh" and witness_script_type == 'p2sh': - inp_witness_type = 'p2sh-segwit' - script_type = 'p2sh_p2wsh' - inputs[n] = Input(prev_txid=inputs[n].prev_txid, output_n=inputs[n].output_n, keys=keys, - unlocking_script_unsigned=inputs[n].unlocking_script_unsigned, - unlocking_script=inputs[n].unlocking_script, sigs_required=sigs_required, - signatures=signatures, witness_type=inp_witness_type, script_type=script_type, - sequence=inputs[n].sequence, index_n=inputs[n].index_n, public_hash=public_hash, - network=inputs[n].network, witnesses=witnesses) - if len(rawtx[cursor:]) != 4 and check_size: - raise TransactionError("Error when deserializing raw transaction, bytes left for locktime must be 4 not %d" % - len(rawtx[cursor:])) - locktime = int.from_bytes(rawtx[cursor:cursor + 4][::-1], 'big') - - return Transaction(inputs, outputs, locktime, version, network, size=cursor + 4, output_total=output_total, - coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=rawtx) - - -@deprecated # Replaced by Script class in version 0.6 -def script_deserialize(script, script_types=None, locking_script=None, size_bytes_check=True): # pragma: no cover - """ - Deserialize a script: determine type, number of signatures and script data. - - :param script: Raw script - :type script: str, bytes - :param script_types: Limit script type determination to this list. Leave to default None to search in all script types. - :type script_types: list - :param locking_script: Only deserialize locking scripts. Specify False to only deserialize for unlocking scripts. Default is None for both - :type locking_script: bool - :param size_bytes_check: Check if script or signature starts with size bytes and remove size bytes before parsing. Default is True - :type size_bytes_check: bool - - :return list: With this items: [script_type, data, number_of_sigs_n, number_of_sigs_m] - """ - - def _parse_data(scr, max_items=None, redeemscript_expected=False, item_length=0): - # scr = to_bytes(scr) - items = [] - total_length = 0 - if 69 <= len(scr) <= 74 and scr[:1] == b'\x30': - return [scr], len(scr) - while len(scr) and (max_items is None or max_items > len(items)): - itemlen, size = varbyteint_to_int(scr[0:9]) - if item_length and itemlen != item_length: - break - if not item_length and itemlen not in [20, 33, 65, 70, 71, 72, 73]: - break - if redeemscript_expected and len(scr[itemlen + 1:]) < 20: - break - items.append(scr[1:itemlen + 1]) - total_length += itemlen + size - scr = scr[itemlen + 1:] - return items, total_length - - def _get_empty_data(): - return {'script_type': '', 'keys': [], 'signatures': [], 'hashes': [], 'redeemscript': b'', - 'number_of_sigs_n': 1, 'number_of_sigs_m': 1, 'locktime_cltv': None, 'locktime_csv': None, 'result': ''} - - def _parse_script(script): - found = False - cur = 0 - data = _get_empty_data() - for script_type in script_types: - cur = 0 - try: - ost = SCRIPT_TYPES_UNLOCKING[script_type] - except KeyError: - ost = SCRIPT_TYPES_LOCKING[script_type] - data = _get_empty_data() - data['script_type'] = script_type - found = True - for ch in ost: - if cur >= len(script): - found = False - break - cur_char = script[cur] - if ch[:4] == 'hash': - hash_length = 0 - if len(ch) > 5: - hash_length = int(ch.split("-")[1]) - s, total_length = _parse_data(script[cur:], 1, item_length=hash_length) - if not s: - found = False - break - data['hashes'] += s - cur += total_length - elif ch == 'signature': - signature_length = 0 - s, total_length = _parse_data(script[cur:], 1, item_length=signature_length) - if not s: - found = False - break - data['signatures'] += s - cur += total_length - elif ch == 'public_key': - pk_size, size = varbyteint_to_int(script[cur:cur + 9]) - key = script[cur + size:cur + size + pk_size] - if not key: - found = False - break - data['keys'].append(key) - cur += size + pk_size - elif ch == 'OP_RETURN': - if cur_char == op.op_return and cur == 0: - data.update({'op_return': script[cur + 1:]}) - cur = len(script) - found = True - break - else: - found = False - break - elif ch == 'multisig': # one or more signatures - redeemscript_expected = False - if 'redeemscript' in ost: - redeemscript_expected = True - s, total_length = _parse_data(script[cur:], redeemscript_expected=redeemscript_expected) - if not s: - found = False - break - data['signatures'] += s - cur += total_length - elif ch == 'redeemscript': - size_byte = 0 - if script[cur:cur + 1] == b'\x4c': - size_byte = 1 - elif script[cur:cur + 1] == b'\x4d': - size_byte = 2 - elif script[cur:cur + 1] == b'\x4e': - size_byte = 3 - data['redeemscript'] = script[cur + 1 + size_byte:] - data2 = script_deserialize(data['redeemscript'], locking_script=True) - if 'signatures' not in data2 or not data2['signatures']: - found = False - break - data['keys'] = data2['signatures'] - data['number_of_sigs_m'] = data2['number_of_sigs_m'] - data['number_of_sigs_n'] = data2['number_of_sigs_n'] - cur = len(script) - elif ch == 'push_size': - push_size, size = varbyteint_to_int(script[cur:cur + 9]) - found = bool(len(script[cur:]) - size == push_size) - if not found: - break - elif ch == 'op_m': - if cur_char in OP_N_CODES: - data['number_of_sigs_m'] = cur_char - op.op_1 + 1 - else: - found = False - break - cur += 1 - elif ch == 'op_n': - if cur_char in OP_N_CODES: - data['number_of_sigs_n'] = cur_char - op.op_1 + 1 - else: - found = False - break - if data['number_of_sigs_m'] > data['number_of_sigs_n']: - raise TransactionError("Number of signatures to sign (%s) is higher then actual " - "amount of signatures (%s)" % - (data['number_of_sigs_m'], data['number_of_sigs_n'])) - if len(data['signatures']) > int(data['number_of_sigs_n']): - raise TransactionError("%d signatures found, but %s sigs expected" % - (len(data['signatures']), data['number_of_sigs_n'])) - cur += 1 - elif ch == 'SIGHASH_ALL': - pass - # if cur_char != SIGHASH_ALL: - # found = False - # break - elif ch == 'locktime_cltv': - if len(script) < 4: - found = False - break - data['locktime_cltv'] = int.from_bytes(script[cur:cur + 4], 'little') - cur += 4 - elif ch == 'locktime_csv': - if len(script) < 4: - found = False - break - data['locktime_csv'] = int.from_bytes(script[cur:cur + 4], 'little') - cur += 4 - else: - try: - if opcodenames.get(cur_char) == ch: - cur += 1 - else: - found = False - data = _get_empty_data() - break - except IndexError: - raise TransactionError("Opcode %s not found [type %s]" % (ch, script_type)) - if found and not len(script[cur:]): # Found is True and no remaining script to parse - break - - if found and not len(script[cur:]): - return data, script[cur:] - data = _get_empty_data() - data['result'] = 'Script not recognised' - return data, '' - - data = _get_empty_data() - script = to_bytes(script) - if not script: - data.update({'result': 'Empty script'}) - return data - - # Check if script starts with size byte - if size_bytes_check: - script_size, size = varbyteint_to_int(script[0:9]) - if len(script[1:]) == script_size: - data = script_deserialize(script[1:], script_types, locking_script, size_bytes_check=False) - if 'result' in data and data['result'][:22] not in \ - ['Script not recognised', 'Empty script', 'Could not parse script']: - return data - - if script_types is None: - if locking_script is None: - script_types = dict(SCRIPT_TYPES_UNLOCKING, **SCRIPT_TYPES_LOCKING) - elif locking_script: - script_types = SCRIPT_TYPES_LOCKING - else: - script_types = SCRIPT_TYPES_UNLOCKING - elif not isinstance(script_types, list): - script_types = [script_types] - - locktime_cltv = 0 - locktime_csv = 0 - while len(script): - begin_script = script - data, script = _parse_script(script) - if begin_script == script: - break - if script and data['script_type'] == 'locktime_cltv': - locktime_cltv = data['locktime_cltv'] - if script and data['script_type'] == 'locktime_csv': - locktime_csv = data['locktime_csv'] - if data and data['result'] != 'Script not recognised': - data['locktime_cltv'] = locktime_cltv - data['locktime_csv'] = locktime_csv - return data - - wrn_msg = "Could not parse script, unrecognized script" - # _logger.debug(wrn_msg) - data = _get_empty_data() - data['result'] = wrn_msg - return data - - -@deprecated # Replaced by Script class in version 0.6 -def script_to_string(script, name_data=False): # pragma: no cover - """ - Convert script to human-readable string format with OP-codes, signatures, keys, etc - - :param script: A locking or unlocking script - :type script: bytes, str - :param name_data: Replace signatures and keys strings with name - :type name_data: bool - - :return str: - """ - - # script = to_bytes(script) - data = script_deserialize(script) - if not data or data['script_type'] == 'empty': - return "" - if name_data: - name = 'signature' - if data['signatures'] and len(data['signatures'][0]) in [33, 65]: - name = 'key' - sigs = ' '.join(['%s-%d' % (name, i) for i in range(1, len(data['signatures']) + 1)]) - else: - sigs = ' '.join([i.hex() for i in data['signatures']]) - - try: - scriptstr = SCRIPT_TYPES_LOCKING[data['script_type']] - except KeyError: - scriptstr = SCRIPT_TYPES_UNLOCKING[data['script_type']] - scriptstr = [sigs if x in ['signature', 'multisig', 'return_data'] else x for x in scriptstr] - if 'redeemscript' in data and data['redeemscript']: - redeemscript_str = script_to_string(data['redeemscript'], name_data=name_data) - scriptstr = [redeemscript_str if x == 'redeemscript' else x for x in scriptstr] - scriptstr = [opcodenames[80 + int(data['number_of_sigs_m'])] if x == 'op_m' else x for x in scriptstr] - scriptstr = [opcodenames[80 + int(data['number_of_sigs_n'])] if x == 'op_n' else x for x in scriptstr] - - return ' '.join(scriptstr) - - -# @deprecated # Replaced by Script class in version 0.6 -# def _serialize_multisig_redeemscript(public_key_list, n_required=None): # pragma: no cover -# # Serialize m-to-n multisig script. Needs a list of public keys -# for key in public_key_list: -# if not isinstance(key, (str, bytes)): -# raise TransactionError("Item %s in public_key_list is not of type string or bytes") -# if n_required is None: -# n_required = len(public_key_list) -# -# script = int_to_varbyteint(op.op_1 + n_required - 1) -# for key in public_key_list: -# script += varstr(key) -# script += int_to_varbyteint(op.op_1 + len(public_key_list) - 1) -# script += b'\xae' # 'OP_CHECKMULTISIG' -# -# return script -# -# -# @deprecated # Replaced by Script class in version 0.6 -# def serialize_multisig_redeemscript(key_list, n_required=None, compressed=True): # pragma: no cover -# """ -# Create a multisig redeemscript used in a p2sh. -# -# Contains the number of signatures, followed by the list of public keys and the OP-code for the number of signatures required. -# -# :param key_list: List of public keys -# :type key_list: Key, list -# :param n_required: Number of required signatures -# :type n_required: int -# :param compressed: Use compressed public keys? -# :type compressed: bool -# -# :return bytes: A multisig redeemscript -# """ -# -# if not key_list: -# return b'' -# if not isinstance(key_list, list): -# raise TransactionError("Argument public_key_list must be of type list") -# if len(key_list) > 15: -# raise TransactionError("Redeemscripts with more then 15 keys are non-standard and could result in " -# "locked up funds") -# public_key_list = [] -# for k in key_list: -# if isinstance(k, Key): -# if compressed: -# public_key_list.append(k.public_byte) -# else: -# public_key_list.append(k.public_uncompressed_byte) -# elif len(k) == 65 and k[0:1] == b'\x04' or len(k) == 33 and k[0:1] in [b'\x02', b'\x03']: -# public_key_list.append(k) -# elif len(k) == 132 and k[0:2] == '04' or len(k) == 66 and k[0:2] in ['02', '03']: -# public_key_list.append(bytes.fromhex(k)) -# else: -# kobj = Key(k) -# if compressed: -# public_key_list.append(kobj.public_byte) -# else: -# public_key_list.append(kobj.public_uncompressed_byte) -# -# return _serialize_multisig_redeemscript(public_key_list, n_required) - - -@deprecated # Replaced by Script class in version 0.6 -def _p2sh_multisig_unlocking_script(sigs, redeemscript, hash_type=None, as_list=False): # pragma: no cover - usu = b'\x00' - if as_list: - usu = [usu] - if not isinstance(sigs, list): - sigs = [sigs] - for sig in sigs: - s = sig - if hash_type: - s += hash_type.to_bytes(1, 'big') - if as_list: - usu.append(s) - else: - usu += varstr(s) - - rs_size = b'' - size_byte = b'' - if not as_list: - rs_size = int_to_varbyteint(len(redeemscript)) - if len(rs_size) > 1: - rs_size = rs_size[1:] - if len(redeemscript) >= 76: - if len(rs_size) == 1: - size_byte = b'\x4c' - elif len(rs_size) == 2: - size_byte = b'\x4d' - else: - size_byte = b'\x4e' - - redeemscript_str = size_byte + rs_size + redeemscript - if as_list: - usu.append(redeemscript_str) - else: - usu += redeemscript_str - return usu - - @deprecated # Replaced by Script class in version 0.6 def script_add_locktime_cltv(locktime_cltv, script): # pragma: no cover lockbytes = bytes([op.op_checklocktimeverify, op.op_drop]) @@ -1374,29 +868,6 @@ class Transaction(object): Each input in the transaction can be signed with the sign method provided a valid private key. """ - @staticmethod - @deprecated # Replaced by Transaction.parse() in version 0.6 - def import_raw(rawtx, network=DEFAULT_NETWORK, check_size=True): # pragma: no cover - """ - Import a raw transaction and create a Transaction object - - Uses the transaction_deserialize method to parse the raw transaction and then calls the init method of - this transaction class to create the transaction object - - REPLACED BY THE PARSE() METHOD - - :param rawtx: Raw transaction string - :type rawtx: bytes, str - :param network: Network, leave empty for default - :type network: str, Network - :param check_size: Check if no bytes are left when parsing is finished. Disable when parsing list of transactions, such as the transactions in a raw block. Default is True - :type check_size: bool - - :return Transaction: - """ - - return transaction_deserialize(rawtx, network=network, check_size=check_size) - @classmethod def parse(cls, rawtx, strict=True, network=DEFAULT_NETWORK): """ diff --git a/tests/test_keys.py b/tests/test_keys.py index 68c9f1fb..529f7ad2 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -650,7 +650,7 @@ class TestKeysAddress(unittest.TestCase): def test_keys_address_import_conversion(self): address_legacy = '3LPrWmWj1pYPEs8dGsPtWfmg2E9LhL5BHj' address = 'MSbzpevgxwPp3NQXNkPELK25Lvjng7DcBk' - ac = Address.import_address(address_legacy, network_overrides={"prefix_address_p2sh": "32"}) + ac = Address.parse(address_legacy, network_overrides={"prefix_address_p2sh": "32"}) self.assertEqual(ac.address, address) def test_keys_address_encodings(self): @@ -685,7 +685,7 @@ def test_keys_address_deserialize_litecoin(self): def test_keys_address_litecoin_import(self): address = 'LUPKYv9Z7AvQgxuVkDdqQrBDswsQJMxsN8' - a = Address.import_address(address) + a = Address.parse(address) self.assertEqual(a.hashed_data, '647ea562d9e72daca10fa476297f10576f284ba4') self.assertEqual(a.network.name, 'litecoin') self.assertEqual(a.address_orig, 'LUPKYv9Z7AvQgxuVkDdqQrBDswsQJMxsN8') diff --git a/tests/test_networks.py b/tests/test_networks.py index 5fe3a247..d2c385fb 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -26,13 +26,6 @@ def test_networks_prefix_hdkey_wif(self): self.assertEqual(network.wif_prefix(is_private=True), b'\x04\x88\xad\xe4') self.assertEqual(network.wif_prefix(is_private=False), b'\x04\x88\xb2\x1e') - def test_networks_print_value(self): - self.assertEqual(print_value(123, rep='symbol', denominator=0.001), '0.00123 m₿') - self.assertEqual(print_value(123, denominator=1e-6), '1.23 µBTC') - self.assertEqual(print_value(1e+14, network='dogecoin', denominator=1e+6, decimals=0), '1 MDOGE') - self.assertEqual(print_value(1200, denominator=1e-8, rep='Satoshi'), '1200 Satoshi') - self.assertEqual(print_value(1200, denominator=1e-6, rep='none'), '12.00') - def test_networks_network_value_for(self): prefixes = network_values_for('prefix_wif') expected_prefixes = [b'\xb0', b'\xef', b'\x99', b'\x80'] diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 2a278c87..9b2a4fe2 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1066,25 +1066,25 @@ class TestTransactionsScripts(unittest.TestCase, CustomAssertions): # keys.append(HDKey().public_hex) # self.assertRaisesRegexp(TransactionError, exp_error, serialize_multisig_redeemscript, keys) - def test_transaction_script_type_string(self): - # Locking script - s = bytes.fromhex('5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d169' - '87eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae') - os = "OP_1 032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca33016 " \ - "02308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a OP_2 OP_CHECKMULTISIG" - self.assertEqual(os, str(script_to_string(s))) - # Signature unlocking script - sig = '304402203359857b3bc3409c161a3b9570306bde53f21a15fcf3d3946d8ddfc94dd6ff35022024dc076c7014ee199831079cc0f' \ - 'df5e55aeebee7e90f4d51a2d923cc57f9173a01' - self.assertEqual(script_to_string(sig), sig) - # Multisig redeemscript - script = '52210294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e21024b68079ccf41b9df944f4aa37' \ - '7a2431a8df6efd7d7939d1f4d4f17376dc3434d21028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd' \ - '63676d0e53ae' - script_string = 'OP_2 0294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e ' \ - '024b68079ccf41b9df944f4aa377a2431a8df6efd7d7939d1f4d4f17376dc3434d ' \ - '028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd63676d0e OP_3 OP_CHECKMULTISIG' - self.assertEqual(script_to_string(script), script_string) + # def test_transaction_script_type_string(self): + # # Locking script + # s = bytes.fromhex('5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d169' + # '87eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae') + # os = "OP_1 032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca33016 " \ + # "02308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a OP_2 OP_CHECKMULTISIG" + # self.assertEqual(os, str(script_to_string(s))) + # # Signature unlocking script + # sig = '304402203359857b3bc3409c161a3b9570306bde53f21a15fcf3d3946d8ddfc94dd6ff35022024dc076c7014ee199831079cc0f' \ + # 'df5e55aeebee7e90f4d51a2d923cc57f9173a01' + # self.assertEqual(script_to_string(sig), sig) + # # Multisig redeemscript + # script = '52210294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e21024b68079ccf41b9df944f4aa37' \ + # '7a2431a8df6efd7d7939d1f4d4f17376dc3434d21028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd' \ + # '63676d0e53ae' + # script_string = 'OP_2 0294d7bf6363ab715168e812dd5b64d1f503ba707746b55535b7ee8afadd979c0e ' \ + # '024b68079ccf41b9df944f4aa377a2431a8df6efd7d7939d1f4d4f17376dc3434d ' \ + # '028885aad1fe0ad25ba2d9a0917a415f035e83e2c1a149904006f2d1dd63676d0e OP_3 OP_CHECKMULTISIG' + # self.assertEqual(script_to_string(script), script_string) def test_transaction_p2pk_script(self): rawtx = '0100000001db1a1774240cb1bd39d6cd6df0c57d5624fd2bd25b8b1be471714ab00e1a8b5d00000000484730440220592ce8' \ @@ -1140,43 +1140,43 @@ def test_transaction_sign_p2pk_value(self): self.assertEqual(t.signature_hash(sign_id=0).hex(), '67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986') - def test_transaction_locktime(self): - # FIXME: Add more useful unittests for locktime - s = bytes.fromhex('76a914af8e14a2cecd715c363b3a72b55b59a31e2acac988ac') - s_cltv = script_add_locktime_cltv(10000, s) - s_csv = script_add_locktime_csv(600000, s) - self.assertIsNotNone(s_cltv) - self.assertIsNotNone(s_csv) - # Test deserialize locktime transactions - rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044022003ea7' \ - '34e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b161ffb654be7e89c84' \ - 'de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d0977337376868b033029ba49cc64765df' \ - 'dffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a327696a70a000000006a47304402207758c05e' \ - '849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef9' \ - '8a21a3c567b4c6defe8ffca06012103ab51db28d30d3ac99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdff' \ - 'ffff029b040000000000001976a91406d66adea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a914061' \ - '4a615ee10d84a1e6d85ec1ff7fff527757d5987b0cc0800' - t = Transaction.parse_hex(rawtx) - self.assertEqual(t.locktime, 576688) - rawtx = '010000000159dc9ad3dc18cd76827f107a50fd96981e323aec7be4cbf982df176b9ab64f4900000000fd170147304402207' \ - '97987a17ee28181a94437e20c60b9d8da8974e68f91f250c424b623f06aeea9022036faa2834da6f883078abc3dd2fb48c1' \ - '9fc17097aa5b87fa11d00385fd21740b0121025c8ee352e8b0d12aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac80' \ - '0bd206a9068119b30840206281418227f33f76c53c43fa59fad748d2954e6ecd595a94c8aa6140d424014e59608dae01e97' \ - '700da0b53b3095a1af882102ef7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d1187632102ef' \ - '7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d11ac670475f2df5cb17521025c8ee352e8b0d12' \ - 'aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac800bdac68feffffff011f000200000000001976a91436963a21b49f' \ - '701acf03dd1e778ab5774017b53c88ac75f2df5c' - t = Transaction.parse_hex(rawtx) - self.assertEqual(t.locktime, 1558180469) - # Input level locktimes - t = Transaction() - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df49453e', 0, locktime_cltv=10000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494511', 0, locktime_csv=20000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494522', 0, - locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 30000) - t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494533', 0, - locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 40000) - self.assertIsNone(t.info()) + # def test_transaction_locktime(self): + # # FIXME: Add more useful unittests for locktime + # s = bytes.fromhex('76a914af8e14a2cecd715c363b3a72b55b59a31e2acac988ac') + # s_cltv = script_add_locktime_cltv(10000, s) + # s_csv = script_add_locktime_csv(600000, s) + # self.assertIsNotNone(s_cltv) + # self.assertIsNotNone(s_csv) + # # Test deserialize locktime transactions + # rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044022003ea7' \ + # '34e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b161ffb654be7e89c84' \ + # 'de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d0977337376868b033029ba49cc64765df' \ + # 'dffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a327696a70a000000006a47304402207758c05e' \ + # '849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef9' \ + # '8a21a3c567b4c6defe8ffca06012103ab51db28d30d3ac99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdff' \ + # 'ffff029b040000000000001976a91406d66adea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a914061' \ + # '4a615ee10d84a1e6d85ec1ff7fff527757d5987b0cc0800' + # t = Transaction.parse_hex(rawtx) + # self.assertEqual(t.locktime, 576688) + # rawtx = '010000000159dc9ad3dc18cd76827f107a50fd96981e323aec7be4cbf982df176b9ab64f4900000000fd170147304402207' \ + # '97987a17ee28181a94437e20c60b9d8da8974e68f91f250c424b623f06aeea9022036faa2834da6f883078abc3dd2fb48c1' \ + # '9fc17097aa5b87fa11d00385fd21740b0121025c8ee352e8b0d12aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac80' \ + # '0bd206a9068119b30840206281418227f33f76c53c43fa59fad748d2954e6ecd595a94c8aa6140d424014e59608dae01e97' \ + # '700da0b53b3095a1af882102ef7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d1187632102ef' \ + # '7f775819d4518c67c904201e30d4181190552f0026db94f93bfde557e23d11ac670475f2df5cb17521025c8ee352e8b0d12' \ + # 'aecd8b3d9ac3bd93cae1b2cc5de7ac56c2995ab506ac800bdac68feffffff011f000200000000001976a91436963a21b49f' \ + # '701acf03dd1e778ab5774017b53c88ac75f2df5c' + # t = Transaction.parse_hex(rawtx) + # self.assertEqual(t.locktime, 1558180469) + # # Input level locktimes + # t = Transaction() + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df49453e', 0, locktime_cltv=10000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494511', 0, locktime_csv=20000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494522', 0, + # locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 30000) + # t.add_input('f601e39f6b99b64fc2e98beb706ec7f14d114db7e61722c0313b0048df494533', 0, + # locktime_csv=SEQUENCE_LOCKTIME_TYPE_FLAG + 40000) + # self.assertIsNone(t.info()) def test_transaction_get_unlocking_script_type(self): self.assertEqual(get_unlocking_script_type('p2pk'), 'signature') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 7a0a6563..cb5d8568 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1684,7 +1684,6 @@ def test_wallet_scan(self): 'ZY6DFj6x61Wwbrg8Q' wallet = wallet_create_or_open('scan-test', keys=account_key, network='testnet', db_uri=self.DATABASE_URI) wallet.scan(scan_gap_limit=8) - wallet.info() self.assertEqual(len(wallet.keys()), 27) self.assertEqual(wallet.balance(), 60500000) self.assertEqual(len(wallet.transactions()), 4) @@ -2598,7 +2597,7 @@ def setUpClass(cls): def test_wallet_readonly_create_and_import(self): k = '13A1W4jLPP75pzvn2qJ5KyyqG3qPSpb9jM' w = wallet_create_or_open('addrwlt', k, db_uri=self.DATABASE_URI) - addr = Address.import_address('12yuSkjKmHzXCFn39PK1XP3XyeoVw9LJdN') + addr = Address.parse('12yuSkjKmHzXCFn39PK1XP3XyeoVw9LJdN') w.import_key(addr) self.assertEqual(len(w.accounts()), 1) w.utxos_update() From 11c2857cb356f9289513665cc793cfb8d9b1e462 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Jan 2024 22:53:51 +0100 Subject: [PATCH 056/207] Add replace-by-fee option to Wallet, Transaction and clw --- bitcoinlib/tools/clw.py | 9 +++++++-- bitcoinlib/transactions.py | 11 +++++++---- bitcoinlib/wallets.py | 38 ++++++++++++++++++++++++++------------ tests/test_transactions.py | 10 +++++----- tests/test_wallets.py | 12 ++++++++++++ 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 79a81721..9245a22d 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -138,6 +138,9 @@ def parse_args(): group_transaction.add_argument('--import-tx-file', '-a', metavar="FILENAME_TRANSACTION", help="Import transaction dictionary or raw transaction string from specified " "filename and sign it with available key(s)") + group_transaction.add_argument('--rbf', action='store_true', + help="Enable replace-by-fee flag. Allow to replace transaction with a new one " + "with higher fees, to avoid transactions taking to long to confirm.") pa = parser.parse_args() @@ -212,7 +215,8 @@ def create_transaction(wlt, send_args, args): output_arr = [(address, value) for [address, value] in send_args] return wlt.transaction_create(output_arr=output_arr, network=args.network, fee=args.fee, min_confirms=0, input_key_id=args.input_key_id, - number_of_change_outputs=args.number_of_change_outputs) + number_of_change_outputs=args.number_of_change_outputs, + replace_by_fee=args.rbf) def print_transaction(wt): @@ -382,7 +386,8 @@ def main(): print("Sweep wallet. Send all funds to %s" % args.sweep, file=output_to) if args.push: offline = False - wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee) + wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee, + replace_by_fee=args.rbf) if not wt: raise WalletError("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) wt.info() diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 004816dd..3ae0b055 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -171,7 +171,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :type script_type: str :param address: Address string or object for input :type address: str, Address - :param sequence: Sequence part of input, you normally do not have to touch this + :param sequence: Sequence part of input, used for locktime setting and replace by fee. No need to set directly normally. :type sequence: bytes, int :param compressed: Use compressed or uncompressed public keys. Default is compressed :type compressed: bool @@ -1065,7 +1065,7 @@ def load(txid=None, filename=None): def __init__(self, inputs=None, outputs=None, locktime=0, version=None, network=DEFAULT_NETWORK, fee=None, fee_per_kb=None, size=None, txid='', txhash='', date=None, confirmations=None, block_height=None, block_hash=None, input_total=0, output_total=0, rawtx=b'', - status='new', coinbase=False, verified=False, witness_type='legacy', flag=None): + status='new', coinbase=False, verified=False, witness_type='segwit', flag=None, replace_by_fee=False): """ Create a new transaction class with provided inputs and outputs. @@ -1176,6 +1176,7 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, self.status = status self.verified = verified self.witness_type = witness_type + self.replace_by_fee = replace_by_fee self.change = 0 self.calc_weight_units() if self.witness_type not in ['legacy', 'segwit']: @@ -1409,7 +1410,7 @@ def set_locktime_blocks(self, blocks): if blocks != 0 and blocks != 0xffffffff: for i in self.inputs: if i.sequence == 0xffffffff: - i.sequence = 0xfffffffd + i.sequence = SEQUENCE_ENABLE_LOCKTIME self.sign_and_update() def set_locktime_time(self, timestamp): @@ -1434,7 +1435,7 @@ def set_locktime_time(self, timestamp): # Input sequence value must be below 0xffffffff for i in self.inputs: if i.sequence == 0xffffffff: - i.sequence = 0xfffffffd + i.sequence = SEQUENCE_ENABLE_LOCKTIME self.sign_and_update() def signature_hash(self, sign_id=None, hash_type=SIGHASH_ALL, witness_type=None, as_hex=False): @@ -1818,6 +1819,8 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash if index_n is None: index_n = len(self.inputs) + if self.replace_by_fee and sequence == 0xffffffff: + sequence = SEQUENCE_REPLACE_BY_FEE sequence_int = sequence if isinstance(sequence, bytes): sequence_int = int.from_bytes(sequence, 'little') diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 121d7c39..b75ce8bb 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3513,7 +3513,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non def transaction_create(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, max_utxos=None, locktime=0, number_of_change_outputs=1, - random_output_order=True): + random_output_order=True, replace_by_fee=False): """ Create new transaction with specified outputs. @@ -3550,6 +3550,8 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco :type number_of_change_outputs: int :param random_output_order: Shuffle order of transaction outputs to increase privacy. Default is True :type random_output_order: bool + :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE + :type replace_by_fee: bool :return WalletTransaction: object """ @@ -3570,7 +3572,8 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Create transaction and add outputs amount_total_output = 0 - transaction = WalletTransaction(hdwallet=self, account_id=account_id, network=network, locktime=locktime) + transaction = WalletTransaction(hdwallet=self, account_id=account_id, network=network, locktime=locktime, + replace_by_fee=replace_by_fee) transaction.outgoing_tx = True for o in output_arr: if isinstance(o, Output): @@ -3605,7 +3608,9 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Add inputs sequence = 0xffffffff if 0 < transaction.locktime < 0xffffffff: - sequence = 0xfffffffe + sequence = SEQUENCE_ENABLE_LOCKTIME + elif replace_by_fee: + sequence = SEQUENCE_REPLACE_BY_FEE amount_total_input = 0 if input_arr is None: selected_utxos = self.select_inputs(amount_total_output + fee_estimate, transaction.network.dust_amount, @@ -3616,13 +3621,14 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco amount_total_input += utxo.value inp_keys, key = self._objects_by_key_id(utxo.key_id) multisig = False if isinstance(inp_keys, list) and len(inp_keys) < 2 else True - unlock_script_type = get_unlocking_script_type(utxo.script_type, utxo.key.witness_type, + witness_type = utxo.key.witness_type if utxo.key.witness_type else self.witness_type + unlock_script_type = get_unlocking_script_type(utxo.script_type, witness_type, multisig=multisig) transaction.add_input(utxo.transaction.txid, utxo.output_n, keys=inp_keys, script_type=unlock_script_type, sigs_required=self.multisig_n_required, sort=self.sort_keys, compressed=key.compressed, value=utxo.value, address=utxo.key.address, sequence=sequence, - key_path=utxo.key.path, witness_type=utxo.key.witness_type) + key_path=utxo.key.path, witness_type=witness_type) # FIXME: Missing locktime_cltv=locktime_cltv, locktime_csv=locktime_csv (?) else: for inp in input_arr: @@ -3899,7 +3905,8 @@ def transaction_import_raw(self, rawtx, network=None): return rt def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, - min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, offline=True, number_of_change_outputs=1): + min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, offline=True, number_of_change_outputs=1, + replace_by_fee=False): """ Create a new transaction with specified outputs and push it to the network. Inputs can be specified but if not provided they will be selected from wallets utxo's @@ -3938,6 +3945,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n :type offline: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int + :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE + :type replace_by_fee: bool :return WalletTransaction: """ @@ -3947,7 +3956,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n (len(input_arr), max_utxos)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee, - min_confirms, max_utxos, locktime, number_of_change_outputs) + min_confirms, max_utxos, locktime, number_of_change_outputs, True, + replace_by_fee) transaction.sign(priv_keys) # Calculate exact fees and update change output if necessary if fee is None and transaction.fee_per_kb and transaction.change: @@ -3959,7 +3969,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n "Recreate transaction with correct fee" % (transaction.fee, fee_exact)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee_exact, min_confirms, max_utxos, locktime, - number_of_change_outputs) + number_of_change_outputs, True, replace_by_fee) transaction.sign(priv_keys) transaction.rawtx = transaction.raw() @@ -3971,7 +3981,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n return transaction def send_to(self, to_address, amount, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, - priv_keys=None, locktime=0, offline=True, number_of_change_outputs=1): + priv_keys=None, locktime=0, offline=True, number_of_change_outputs=1, replace_by_fee=False): """ Create transaction and send it with default Service objects :func:`services.sendrawtransaction` method. @@ -4006,6 +4016,8 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ :type offline: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int + :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE + :type replace_by_fee: bool :return WalletTransaction: """ @@ -4013,10 +4025,10 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ outputs = [(to_address, amount)] return self.send(outputs, input_key_id=input_key_id, account_id=account_id, network=network, fee=fee, min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, offline=offline, - number_of_change_outputs=number_of_change_outputs) + number_of_change_outputs=number_of_change_outputs, replace_by_fee=replace_by_fee) def sweep(self, to_address, account_id=None, input_key_id=None, network=None, max_utxos=999, min_confirms=1, - fee_per_kb=None, fee=None, locktime=0, offline=True): + fee_per_kb=None, fee=None, locktime=0, offline=True, replace_by_fee=False): """ Sweep all unspent transaction outputs (UTXO's) and send them to one or more output addresses. @@ -4055,6 +4067,8 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma :type locktime: int :param offline: Just return the transaction object and do not send it when offline = True. Default is True :type offline: bool + :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE + :type replace_by_fee: bool :return WalletTransaction: """ @@ -4106,7 +4120,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma "outputs, use amount value = 0 to indicate a change/rest output") return self.send(to_list, input_arr, network=network, fee=fee, min_confirms=min_confirms, locktime=locktime, - offline=offline) + offline=offline, replace_by_fee=replace_by_fee) def wif(self, is_private=False, account_id=0): """ diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 9b2a4fe2..063e0f99 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -337,14 +337,14 @@ def test_transaction_parse_short_signature(self): self.assertTrue(t.verify()) def test_transactions_estimate_size_p2pkh(self): - t = Transaction() + t = Transaction(witness_type='legacy') t.add_output(2710000, '1Khyc5eUddbhYZ8bEZi9wiN8TrmQ8uND4j') t.add_output(2720000, '1D1gLEHsvjunpJxqjkWcPZqU4QzzRrHDdL') t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) self.assertEqual(t.estimate_size(), 227) def test_transactions_estimate_size_nulldata(self): - t = Transaction() + t = Transaction(witness_type='legacy') lock_script = b'j' + varstr(b'Please leave a message after the beep') t.add_output(0, lock_script=lock_script) t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) @@ -1117,7 +1117,7 @@ def test_transaction_sign_p2pk(self): inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') outp = Output(7000, k, network='testnet', script_type='p2pk') - t = Transaction([inp], [outp], network='testnet') + t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) self.assertTrue(t.verify()) self.assertEqual(t.signature_hash(sign_id=0).hex(), @@ -1134,7 +1134,7 @@ def test_transaction_sign_p2pk_value(self): inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') outp = Output(value - fee, k, network='testnet', script_type='p2pk') - t = Transaction([inp], [outp], network='testnet') + t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) self.assertTrue(t.verify()) self.assertEqual(t.signature_hash(sign_id=0).hex(), @@ -1429,7 +1429,7 @@ def test_transaction_multisig_estimate_size(self): pk2 = HDKey.from_passphrase(phrase2, network=network) pk3 = HDKey.from_passphrase(phrase3, network=network) - t = Transaction(network=network) + t = Transaction(network=network, witness_type='legacy') t.add_input(prev_txid, 0, [pk1.private_byte, pk2.public_byte, pk3.public_byte], script_type='p2sh_multisig', sigs_required=2) t.add_output(10000, '22zkxRGNsjHJpqU8tSS7cahSZVXrz9pJKSs') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index cb5d8568..1de8285c 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2272,6 +2272,18 @@ def test_wallet_merge_transactions(self): self.assertEqual(t1.output_total + t2.output_total, t.output_total) self.assertEqual(t1.fee + t2.fee, t.fee) + def test_wallet_transaction_replace_by_fee(self): + w = wallet_create_or_open('wallet_transaction_rbf', network='bitcoinlib_test', + db_uri=self.DATABASE_URI) + w.utxos_update() + address = w.get_key() + t = w.send_to(address, 10000, fee=500, replace_by_fee=True) + self.assertTrue(t.verify()) + t2 = w.send_to(address, 10000, fee=1000, replace_by_fee=True) + self.assertTrue(t2.verify()) + self.assertTrue(t2.replace_by_fee) + self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) + @parameterized_class(*params) class TestWalletSegwit(TestWalletMixin, unittest.TestCase): From 82df6742359a6955e59337e6007215ef3923f60e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 23 Jan 2024 13:37:14 +0100 Subject: [PATCH 057/207] Resize key database fields --- bitcoinlib/db.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 03b98d2b..94aed907 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -283,12 +283,12 @@ class DbKey(Base): "depth=1 are the masterkeys children.") change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(128), index=True, doc="Bytes representation of public key") + public = Column(LargeBinary(32), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") - wif = Column(EncryptedString(255), index=True, doc="Public or private WIF (Wallet Import Format) representation") + wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") - address = Column(String(255), index=True, + address = Column(String(100), index=True, doc="Address representation of key. An cryptocurrency address is a hash of the public key") cosigner_id = Column(Integer, doc="ID of cosigner, used if key is part of HD Wallet") encoding = Column(String(15), default='base58', doc='Encoding used to represent address: base58 or bech32') From 38cbb49606fa404728bb08cc53e3335423926dec Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 23 Jan 2024 13:47:00 +0100 Subject: [PATCH 058/207] Disable insecure connection warnings --- bitcoinlib/services/baseclient.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bitcoinlib/services/baseclient.py b/bitcoinlib/services/baseclient.py index d82dd641..f1883a7a 100644 --- a/bitcoinlib/services/baseclient.py +++ b/bitcoinlib/services/baseclient.py @@ -19,6 +19,7 @@ # import requests +import urllib3 from urllib.parse import urlencode import json from bitcoinlib.main import * @@ -27,6 +28,9 @@ _logger = logging.getLogger(__name__) +# Disabled warning about insecure requests, as we only connect to familiar sources and local nodes +urllib3.disable_warnings() + class ClientError(Exception): def __init__(self, msg=''): From 6cefd372914604fc7db3a3fe9b3ef50714655296 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 24 Jan 2024 09:36:21 +0100 Subject: [PATCH 059/207] Fix old database updates --- bitcoinlib/db.py | 27 ++++++++++++--------------- bitcoinlib/keys.py | 1 - bitcoinlib/services/baseclient.py | 2 +- bitcoinlib/services/services.py | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 94aed907..7b7d5d93 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy import (Column, Integer, BigInteger, UniqueConstraint, CheckConstraint, String, Boolean, Sequence, ForeignKey, DateTime, LargeBinary, TypeDecorator) from sqlalchemy.ext.declarative import declarative_base @@ -128,7 +128,10 @@ def add_column(engine, table_name, column): """ column_name = column.compile(dialect=engine.dialect) column_type = column.type.compile(engine.dialect) - engine.execute("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) + statement = text("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) + with engine.connect() as conn: + result = conn.execute(statement) + return result class EncryptedBinary(TypeDecorator): @@ -506,19 +509,13 @@ def db_update(db, version_db, code_version=BITCOINLIB_VERSION): column = Column('witnesses', LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") add_column(db.engine, 'transaction_inputs', column) # version_db = db_update_version_id(db, '0.6.4') - if True or version_db < '7.0.0' and code_version >= '7.0.0': - # Add witness_type to keys table so we can use mixed keys in a single wallet - column = Column('witness_type', String(20), doc="Wallet witness type. Can be 'legacy', 'segwit' or " - "'p2sh-segwit'. Default is segwit.") - add_column(db.engine, 'keys', column) - # TODO: Add to upgrade script, use alembic?? - # - Add mixed to wallet_constraint_allowed_types - # - Wallet.witness_type default='segwit', doc="Wallet witness type. Can be 'legacy', 'segwit', 'p2sh-segwit' or - # 'mixed. Default is " "segwit.") - # - Transaction.witness_type default='segwit' - # - Transaction.input: default='segwit', doc="Type of transaction, can be legacy, - # segwit or p2sh-segwit. Default is segwit") - + if True or version_db < '0.7.0' and code_version >= '0.7.0': + raise ValueError("Old database version %s is not supported in version 0.7+. " + "Please copy private keys and recreate wallets" % version_db) + # TODO: write update script to copy private keys from db + # column = Column('witness_type', String(20), doc="Wallet witness type. Can be 'legacy', 'segwit' or " + # "'p2sh-segwit'. Default is segwit.") + # add_column(db.engine, 'keys', column) version_db = db_update_version_id(db, code_version) return version_db diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index a7d37be0..0815b8e9 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1966,7 +1966,6 @@ def parse_bytes(signature, public_key=None): return Signature(r, s, signature=signature, der_signature=der_signature, public_key=public_key, hash_type=hash_type) - @staticmethod def create(txid, private, use_rfc6979=True, k=None): """ diff --git a/bitcoinlib/services/baseclient.py b/bitcoinlib/services/baseclient.py index f1883a7a..79c5908d 100644 --- a/bitcoinlib/services/baseclient.py +++ b/bitcoinlib/services/baseclient.py @@ -28,7 +28,7 @@ _logger = logging.getLogger(__name__) -# Disabled warning about insecure requests, as we only connect to familiar sources and local nodes +# Disable warnings about insecure requests, as we only connect to familiar sources and local nodes urllib3.disable_warnings() diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index b032dbf3..db2c4c32 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -865,7 +865,7 @@ def getutxos(self, address, after_txid=''): """ Get list of unspent outputs (UTXO's) for specified address from database cache. - Sorted from old to new, so highest number of confirmations first. + Sorted from old to new, so the highest number of confirmations first. :param address: Address string :type address: str From f5071a104953c95be71b5c76c5abea673dcc55ce Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 26 Jan 2024 14:07:09 +0100 Subject: [PATCH 060/207] Rewrite wallet key generation system (6 times faster when creating keys in bulk) --- bitcoinlib/db.py | 2 +- bitcoinlib/wallets.py | 207 ++++++++++++++++++++++++++++++------------ tests/test_wallets.py | 5 +- 3 files changed, 153 insertions(+), 61 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 7b7d5d93..b3ebadac 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -509,7 +509,7 @@ def db_update(db, version_db, code_version=BITCOINLIB_VERSION): column = Column('witnesses', LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") add_column(db.engine, 'transaction_inputs', column) # version_db = db_update_version_id(db, '0.6.4') - if True or version_db < '0.7.0' and code_version >= '0.7.0': + if version_db < '0.7.0' and code_version >= '0.7.0': raise ValueError("Old database version %s is not supported in version 0.7+. " "Please copy private keys and recreate wallets" % version_db) # TODO: write update script to copy private keys from db diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index b75ce8bb..ebbcd023 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -302,7 +302,7 @@ class WalletKey(object): @staticmethod def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0, purpose=84, parent_id=0, path='m', key_type=None, encoding=None, witness_type=DEFAULT_WITNESS_TYPE, multisig=False, - cosigner_id=None): + cosigner_id=None, new_key_id=None): """ Create WalletKey from a HDKey object or key. @@ -346,6 +346,8 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 :type multisig: bool :param cosigner_id: Set this if you would like to create keys for other cosigners. :type cosigner_id: int + :param new_key_id: Key ID in database (DbKey.id), use to directly insert key in database without checks and without commiting. Mainly for internal usage, to significantly increase speed when inserting multiple keys. + :type new_key_id: int :return WalletKey: WalletKey object """ @@ -374,33 +376,34 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 script_type = script_type_default(witness_type, multisig) if not key_is_address: - keyexists = session.query(DbKey).\ - filter(DbKey.wallet_id == wallet_id, - DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() - if keyexists: - _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) - return WalletKey(keyexists.id, session, k) - if key_type != 'single' and k.depth != len(path.split('/'))-1: if path == 'm' and k.depth > 1: path = "M" - address = k.address(encoding=encoding, script_type=script_type) - wk = session.query(DbKey).filter( - DbKey.wallet_id == wallet_id, - or_(DbKey.public == k.public_byte, - DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=False), - DbKey.address == address)).first() - if wk: - wk.wif = k.wif(witness_type=witness_type, multisig=multisig, is_private=True) - wk.is_private = True - wk.private = k.private_byte - wk.public = k.public_byte - wk.path = path - session.commit() - return WalletKey(wk.id, session, k) - - nk = DbKey(name=name[:80], wallet_id=wallet_id, public=k.public_byte, private=k.private_byte, purpose=purpose, + + if not new_key_id: + keyexists = session.query(DbKey).\ + filter(DbKey.wallet_id == wallet_id, + DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() + if keyexists: + _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) + return WalletKey(keyexists.id, session, k) + + wk = session.query(DbKey).filter( + DbKey.wallet_id == wallet_id, + or_(DbKey.public == k.public_byte, + DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=False), + DbKey.address == address)).first() + if wk: + wk.wif = k.wif(witness_type=witness_type, multisig=multisig, is_private=True) + wk.is_private = True + wk.private = k.private_byte + wk.public = k.public_byte + wk.path = path + session.commit() + return WalletKey(wk.id, session, k) + + nk = DbKey(id=new_key_id, name=name[:80], wallet_id=wallet_id, public=k.public_byte, private=k.private_byte, purpose=purpose, account_id=account_id, depth=k.depth, change=change, address_index=k.child_index, wif=k.wif(witness_type=witness_type, multisig=multisig, is_private=True), address=address, parent_id=parent_id, compressed=k.compressed, is_private=k.is_private, path=path, @@ -411,17 +414,19 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 filter(DbKey.wallet_id == wallet_id, DbKey.address == k.address).first() if keyexists: - _logger.warning("Key with ID %s already exists" % keyexists.id) + _logger.warning("Key %s with ID %s already exists" % (k.address, keyexists.id)) return WalletKey(keyexists.id, session, k) - nk = DbKey(name=name[:80], wallet_id=wallet_id, purpose=purpose, + nk = DbKey(id=new_key_id, name=name[:80], wallet_id=wallet_id, purpose=purpose, account_id=account_id, depth=k.depth, change=change, address=k.address, parent_id=parent_id, compressed=k.compressed, is_private=False, path=path, key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, witness_type=witness_type) - session.merge(DbNetwork(name=network)) + if not new_key_id: + session.merge(DbNetwork(name=network)) session.add(nk) - session.commit() + if new_key_id is None: + session.commit() return WalletKey(nk.id, session, k) def _commit(self): @@ -1678,9 +1683,6 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, public_key_list = [pubk.key_public for pubk in public_keys] public_key_ids = [str(x.key_id) for x in public_keys] - # Calculate redeemscript and address and add multisig key to database - # redeemscript = serialize_multisig_redeemscript(public_key_list, n_required=self.multisig_n_required) - # todo: pass key object, reuse key objects redeemscript = Script(script_types=['multisig'], keys=public_key_list, sigs_required=self.multisig_n_required).serialize() @@ -1710,7 +1712,8 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, + number_of_keys=1, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet with index 0 if there is no account defined yet. @@ -1727,6 +1730,8 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ :type change: int :param cosigner_id: Cosigner ID for key path :type cosigner_id: int + :param witness_type: Use to create key with different witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str @@ -1754,7 +1759,9 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) address_index = 0 - if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): + if (self.multisig and cosigner_id is not None and + (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or + self.cosigner[cosigner_id].key_path == ['m'])): req_path = [] else: prevkey = self._session.query(DbKey).\ @@ -1766,7 +1773,51 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ req_path = [change, address_index] return self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, - cosigner_id=cosigner_id, address_index=address_index) + cosigner_id=cosigner_id, address_index=address_index, number_of_keys=number_of_keys) + + def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None, + number_of_keys=1): + if self.scheme == 'single': + return self.main_key + ret_keys = [] + + network, account_id, _ = self._get_account_defaults(network, account_id) + if network != self.network.name and "coin_type'" not in self.key_path: + raise WalletError("Multiple networks not supported by wallet key structure") + if self.multisig: + # if witness_type: + # TODO: raise error + if not self.multisig_n_required: + raise WalletError("Multisig_n_required not set, cannot create new key") + if cosigner_id is None: + if self.cosigner_id is None: + raise WalletError("Missing Cosigner ID value, cannot create new key") + cosigner_id = self.cosigner_id + witness_type = self.witness_type if not witness_type else witness_type + purpose = self.purpose + if witness_type != self.witness_type: + _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) + + address_index = 0 + if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): + req_path = [] + else: + prevkey = self._session.query(DbKey).\ + filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, + witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ + order_by(DbKey.address_index.desc()).first() + if prevkey: + address_index = prevkey.address_index + 1 + req_path = [change, address_index] + + for i in range(number_of_keys): + address_index += 1 + req_path = [change, address_index] + ret_keys.append(self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, + network=network, cosigner_id=cosigner_id, address_index=address_index, commit=False)) + + self._session.commit() + return ret_keys def new_key_change(self, name='', account_id=None, witness_type=None, network=None): """ @@ -1906,22 +1957,23 @@ def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id last_used_key_id = 0 if last_used_qr: last_used_key_id = last_used_qr.id - dbkey = (self._session.query(DbKey). + dbkey = (self._session.query(DbKey.id). filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, used=False, change=change, depth=self.key_depth, witness_type=witness_type). filter(DbKey.id > last_used_key_id). - order_by(DbKey.id.desc()).all()) - key_list = [] + order_by(DbKey.id.asc()).all()) if self.scheme == 'single' and len(dbkey): number_of_keys = len(dbkey) if number_of_keys > len(dbkey) else number_of_keys - for i in range(number_of_keys): - if dbkey: - dk = dbkey.pop() - nk = self.key(dk.id) - else: - nk = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, - witness_type=witness_type, network=network) - key_list.append(nk) + key_list = [self.key(key_id[0]) for key_id in dbkey] + + if len(key_list) > number_of_keys: + key_list = key_list[:number_of_keys] + else: + new_keys = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, + witness_type=witness_type, network=network, + number_of_keys=number_of_keys - len(key_list)) + key_list = key_list + (new_keys if type(new_keys) is list else [new_keys]) + if as_list: return key_list else: @@ -2099,7 +2151,8 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, witness_type=None, network=None, recreate=False): + address_index=0, change=0, witness_type=None, network=None, recreate=False, + number_of_keys=1): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2142,9 +2195,11 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info :type recreate: bool - :return WalletKey: + :return WalletKey, list of WalletKey: """ + if number_of_keys == 0: + return [] network, account_id, _ = self._get_account_defaults(network, account_id) cosigner_id = cosigner_id if cosigner_id is not None else self.cosigner_id level_offset_key = level_offset @@ -2173,10 +2228,18 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi else: wk = wlt.key_for_path(path, level_offset=level_offset, account_id=account_id, name=name, cosigner_id=cosigner_id, network=network, recreate=recreate, - witness_type=witness_type) - public_keys.append(wk) - return self._new_key_multisig(public_keys, name, account_id, change, cosigner_id, network, address_index, - witness_type) + witness_type=witness_type, number_of_keys=number_of_keys) + public_keys.append(wk if type(wk) == list else [wk]) + # public_keys.append(wk) + keys_to_add = [public_keys] + if type(public_keys[0]) is list: + keys_to_add = list(zip(*public_keys)) + ms_keys = [] + for ms_key_cosigners in keys_to_add: + ms_keys.append(self._new_key_multisig(list(ms_key_cosigners), name, account_id, change, cosigner_id, + network, address_index, witness_type)) + if not ms_keys: return None + return ms_keys[0] if len(ms_keys) == 1 else ms_keys # Check for closest ancestor in wallet wpath = fullpath @@ -2196,10 +2259,15 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi topkey = self.key(dbkey.id) # Key already found in db, return key - if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate: + if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys == 1: return topkey else: - # Create 1 or more keys add them to wallet + if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys > 1: + nks = [topkey] + else: + # Create 1 or more keys add them to wallet + nks = [] + nk = None parent_id = topkey.key_id ck = topkey.key() @@ -2208,11 +2276,12 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi newpath = topkey.path n_items = len(str(dbkey.path).split('/')) for lvl in fullpath[n_items:]: + # parent_key = ck ck = ck.subkey_for_path(lvl, network=network) newpath += '/' + lvl if not account_id: - account_id = 0 if "account'" not in self.key_path or self.key_path.index("account'") >= len( - fullpath) \ + account_id = 0 if ("account'" not in self.key_path or + self.key_path.index("account'") >= len(fullpath)) \ else int(fullpath[self.key_path.index("account'")][:-1]) change = None if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ else int(fullpath[self.key_path.index("change")]) @@ -2227,7 +2296,30 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi cosigner_id=cosigner_id, network=network, session=self._session) self._key_objects.update({nk.key_id: nk}) parent_id = nk.key_id - return nk + if nk: + nks.append(nk) + if len(nks) < number_of_keys: + topkey = self._key_objects[nks[0].parent_id] + parent_key = topkey.key() + new_key_id = self._session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 + keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1]) + len(nks), int(fullpath[-1]) + + number_of_keys)] + + for key_idx in keys_to_add: + new_key_id += 1 + ck = parent_key.subkey_for_path(key_idx, network=network) + key_name = 'address index %s' % key_idx + newpath = '/'.join(newpath.split('/')[:-1] + [key_idx]) + nks.append(WalletKey.from_key( + key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, + change=change, purpose=purpose, path=newpath, parent_id=parent_id, + encoding=encoding, witness_type=witness_type, new_key_id=new_key_id, + cosigner_id=cosigner_id, network=network, session=self._session)) + self._session.commit() + nk = nks[0] if len(nks) == 1 else nks + if nk == []: nk = None + + return nk def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, used=None, is_private=None, has_balance=None, is_active=None, witness_type=None, network=None, include_private=False, as_dict=False): @@ -2259,6 +2351,8 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, :type has_balance: bool :param is_active: Hide inactive keys. Only include active keys with either a balance or which are unused, default is None (show all) :type is_active: bool + :param witness_type: Filter by witness_type + :type witness_type: str :param network: Network name filter :type network: str :param include_private: Include private key information in dictionary @@ -2863,7 +2957,6 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d change=change, depth=depth) random.shuffle(addresslist) srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) utxos = [] for address in addresslist: if rescan_all: diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1de8285c..1b2726ad 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -874,13 +874,12 @@ def test_wallet_multi_networks_send_transaction(self): wallet.new_key(account_id=acc.account_id, network='bitcoinlib_test') wallet.get_keys(network='testnet', number_of_keys=2) wallet.get_key(network='testnet', change=1) - # wallet.utxos_update(networks='testnet') self.assertEqual(wallet.balance(network='bitcoinlib_test', account_id=0), 0) self.assertEqual(wallet.balance(network='bitcoinlib_test', account_id=1), 600000000) self.assertEqual(wallet.balance(network='testnet'), 0) - ltct_addresses = ['mhHhSx66jdXdUPu2A8pXsCBkX1UvHmSkUJ', 'mrdtENj75WUfrJcZuRdV821tVzKA4VtCBf', + tbtc_addresses = ['mhHhSx66jdXdUPu2A8pXsCBkX1UvHmSkUJ', 'mrdtENj75WUfrJcZuRdV821tVzKA4VtCBf', 'mmWFgfG43tnP2SJ8u8UDN66Xm63okpUctk'] - self.assertListEqual(wallet.addresslist(network='testnet'), ltct_addresses) + self.assertListEqual(wallet.addresslist(network='testnet'), tbtc_addresses) t = wallet.send_to('21EsLrvFQdYWXoJjGX8LSEGWHFJDzSs2F35', 10000000, account_id=1, network='bitcoinlib_test', fee=1000, offline=False) From bf488c21e838fba85654fabd56e5db5185405b12 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 26 Jan 2024 17:51:28 +0100 Subject: [PATCH 061/207] Cleanup key create methods, update docstrings --- bitcoinlib/wallets.py | 181 +++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 83 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index ebbcd023..bd0f9daa 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1712,7 +1712,28 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, self._commit() return self.key(multisig_key.id) - def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): + """ + def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None): + + :param name: Key name. Does not have to be unique but if you use it at reference you might chooce to enforce this. If not specified 'Key #' with a unique sequence number will be used + :type name: str + :param account_id: Account ID. Default is last used or created account ID. + :type account_id: int + :param change: Change (1) or payments (0). Default is 0 + :type change: int + :param cosigner_id: Cosigner ID for key path + :type cosigner_id: int + :param witness_type: Use to create key with different witness_type + :type witness_type: str + :param network: Network name. Leave empty for default network + :type network: str + + :return WalletKey: + """ + return self.new_keys(name, account_id, change, cosigner_id, witness_type, 1, network)[0] + + def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, number_of_keys=1, network=None): """ Create a new HD Key derived from this wallet's masterkey. An account will be created for this wallet @@ -1732,21 +1753,21 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ :type cosigner_id: int :param witness_type: Use to create key with different witness_type :type witness_type: str - :param network: Network name. Leave empty for default network + :param number_of_keys: Number of keys to generate. Use positive integer + :type number_of_keys: int + :param network: Network name. Leave empty for default network :type network: str - :return WalletKey: + :return list of WalletKey: """ if self.scheme == 'single': - return self.main_key + return [self.main_key] network, account_id, _ = self._get_account_defaults(network, account_id) if network != self.network.name and "coin_type'" not in self.key_path: raise WalletError("Multiple networks not supported by wallet key structure") if self.multisig: - # if witness_type: - # TODO: raise error if not self.multisig_n_required: raise WalletError("Multisig_n_required not set, cannot create new key") if cosigner_id is None: @@ -1772,53 +1793,9 @@ def new_key(self, name='', account_id=None, change=0, cosigner_id=None, witness_ address_index = prevkey.address_index + 1 req_path = [change, address_index] - return self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, + return self.keys_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, cosigner_id=cosigner_id, address_index=address_index, number_of_keys=number_of_keys) - def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness_type=None, network=None, - number_of_keys=1): - if self.scheme == 'single': - return self.main_key - ret_keys = [] - - network, account_id, _ = self._get_account_defaults(network, account_id) - if network != self.network.name and "coin_type'" not in self.key_path: - raise WalletError("Multiple networks not supported by wallet key structure") - if self.multisig: - # if witness_type: - # TODO: raise error - if not self.multisig_n_required: - raise WalletError("Multisig_n_required not set, cannot create new key") - if cosigner_id is None: - if self.cosigner_id is None: - raise WalletError("Missing Cosigner ID value, cannot create new key") - cosigner_id = self.cosigner_id - witness_type = self.witness_type if not witness_type else witness_type - purpose = self.purpose - if witness_type != self.witness_type: - _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) - - address_index = 0 - if self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or self.cosigner[cosigner_id].key_path == ['m']): - req_path = [] - else: - prevkey = self._session.query(DbKey).\ - filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, - witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ - order_by(DbKey.address_index.desc()).first() - if prevkey: - address_index = prevkey.address_index + 1 - req_path = [change, address_index] - - for i in range(number_of_keys): - address_index += 1 - req_path = [change, address_index] - ret_keys.append(self.key_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, - network=network, cosigner_id=cosigner_id, address_index=address_index, commit=False)) - - self._session.commit() - return ret_keys - def new_key_change(self, name='', account_id=None, witness_type=None, network=None): """ Create new key to receive change for a transaction. Calls :func:`new_key` method with change=1. @@ -1969,10 +1946,10 @@ def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id if len(key_list) > number_of_keys: key_list = key_list[:number_of_keys] else: - new_keys = self.new_key(account_id=account_id, change=change, cosigner_id=cosigner_id, - witness_type=witness_type, network=network, - number_of_keys=number_of_keys - len(key_list)) - key_list = key_list + (new_keys if type(new_keys) is list else [new_keys]) + new_keys = self.new_keys(account_id=account_id, change=change, cosigner_id=cosigner_id, + witness_type=witness_type, network=network, + number_of_keys=number_of_keys - len(key_list)) + key_list += new_keys if as_list: return key_list @@ -1994,6 +1971,8 @@ def get_key(self, account_id=None, witness_type=None, network=None, cosigner_id= :param account_id: Account ID. Default is last used or created account ID. :type account_id: int + :param witness_type: Use to create key with specific witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param cosigner_id: Cosigner ID for key path @@ -2014,6 +1993,8 @@ def get_keys(self, account_id=None, witness_type=None, network=None, cosigner_id :param account_id: Account ID. Default is last used or created account ID. :type account_id: int + :param witness_type: Use to create key with specific witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param cosigner_id: Cosigner ID for key path @@ -2036,6 +2017,8 @@ def get_key_change(self, account_id=None, witness_type=None, network=None): :param account_id: Account ID. Default is last used or created account ID. :type account_id: int + :param witness_type: Use to create key with specific witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str @@ -2051,6 +2034,8 @@ def get_keys_change(self, account_id=None, witness_type=None, network=None, numb :param account_id: Account ID. Default is last used or created account ID. :type account_id: int + :param witness_type: Use to create key with specific witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str :param number_of_keys: Number of keys to return. Default is 1 @@ -2071,6 +2056,8 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) :type name: str :param account_id: Account ID. Default is last accounts ID + 1 :type account_id: int + :param witness_type: Use to create key with specific witness_type + :type witness_type: str :param network: Network name. Leave empty for default network :type network: str @@ -2151,8 +2138,39 @@ def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, a witness_type=self.witness_type, network=network) def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, - address_index=0, change=0, witness_type=None, network=None, recreate=False, - number_of_keys=1): + address_index=0, change=0, witness_type=None, network=None, recreate=False): + """ + Wrapper for the keys_for_path method. Returns a single wallet key. + + :param path: Part of key path, i.e. [0, 0] for [change=0, address_index=0] + :type path: list, str + :param level_offset: Just create part of path, when creating keys. For example -2 means create path with the last 2 items (change, address_index) or 1 will return the master key 'm' + :type level_offset: int + :param name: Specify key name for latest/highest key in structure + :type name: str + :param account_id: Account ID + :type account_id: int + :param cosigner_id: ID of cosigner + :type cosigner_id: int + :param address_index: Index of key, normally provided to 'path' argument + :type address_index: int + :param change: Change key = 1 or normal = 0, normally provided to 'path' argument + :type change: int + :param witness_type: Use to create key with different witness_type + :type witness_type: str + :param network: Network name. Leave empty for default network + :type network: str + :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info + :type recreate: bool + + :return WalletKey: + """ + return self.keys_for_path(path, level_offset, name, account_id, cosigner_id, address_index, change, + witness_type, network, recreate, 1)[0] + + def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cosigner_id=None, + address_index=0, change=0, witness_type=None, network=None, recreate=False, + number_of_keys=1): """ Return key for specified path. Derive all wallet keys in path if they not already exists @@ -2194,8 +2212,10 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi :type network: str :param recreate: Recreate key, even if already found in wallet. Can be used to update public key with private key info :type recreate: bool + :param number_of_keys: Number of keys to create, use to create keys in bulk fast + :type number_of_keys: int - :return WalletKey, list of WalletKey: + :return list of WalletKey: """ if number_of_keys == 0: @@ -2224,22 +2244,20 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi public_keys = [] for wlt in self.cosigner: if wlt.scheme == 'single': - wk = wlt.main_key + wk = [wlt.main_key] else: - wk = wlt.key_for_path(path, level_offset=level_offset, account_id=account_id, name=name, + wk = wlt.keys_for_path(path, level_offset=level_offset, account_id=account_id, name=name, cosigner_id=cosigner_id, network=network, recreate=recreate, witness_type=witness_type, number_of_keys=number_of_keys) - public_keys.append(wk if type(wk) == list else [wk]) - # public_keys.append(wk) + public_keys.append(wk) keys_to_add = [public_keys] if type(public_keys[0]) is list: keys_to_add = list(zip(*public_keys)) - ms_keys = [] + new_ms_keys = [] for ms_key_cosigners in keys_to_add: - ms_keys.append(self._new_key_multisig(list(ms_key_cosigners), name, account_id, change, cosigner_id, + new_ms_keys.append(self._new_key_multisig(list(ms_key_cosigners), name, account_id, change, cosigner_id, network, address_index, witness_type)) - if not ms_keys: return None - return ms_keys[0] if len(ms_keys) == 1 else ms_keys + return new_ms_keys if new_ms_keys else None # Check for closest ancestor in wallet wpath = fullpath @@ -2260,15 +2278,15 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi # Key already found in db, return key if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys == 1: - return topkey + return [topkey] else: if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys > 1: - nks = [topkey] + new_keys = [topkey] else: # Create 1 or more keys add them to wallet - nks = [] + new_keys = [] - nk = None + nkey = None parent_id = topkey.key_id ck = topkey.key() ck.witness_type = witness_type @@ -2276,7 +2294,6 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi newpath = topkey.path n_items = len(str(dbkey.path).split('/')) for lvl in fullpath[n_items:]: - # parent_key = ck ck = ck.subkey_for_path(lvl, network=network) newpath += '/' + lvl if not account_id: @@ -2290,19 +2307,19 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi else: key_name = "%s %s" % (self.key_path[len(newpath.split('/'))-1], lvl) key_name = key_name.replace("'", "").replace("_", " ") - nk = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, + nkey = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=purpose, path=newpath, parent_id=parent_id, encoding=encoding, witness_type=witness_type, cosigner_id=cosigner_id, network=network, session=self._session) - self._key_objects.update({nk.key_id: nk}) - parent_id = nk.key_id - if nk: - nks.append(nk) - if len(nks) < number_of_keys: - topkey = self._key_objects[nks[0].parent_id] + self._key_objects.update({nkey.key_id: nkey}) + parent_id = nkey.key_id + if nkey: + new_keys.append(nkey) + if len(new_keys) < number_of_keys: + topkey = self._key_objects[new_keys[0].parent_id] parent_key = topkey.key() new_key_id = self._session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 - keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1]) + len(nks), int(fullpath[-1]) + + keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1]) + len(new_keys), int(fullpath[-1]) + number_of_keys)] for key_idx in keys_to_add: @@ -2310,16 +2327,14 @@ def key_for_path(self, path, level_offset=None, name=None, account_id=None, cosi ck = parent_key.subkey_for_path(key_idx, network=network) key_name = 'address index %s' % key_idx newpath = '/'.join(newpath.split('/')[:-1] + [key_idx]) - nks.append(WalletKey.from_key( + new_keys.append(WalletKey.from_key( key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=purpose, path=newpath, parent_id=parent_id, encoding=encoding, witness_type=witness_type, new_key_id=new_key_id, cosigner_id=cosigner_id, network=network, session=self._session)) self._session.commit() - nk = nks[0] if len(nks) == 1 else nks - if nk == []: nk = None - return nk + return new_keys def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, used=None, is_private=None, has_balance=None, is_active=None, witness_type=None, network=None, include_private=False, as_dict=False): From ed983414ac3e46a233468c03edfc9f416271ad7a Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 27 Jan 2024 00:33:54 +0100 Subject: [PATCH 062/207] Add unittest for multisig multinetwork wallets --- bitcoinlib/wallets.py | 3 +++ tests/test_wallets.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index bd0f9daa..e41f7f50 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2276,6 +2276,9 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos else: topkey = self.key(dbkey.id) + if topkey.network != network and topkey.path.split('/') == fullpath: + raise WalletError("Cannot create new keys for network %s, no private masterkey found" % network) + # Key already found in db, return key if dbkey and dbkey.path == normalize_path('/'.join(fullpath)) and not recreate and number_of_keys == 1: return [topkey] diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1b2726ad..41898565 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1500,6 +1500,24 @@ def test_wallet_multisig_replace_sig_bug(self): key_pool.remove(co_id) self.assertTrue(t.verify()) + def test_wallet_multisig_multinetwork(self): + network1 = 'litecoin' + network2 = 'bitcoinlib_test' + p1 = 'only sing document speed outer gauge stand govern way column material odor' + p2 = 'oyster pelican debate mass scene title pipe lock recipe flavor razor accident' + k2 = HDKey.from_passphrase(p2, network=network1, multisig=True).public_master() + w = wallet_create_or_open('ltcswms', [p1, k2], network=network1, witness_type='segwit', + cosigner_id=0, db_uri=self.DATABASE_URI) + self.assertEqual(len(w.get_keys(number_of_keys=2)), 2) + w.utxo_add('ltc1qkewaz7lxn75y6wppvqlsfhrnq5p5mksmlp26n8xsef0556cdfzqq2uhdrt', 2100000000000001, + '21da13be453624cf46b3d883f39602ce74d04efa7a186037898b6d7bcfd405ee', 0, 15) + t = w.sweep('ltc1q9h8xvtrge5ttcwzy3xtz7l8kj4dewgh6hgqfjdhtq6lwr4k3527qd8tyzs') + self.assertFalse(t.verified) + t.sign(p2) + self.assertTrue(t.verified) + self.assertRaisesRegex(WalletError, "Cannot create new keys for network bitcoinlib_test, " + "no private masterkey found", w.new_key, network=network2) + @parameterized_class(*params) class TestWalletKeyImport(TestWalletMixin, unittest.TestCase): From f2542a9dd77d99749a2e7d61f9baf53ade6a8ed5 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 2 Feb 2024 19:04:02 +0100 Subject: [PATCH 063/207] Add tool to test speed of bitcoinlib library --- bitcoinlib/data/providers.old.json | 476 ----------------------------- bitcoinlib/tools/benchmark.py | 150 +++++++++ 2 files changed, 150 insertions(+), 476 deletions(-) delete mode 100644 bitcoinlib/data/providers.old.json create mode 100644 bitcoinlib/tools/benchmark.py diff --git a/bitcoinlib/data/providers.old.json b/bitcoinlib/data/providers.old.json deleted file mode 100644 index 5e5ad934..00000000 --- a/bitcoinlib/data/providers.old.json +++ /dev/null @@ -1,476 +0,0 @@ -{ - "bitcoinlib_test": { - "provider": "bitcoinlibtest", - "network": "bitcoinlib_test", - "client_class": "BitcoinLibTestClient", - "provider_coin_id": "", - "url": "local", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchaininfo": { - "provider": "blockchaininfo", - "network": "bitcoin", - "client_class": "BlockchainInfoClient", - "provider_coin_id": "", - "url": "https://blockchain.info/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher": { - "provider": "blockcypher", - "network": "bitcoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/btc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "bitgo": { - "provider": "bitgo", - "network": "bitcoin", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://www.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "bitgo.testnet": { - "provider": "bitgo", - "network": "testnet", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://test.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "coinfees": { - "provider": "coinfees", - "network": "bitcoin", - "client_class": "CoinfeesClient", - "provider_coin_id": "", - "url": "https://bitcoinfees.earn.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher.testnet": { - "provider": "blockcypher", - "network": "testnet", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/btc/test3/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "blockcypher.litecoin": { - "provider": "blockcypher", - "network": "litecoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/ltc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": {"prefix_address_p2sh": "05"} - }, - "blockcypher.litecoin.legacy": { - "provider": "blockcypher", - "network": "litecoin_legacy", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/ltc/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, - "cryptoid.litecoin": { - "provider": "cryptoid", - "network": "litecoin", - "client_class": "CryptoID", - "provider_coin_id": "ltc", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "cryptoid.litecoin.legacy": { - "provider": "cryptoid", - "network": "litecoin_legacy", - "client_class": "CryptoID", - "provider_coin_id": "ltc", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": {"prefix_address_p2sh": "32"} - }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoreio.litecoin": { - "provider": "litecoreio", - "network": "litecoin", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://insight.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoreio.litecoin.legacy": { - "provider": "litecoreio", - "network": "litecoin_legacy", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://insight.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": {"prefix_address_p2sh": "32"} - }, - "litecoreio.litecoin.testnet": { - "provider": "litecoreio", - "network": "litecoin_testnet", - "client_class": "LitecoreIOClient", - "provider_coin_id": "", - "url": "https://testnet.litecore.io/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair": { - "provider": "blockchair", - "network": "bitcoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/bitcoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.testnet": { - "provider": "blockchair", - "network": "testnet", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/bitcoin/testnet/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.litecoin": { - "provider": "blockchair", - "network": "litecoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/litecoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.dash": { - "provider": "blockchair", - "network": "dash", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dash/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockchair.dogecoin": { - "provider": "blockchair", - "network": "dogecoin", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dogecoin/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps": { - "provider": "bitaps", - "network": "bitcoin", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.testnet": { - "provider": "bitaps", - "network": "testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin": { - "provider": "bitaps", - "network": "litecoin", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin.legacy": { - "provider": "bitaps", - "network": "litecoin_legacy", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "bitaps.litecoin.testnet": { - "provider": "bitaps", - "network": "litecoin_testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/ltc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoinblockexplorer.litecoin": { - "provider": "litecoinblockexplorer", - "network": "litecoin", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://litecoinblockexplorer.net/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "litecoinblockexplorer.litecoin.legacy": { - "provider": "litecoinblockexplorer", - "network": "litecoin_legacy", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://litecoinblockexplorer.net/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockstream": { - "provider": "blockstream", - "network": "bitcoin", - "client_class": "BlockstreamClient", - "provider_coin_id": "", - "url": "https://blockstream.info/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blockstream.testnet": { - "provider": "blockstream", - "network": "testnet", - "client_class": "BlockstreamClient", - "provider_coin_id": "", - "url": "https://blockstream.info/testnet/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "blocksmurfer": { - "provider": "blocksmurfer", - "network": "bitcoin", - "client_class": "BlocksmurferClient", - "provider_coin_id": "", - "url": "http://blocksmurfer.io/api/v1/btc/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin.testnet": { - "provider": "chainso", - "network": "litecoin_testnet", - "client_class": "ChainSo", - "provider_coin_id": "LTCTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso": { - "provider": "chainso", - "network": "bitcoin", - "client_class": "ChainSo", - "provider_coin_id": "BTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 8, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.testnet": { - "provider": "chainso", - "network": "testnet", - "client_class": "ChainSo", - "provider_coin_id": "BTCTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin": { - "provider": "chainso", - "network": "litecoin", - "client_class": "ChainSo", - "provider_coin_id": "LTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.litecoin.legacy": - { - "provider": "chainso", - "network": "litecoin_legacy", - "client_class": "ChainSo", - "provider_coin_id": "LTC", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash": { - "provider": "chainso", - "network": "dash", - "client_class": "ChainSo", - "provider_coin_id": "DASH", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash.testnet": { - "provider": "chainso", - "network": "dash_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DASHTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dogecoin": { - "provider": "chainso", - "network": "dogecoin", - "client_class": "ChainSo", - "provider_coin_id": "DOGE", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dogecoin.testnet": { - "provider": "chainso", - "network": "dogecoin_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DOGETEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "dogecoin": { - "provider": "dogecoind", - "network": "dogecoin", - "client_class": "DogecoindClient", - "provider_coin_id": "", - "url": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "dogecoind.testnet": { - "provider": "dogecoind", - "network": "dogecoin_testnet", - "client_class": "DogecoindClient", - "provider_coin_id": "", - "url": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "blockcypher.dogecoin": { - "provider": "blockcypher", - "network": "dogecoin", - "client_class": "BlockCypher", - "provider_coin_id": "", - "url": "https://api.blockcypher.com/v1/doge/main/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - } -} diff --git a/bitcoinlib/tools/benchmark.py b/bitcoinlib/tools/benchmark.py new file mode 100644 index 00000000..db07579e --- /dev/null +++ b/bitcoinlib/tools/benchmark.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# Benchmark - Test library speed +# © 2020 - 2024 Februari - 1200 Web Development +# + + +import time +import random +import json +from bitcoinlib.keys import * +from bitcoinlib.wallets import * +from bitcoinlib.transactions import * +from bitcoinlib.mnemonic import * + +try: + wallet_method = Wallet +except NameError: + wallet_method = HDWallet + +try: + BITCOINLIB_VERSION +except: + BITCOINLIB_VERSION = '0.4.10 and below' + + +class Benchmark: + + def __init__(self): + wallet_delete_if_exists('wallet_multisig_huge', force=True) + wallet_delete_if_exists('wallet_large', force=True) + + @staticmethod + def benchmark_bip38(): + # Encrypt and decrypt BIP38 key + k = Key() + if BITCOINLIB_VERSION > '0.6.0': + bip38_key = k.encrypt(password='satoshi') + else: + bip38_key = k.bip38_encrypt('satoshi') + if BITCOINLIB_VERSION > '0.5.1': + k2 = Key(bip38_key, password='satoshi') + else: + k2 = Key(bip38_key, passphrase='satoshi') + assert(k.wif() == k2.wif()) + + @staticmethod + def benchmark_create_key(): + for i in range(0, 1000): + k = Key() + + @staticmethod + def benchmark_create_hdkey(): + for i in range(0, 1000): + k = HDKey() + + @staticmethod + def benchmark_encoding(): + # Convert very large numbers to and from base58 / bech32 + pk = random.randint(0, 10 ** 4000) + large_b58 = change_base(pk, 10, 58, 6000) + large_b32 = change_base(pk, 10, 32, 7000) + assert(change_base(large_b58, 58, 10) == pk) + assert(change_base(large_b32, 32, 10) == pk) + + @staticmethod + def benchmark_mnemonic(): + # Generate Mnemonic passphrases + for i in range(100): + m = Mnemonic().generate(256) + Mnemonic().to_entropy(m, includes_checksum=False) + + @staticmethod + def benchmark_transactions(): + # Deserialize transaction and verify + raw_hex = "02000000000101b7006080d9d1d2928f70be1140d4af199d6ba4f9a7b0096b6461d7d4d16a96470600000000fdffffff11205c0600000000001976a91416e7a7d921edff13eaf5831eefd6aaca5728d7fb88acad960700000000001600140dd69a4ce74f03342cd46748fc40a877c7ccef0e808b08000000000017a914bd27a59ba92179389515ecea6b87824a42e002ee873efb0b0000000000160014b4a3a8da611b66123c19408c289faa04c71818d178b21100000000001976a914496609abfa498b6edbbf83e93fd45c1934e05b9888ac34d01900000000001976a9144d1ce518b35e19f413963172bd2c84bd90f8f23488ace06e1f00000000001976a914440d99e9e2879c1b0f8e9a1d5a288a4b6cfcc15288acff762c000000000016001401429b4b17e97f8d4419b4594ffe9f54e85037e7241e4500000000001976a9146083df8eb862f759ea0f1c04d3f13a3dfa9aff5888acf09056000000000017a9144fcaf4edac9da6890c09a819d0d7b8f300edbe478740fa97000000000017a9147431dcb6061217b0c80c6fa0c0256c1221d74b4a87208e9c000000000017a914a3e1e764fefa92fc5befa179b2b80afd5a9c20bd87ecf09f000000000017a9142ca7dc95f76530521a1edfc439586866997a14828754900101000000001976a9142e6c1941e2f9c47b535d0cf5dc4be5038e02336588acc0996d01000000001976a91492268fb9d7b8a3c825a4efc486a0679dbf006fae88acd790ae0300000000160014fe350625e2887e9bc984a69a7a4f60439e7ee7152182c81300000000160014f60834ef165253c571b11ce9fa74e46692fc5ec10248304502210081cb31e1b53a36409743e7c785e00d5df7505ca2373a1e652fec91f00c15746b02203167d7cc1fa43e16d411c620b90d9516cddac31d9e44e452651f50c950dc94150121026e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea28600000000" + for i in range(100): + if BITCOINLIB_VERSION >= '0.5.3': + t = Transaction.parse(raw_hex) + else: + t = Transaction.import_raw(raw_hex) + t.inputs[0].value = 485636658 + t.verify() + assert(t.verified is True) + + @staticmethod + def benchmark_wallets_multisig(): + # Create large multisig wallet + network = 'bitcoinlib_test' + n_keys = 8 + sigs_req = 5 + key_list = [HDKey(network=network) for _ in range(0, n_keys)] + pk_n = random.randint(0, n_keys - 1) + key_list_cosigners = [k.public_master(multisig=True) for k in key_list if k is not key_list[pk_n]] + key_list_wallet = [key_list[pk_n]] + key_list_cosigners + w = wallet_method.create('wallet_multisig_huge', keys=key_list_wallet, sigs_required=sigs_req, network=network) + + if BITCOINLIB_VERSION >= '0.5.0': + w.get_keys(number_of_keys=2) + else: + w.get_key(number_of_keys=2) + w.utxos_update() + to_address = HDKey(network=network).address() + t = w.sweep(to_address, offline=True) + key_pool = [i for i in range(0, n_keys - 1) if i != pk_n] + while len(t.inputs[0].signatures) < sigs_req: + co_id = random.choice(key_pool) + t.sign(key_list[co_id]) + key_pool.remove(co_id) + assert(t.verify() is True) + + @staticmethod + def benchmark_wallets_large(): + # Create large wallet with many keys + network = 'bitcoinlib_test' + n_keys = 250 + w = wallet_method.create('wallet_large', network=network) + if BITCOINLIB_VERSION >= '0.5.0': + w.get_keys(number_of_keys=n_keys) + else: + w.get_key(number_of_keys=n_keys) + + def run(self, only_dict=False): + start_time = time.time() + bench_dict = {'version': BITCOINLIB_VERSION} + only_dict or print("Running BitcoinLib benchmarks speed test for version %s" % BITCOINLIB_VERSION) + + benchmark_methods = [m for m in dir(self) if callable(getattr(self, m)) if m.startswith('benchmark_')] + for method in benchmark_methods: + m_start_time = time.time() + try: + getattr(self, method)() + except Exception as e: + only_dict or print("Error occured running test: %s" % str(e)) + m_duration = 0 + else: + m_duration = time.time() - m_start_time + only_dict or print("%s, %.5f seconds" % (method, m_duration)) + bench_dict.update({method: m_duration}) + + duration = time.time() - start_time + only_dict or print("Total running time: %.5f seconds" % duration) + bench_dict.update({'duration': duration}) + return bench_dict + + +if __name__ == '__main__': + res = Benchmark().run(bool(sys.argv[1:])) + print(json.dumps(res)) From 67437e4f0acc0acc0e27c0369ee739fc200e2461 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 3 Feb 2024 23:04:51 +0100 Subject: [PATCH 064/207] Fix mysql issues and database unittesting --- bitcoinlib/db.py | 10 +- tests/test_tools.py | 161 ++++++------ tests/test_wallets.py | 592 ++++++++++++++++++++++-------------------- 3 files changed, 394 insertions(+), 369 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index b3ebadac..f5d966d7 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -286,7 +286,7 @@ class DbKey(Base): "depth=1 are the masterkeys children.") change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(32), index=True, doc="Bytes representation of public key") + public = Column(LargeBinary(33), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") @@ -309,7 +309,7 @@ class DbKey(Base): "Default is False") network_name = Column(String(20), ForeignKey('networks.name'), doc="Name of key network, i.e. bitcoin, litecoin, dash") - latest_txid = Column(LargeBinary(32), doc="TxId of latest transaction downloaded from the blockchain") + latest_txid = Column(LargeBinary(33), doc="TxId of latest transaction downloaded from the blockchain") witness_type = Column(String(20), default='segwit', doc="Key witness type, only specify when using mixed wallets. Can be 'legacy', 'segwit' or " "'p2sh-segwit'. Default is segwit.") @@ -369,7 +369,7 @@ class DbTransaction(Base): __tablename__ = 'transactions' id = Column(Integer, Sequence('transaction_id_seq'), primary_key=True, doc="Unique transaction index for internal usage") - txid = Column(LargeBinary(32), index=True, doc="Bytes representation of transaction ID") + txid = Column(LargeBinary(33), index=True, doc="Bytes representation of transaction ID") wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, doc="ID of wallet which contains this transaction") account_id = Column(Integer, index=True, doc="ID of account") @@ -443,7 +443,7 @@ class DbTransactionInput(Base): witnesses = Column(LargeBinary, doc="Witnesses (signatures) used in Segwit transaction inputs") witness_type = Column(String(20), default='segwit', doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is segwit") - prev_txid = Column(LargeBinary(32), + prev_txid = Column(LargeBinary(33), doc="Transaction hash of previous transaction. Previous unspent outputs (UTXO) is spent " "in this input") output_n = Column(BigInteger, doc="Output_n of previous transaction output that is spent in this input") @@ -487,7 +487,7 @@ class DbTransactionOutput(Base): "'nulldata', 'unknown', 'p2wpkh', 'p2wsh', 'p2tr'. Default is p2pkh") value = Column(BigInteger, default=0, doc="Total transaction output value") spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction") - spending_txid = Column(LargeBinary(32), doc="Transaction hash of input which spends this output") + spending_txid = Column(LargeBinary(33), doc="Transaction hash of input which spends this output") spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output") __table_args__ = (UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique'),) diff --git a/tests/test_tools.py b/tests/test_tools.py index 6eb62a22..91855a4b 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -4,6 +4,7 @@ # Unit Tests for Bitcoinlib Tools # © 2018 - 2024 January - 1200 Web Development # + import ast import os import sys @@ -13,7 +14,6 @@ try: import mysql.connector import psycopg2 - from parameterized import parameterized_class from psycopg2 import sql from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT except ImportError: @@ -27,60 +27,61 @@ DATABASE_NAME = 'bitcoinlib_unittest' -def init_sqlite(_): - if os.path.isfile(SQLITE_DATABASE_FILE): - os.remove(SQLITE_DATABASE_FILE) - - -def init_postgresql(_): - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - cur = con.cursor() - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( - sql.Identifier(DATABASE_NAME)) - ) - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier(DATABASE_NAME)) - ) - cur.close() - con.close() - - -def init_mysql(_): - con = mysql.connector.connect(user='root', host='localhost') - cur = con.cursor() - cur.execute("DROP DATABASE IF EXISTS {}".format(DATABASE_NAME)) - cur.execute("CREATE DATABASE {}".format(DATABASE_NAME)) - con.commit() - cur.close() - con.close() - - -# db_uris = (('sqlite:///' + SQLITE_DATABASE_FILE, init_sqlite),) -# if UNITTESTS_FULL_DATABASE_TEST: -# db_uris += ( -# ('mysql://root@localhost:3306/' + DATABASE_NAME, init_mysql), -# ('postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, init_postgresql), -# ) +def database_init(): + + def init_sqlite(): + if os.path.isfile(SQLITE_DATABASE_FILE): + os.remove(SQLITE_DATABASE_FILE) + + + def init_postgresql(): + con = psycopg2.connect(user='postgres', host='localhost', password='postgres') + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( + sql.Identifier(DATABASE_NAME)) + ) + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier(DATABASE_NAME)) + ) + cur.close() + con.close() + + + def init_mysql(): + con = mysql.connector.connect(user='user', host='localhost', password='password') + cur = con.cursor() + cur.execute("DROP DATABASE IF EXISTS {}".format(DATABASE_NAME)) + cur.execute("CREATE DATABASE {}".format(DATABASE_NAME)) + con.commit() + cur.close() + con.close() + + if os.getenv('UNITTEST_DATABASE') == 'mysql': + init_mysql() + return 'mysql://user:password@localhost:3306/' + DATABASE_NAME + elif os.getenv('UNITTEST_DATABASE') == 'postgresql': + init_postgresql() + return 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME + else: + init_sqlite() + return SQLITE_DATABASE_FILE -# @parameterized_class(('DATABASE_URI', 'init_fn'), db_uris) class TestToolsCommandLineWallet(unittest.TestCase): def setUp(self): - # self.init_fn() - init_sqlite(self) self.python_executable = sys.executable self.clw_executable = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../bitcoinlib/tools/clw.py')) - self.DATABASE_URI = SQLITE_DATABASE_FILE + self.database_uri = database_init() def test_tools_clw_create_wallet(self): cmd_wlt_create = '%s %s new -w test --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ 'actual chicken obscure spray" -r -d %s' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_delete = "%s %s -w test --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "bc1qdv5tuzrluh4lzhnu59je9n83w4hkqjhgg44d5g" output_wlt_delete = "Wallet test has been removed" @@ -99,9 +100,9 @@ def test_tools_clw_create_multisig_wallet(self): 'MeQHdWDp' ] cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s -o 0" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) cmd_wlt_delete = "%s %s -w testms --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "2NBrLTapyFqU4Wo29xG4QeEt8kn38KVWRR" output_wlt_delete = "Wallet testms has been removed" @@ -118,9 +119,9 @@ def test_tools_clw_create_multisig_wallet_one_key(self): '5zNYeiX8' ] cmd_wlt_create = "%s %s new -w testms1 -m 2 2 %s -r -n testnet -d %s -o 0" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) cmd_wlt_delete = "%s %s -w testms1 --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "if you understood and wrote down your key: Receive address:" output_wlt_delete = "Wallet testms1 has been removed" @@ -133,7 +134,7 @@ def test_tools_clw_create_multisig_wallet_one_key(self): def test_tools_clw_create_multisig_wallet_error(self): cmd_wlt_create = "%s %s new -w testms2 -m 2 a -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "Number of total signatures (second argument) must be a numeric value" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) poutput = process.communicate(input=b'y') @@ -142,13 +143,13 @@ def test_tools_clw_create_multisig_wallet_error(self): def test_tools_clw_transaction_with_script(self): cmd_wlt_create = '%s %s new -w test2 --passphrase "emotion camp sponsor curious bacon squeeze bean world ' \ 'actual chicken obscure spray" -r -n bitcoinlib_test -d %s' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_update = "%s %s -w test2 -x -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_transaction = "%s %s -w test2 -d %s -s 21HVXMEdxdgjNzgfERhPwX4okXZ8WijHkvu 0.5 -f 100000 -p" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_delete = "%s %s -w test2 --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "blt1qj0mgwyhxuw9p0ngj5kqnxhlrx8ypecqekm2gr7" output_wlt_transaction = 'Transaction pushed to network' output_wlt_delete = "Wallet test2 has been removed" @@ -171,9 +172,9 @@ def test_tools_clw_transaction_with_script(self): def test_tools_clw_create_litecoin_segwit_wallet(self): cmd_wlt_create = '%s %s new -w ltcsw --passphrase "lounge chief tip frog camera build trouble write end ' \ 'sword order share" -d %s -j segwit -n litecoin -r' % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_delete = "%s %s -w ltcsw --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "ltc1qgc7c2z56rr4lftg0fr8tgh2vknqc3yuydedu6m" output_wlt_delete = "Wallet ltcsw has been removed" @@ -194,9 +195,9 @@ def test_tools_clw_create_multisig_wallet_p2sh_segwit(self): '7FW6wpKW' ] cmd_wlt_create = "%s %s new -w testms-p2sh-segwit -m 2 3 %s -r -j p2sh-segwit -d %s -o 0" % \ - (self.python_executable, self.clw_executable, ' '.join(key_list), self.DATABASE_URI) + (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) cmd_wlt_delete = "%s %s -w testms-p2sh-segwit --wallet-remove -d %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "3MtNi5U2cjs3EcPizzjarSz87pU9DTANge" output_wlt_delete = "Wallet testms-p2sh-segwit has been removed" @@ -218,7 +219,7 @@ def test_tools_wallet_create_from_key(self): phrase = ("hover rescue clock ocean strategy post melt banner anxiety phone pink paper enhance more " "copy gate bag brass raise logic stone duck muffin conduct") cmd_wlt_create = "%s %s new -w wlt_from_key -c \"%s\" -d %s -y" % \ - (self.python_executable, self.clw_executable, phrase, self.DATABASE_URI) + (self.python_executable, self.clw_executable, phrase, self.database_uri) output_wlt_create = "bc1qpylcrcyqa5wkwe2stzc6h7q0mhs5skxuas44w2" poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() @@ -228,11 +229,11 @@ def test_tools_wallet_send_to_multi(self): send_str = ("-s blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz 0.1 " "-s blt1qu825hm0a6ajg66j79x4tzkn56qmljjms97c5tp 1") cmd_wlt_create = "%s %s new -w wallet_send_to_multi -d %s -n bitcoinlib_test -yq" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_update = "%s %s -w wallet_send_to_multi -d %s -x" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_send = "%s %s -w wallet_send_to_multi -d %s %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, send_str) + (self.python_executable, self.clw_executable, self.database_uri, send_str) Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() @@ -243,31 +244,31 @@ def test_tools_wallet_empty(self): pk = ("zprvAWgYBBk7JR8GiejuVoZaVXtWf5zNawFbTH88uKao9qnZxBypJQNvh1tGHZghpfjUfSUiS7G7MmNw3cyakkNcNis3MjD4ic54n" "FY5LQxMszQ") cmd_wlt_create = "%s %s new -w wlt_create_and_empty -c %s -d %s -y" % \ - (self.python_executable, self.clw_executable, pk, self.DATABASE_URI) + (self.python_executable, self.clw_executable, pk, self.database_uri) output_wlt_create = "bc1qqnqkjpnmr5zsxar76wxqcntp28ltly0fz6crdg" poutput = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() self.assertIn(output_wlt_create, normalize_string(poutput[0])) cmd_wlt_empty = "%s %s -w wlt_create_and_empty -d %s --wallet-empty" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) poutput = Popen(cmd_wlt_empty, stdin=PIPE, stdout=PIPE, shell=True).communicate() self.assertIn("Removed transactions and emptied wallet", normalize_string(poutput[0])) cmd_wlt_info = "%s %s -w wlt_create_and_empty -d %s -i" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) poutput = Popen(cmd_wlt_info, stdin=PIPE, stdout=PIPE, shell=True).communicate() self.assertIn("- - Transactions Account 0 (0)", normalize_string(poutput[0])) self.assertNotIn(output_wlt_create, normalize_string(poutput[0])) def test_tools_wallet_sweep(self): cmd_wlt_create = "%s %s new -w wlt_sweep -d %s -n bitcoinlib_test -yq" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_update = "%s %s -w wlt_sweep -d %s -x" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_send = "%s %s -w wlt_sweep -d %s --sweep blt1qzt90vqqjsqspuaegu9fh4e2htaxrgt0l76d9gz -p" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_info = "%s %s -w wlt_sweep -d %s -i" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() process = Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True) @@ -290,34 +291,34 @@ def test_tools_wallet_multisig_cosigners(self): pub_key3 = ('BC11mYrL5yBtMgaYxHEUg3anvLX3gcLi8hbtwbjymReCgGiP6hYifVMi96M3ejtvZpZbDvetBfbzgRxmu22ZkqP2i7yhFge' 'mSkHp7BRhoDubrQvs') cmd_wlt_create1 = "%s %s new -w wlt_multisig_2_3_A -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ - (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.DATABASE_URI) + (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.database_uri) Popen(cmd_wlt_create1, stdin=PIPE, stdout=PIPE, shell=True).communicate() cmd_wlt_create2 = "%s %s new -w wlt_multisig_2_3_B -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ - (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.DATABASE_URI) + (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.database_uri) Popen(cmd_wlt_create2, stdin=PIPE, stdout=PIPE, shell=True).communicate() cmd_wlt_receive1 = "%s %s -w wlt_multisig_2_3_A -d %s -r -o 1 -q" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output1 = Popen(cmd_wlt_receive1, stdin=PIPE, stdout=PIPE, shell=True).communicate() cmd_wlt_receive2 = "%s %s -w wlt_multisig_2_3_B -d %s -r -o 1 -q" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output2 = Popen(cmd_wlt_receive2, stdin=PIPE, stdout=PIPE, shell=True).communicate() self.assertEqual(output1[0], output2[0]) address = normalize_string(output1[0].strip(b'\n')) cmd_wlt_update1 = "%s %s -w wlt_multisig_2_3_A -d %s -x -o 1" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) Popen(cmd_wlt_update1, stdin=PIPE, stdout=PIPE, shell=True).communicate() cmd_wlt_update2 = "%s %s -w wlt_multisig_2_3_B -d %s -x -o 1" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate() create_tx = "%s %s -w wlt_multisig_2_3_A -d %s -s %s 0.5 -o 1" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, address) + (self.python_executable, self.clw_executable, self.database_uri, address) output = Popen(create_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() tx_dict_str = '{' + normalize_string(output[0]).split('{', 1)[1] sign_tx = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx \"%s\"" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, + (self.python_executable, self.clw_executable, self.database_uri, tx_dict_str.replace('\r', '').replace('\n', '')) output = Popen(sign_tx, stdin=PIPE, stdout=PIPE, shell=True).communicate() response = normalize_string(output[0]) @@ -327,7 +328,7 @@ def test_tools_wallet_multisig_cosigners(self): filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'import_test.tx') sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file %s" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, filename) + (self.python_executable, self.clw_executable, self.database_uri, filename) output = Popen(sign_import_tx_file, stdin=PIPE, stdout=PIPE, shell=True).communicate() response2 = normalize_string(output[0]) self.assertIn('2e07be62d933f5b257ac066b874df651cd6e6763795c24036904024a2b44180b', response2) @@ -337,12 +338,12 @@ def test_tools_wallet_multisig_cosigners(self): def test_tools_transaction_options(self): cmd_wlt_create = "%s %s new -w test_tools_transaction_options -d %s -n bitcoinlib_test -yq" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_update = "%s %s -w test_tools_transaction_options -d %s -x" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) cmd_wlt_send = ("%s %s -w test_tools_transaction_options -d %s -s blt1qg7du8cs0scxccmfly7x252qurv7kwsy6rm4xr7 0.001 " "--number-of-change-outputs 5") % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True).communicate() Popen(cmd_wlt_update, stdin=PIPE, stdout=PIPE, shell=True).communicate() output = normalize_string(Popen(cmd_wlt_send, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) @@ -352,14 +353,14 @@ def test_tools_transaction_options(self): self.assertTrue(tx_dict['verified']) cmd_wlt_update2 = "%s %s -w test_tools_transaction_options -d %s -ix" % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI) + (self.python_executable, self.clw_executable, self.database_uri) output = normalize_string(Popen(cmd_wlt_update2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) output_list = [i for i in output.split('Keys')[1].split(' ') if i != ''] first_key_id = int(output_list[1]) address = output_list[3] cmd_wlt_send2 = ("%s %s -w test_tools_transaction_options -d %s " "-s blt1qdjre3yw9hnt53entkp6tflhg34y4sp999emjnk 0.5 -k %d") % \ - (self.python_executable, self.clw_executable, self.DATABASE_URI, first_key_id) + (self.python_executable, self.clw_executable, self.database_uri, first_key_id) output = normalize_string(Popen(cmd_wlt_send2, stdin=PIPE, stdout=PIPE, shell=True).communicate()[0]) self.assertIn(address, output) self.assertIn("Transaction created", output) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 41898565..23b447bb 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -43,6 +43,8 @@ DATABASEFILE_UNITTESTS_2 = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest2.sqlite') DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' +# SQLITE_DATABASE_FILE = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % DATABASE_NAME) +# SQLITE_DATABASE_FILE2 = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % DATABASE_NAME_2) db_uris = ( ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) @@ -134,16 +136,45 @@ def db_remove(cls): con.close() -@parameterized_class(*params) -class TestWalletCreate(TestWalletMixin, unittest.TestCase): +def database_init(dbname=DATABASE_NAME): + + print(os.getenv('UNITTEST_DATABASE')) + if os.getenv('UNITTEST_DATABASE') == 'postgresql': + con = psycopg2.connect(user='postgres', host='localhost', password='postgres') + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( + sql.Identifier(dbname)) + ) + cur.close() + con.close() + return 'postgresql://postgres:postgres@localhost:5432/' + dbname + elif os.getenv('UNITTEST_DATABASE') == 'mysql': + con = mysql.connector.connect(user='user', host='localhost', password='password') + cur = con.cursor() + cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) + cur.execute("CREATE DATABASE {}".format(dbname)) + con.commit() + cur.close() + con.close() + return 'mysql://user:password@localhost:3306/' + dbname + else: + dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) + if os.path.isfile(dburi): + os.remove(dburi) + return dburi + + +class TestWalletCreate(unittest.TestCase): wallet = None @classmethod def setUpClass(cls): - cls.db_remove() + # cls.database_uri = database_init() + cls.database_uri = database_init() cls.wallet = Wallet.create( name='test_wallet_create', witness_type='legacy', - db_uri=cls.DATABASE_URI) + db_uri=cls.database_uri) def test_wallet_create(self): self.assertTrue(isinstance(self.wallet, Wallet)) @@ -155,8 +186,8 @@ def test_wallet_info(self): self.assertIn(" Date: Mon, 5 Feb 2024 12:38:47 +0100 Subject: [PATCH 065/207] Add teardown methods to wallet unittests --- tests/test_tools.py | 45 +++++----- tests/test_wallets.py | 196 ++++++++++++++++++++++++++---------------- 2 files changed, 141 insertions(+), 100 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 91855a4b..9f50b0f7 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -19,7 +19,7 @@ except ImportError: pass # Only necessary when mysql or postgres is used -from bitcoinlib.main import UNITTESTS_FULL_DATABASE_TEST +# from bitcoinlib.main import UNITTESTS_FULL_DATABASE_TEST from bitcoinlib.db import BCL_DATABASE_DIR from bitcoinlib.encoding import normalize_string @@ -27,45 +27,40 @@ DATABASE_NAME = 'bitcoinlib_unittest' -def database_init(): - - def init_sqlite(): - if os.path.isfile(SQLITE_DATABASE_FILE): - os.remove(SQLITE_DATABASE_FILE) - - - def init_postgresql(): +def database_init(dbname=DATABASE_NAME): + if os.getenv('UNITTEST_DATABASE') == 'postgresql': con = psycopg2.connect(user='postgres', host='localhost', password='postgres') con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) cur = con.cursor() + # try: cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( - sql.Identifier(DATABASE_NAME)) + sql.Identifier(dbname)) ) cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier(DATABASE_NAME)) + sql.Identifier(dbname)) ) + # except Exception: + # pass + # finally: + # cur.close() + # con.close() cur.close() con.close() - - - def init_mysql(): + return 'postgresql://postgres:postgres@localhost:5432/' + dbname + elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() - cur.execute("DROP DATABASE IF EXISTS {}".format(DATABASE_NAME)) - cur.execute("CREATE DATABASE {}".format(DATABASE_NAME)) + cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) + cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - - if os.getenv('UNITTEST_DATABASE') == 'mysql': - init_mysql() - return 'mysql://user:password@localhost:3306/' + DATABASE_NAME - elif os.getenv('UNITTEST_DATABASE') == 'postgresql': - init_postgresql() - return 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME + return 'mysql://user:password@localhost:3306/' + dbname else: - init_sqlite() - return SQLITE_DATABASE_FILE + dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) + if os.path.isfile(dburi): + os.remove(dburi) + return dburi class TestToolsCommandLineWallet(unittest.TestCase): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 23b447bb..927f44b6 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -65,87 +65,110 @@ )) -class TestWalletMixin: - SCHEMA = None - - @classmethod - def create_db_if_needed(cls, db): - if cls.SCHEMA == 'postgresql': - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - cur = con.cursor() - try: - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier(db)) - ) - except Exception: - pass - finally: - cur.close() - con.close() - elif cls.SCHEMA == 'mysql': - con = mysql.connector.connect(user='root', host='localhost') - cur = con.cursor() - cur.execute('CREATE DATABASE IF NOT EXISTS {}'.format(db)) - con.commit() - cur.close() - con.close() - - @classmethod - def db_remove(cls): - close_all_sessions() - if cls.SCHEMA == 'sqlite': - for db in [DATABASEFILE_UNITTESTS, DATABASEFILE_UNITTESTS_2]: - if os.path.isfile(db): - try: - os.remove(db) - except PermissionError: - db_obj = Db(db) - db_obj.drop_db(True) - db_obj.session.close() - db_obj.engine.dispose() - elif cls.SCHEMA == 'postgresql': - for db in [DATABASE_NAME, DATABASE_NAME_2]: - cls.create_db_if_needed(db) - con = psycopg2.connect(user='postgres', host='localhost', password='postgres', database=db) - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) - cur = con.cursor() - try: - # drop all tables - cur.execute(sql.SQL(""" - DO $$ DECLARE - r RECORD; - BEGIN - FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP - EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; - END LOOP; - END $$;""")) - finally: - cur.close() - con.close() - elif cls.SCHEMA == 'mysql': - for db in [DATABASE_NAME, DATABASE_NAME_2]: - cls.create_db_if_needed(db) - con = mysql.connector.connect(user='root', host='localhost', database=db, autocommit=True) - cur = con.cursor(buffered=True) - try: - cur.execute("DROP DATABASE {};".format(db)) - cur.execute("CREATE DATABASE {};".format(db)) - finally: - cur.close() - con.close() +# class TestWalletMixin: +# SCHEMA = None +# +# @classmethod +# def create_db_if_needed(cls, db): +# if cls.SCHEMA == 'postgresql': +# con = psycopg2.connect(user='postgres', host='localhost', password='postgres') +# con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) +# cur = con.cursor() +# try: +# cur.execute(sql.SQL("CREATE DATABASE {}").format( +# sql.Identifier(db)) +# ) +# except Exception: +# pass +# finally: +# cur.close() +# con.close() +# elif cls.SCHEMA == 'mysql': +# con = mysql.connector.connect(user='root', host='localhost') +# cur = con.cursor() +# cur.execute('CREATE DATABASE IF NOT EXISTS {}'.format(db)) +# con.commit() +# cur.close() +# con.close() +# +# @classmethod +# def db_remove(cls): +# close_all_sessions() +# if cls.SCHEMA == 'sqlite': +# for db in [DATABASEFILE_UNITTESTS, DATABASEFILE_UNITTESTS_2]: +# if os.path.isfile(db): +# try: +# os.remove(db) +# except PermissionError: +# db_obj = Db(db) +# db_obj.drop_db(True) +# db_obj.session.close() +# db_obj.engine.dispose() +# elif cls.SCHEMA == 'postgresql': +# for db in [DATABASE_NAME, DATABASE_NAME_2]: +# cls.create_db_if_needed(db) +# con = psycopg2.connect(user='postgres', host='localhost', password='postgres', database=db) +# con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) +# cur = con.cursor() +# try: +# # drop all tables +# cur.execute(sql.SQL(""" +# DO $$ DECLARE +# r RECORD; +# BEGIN +# FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP +# EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; +# END LOOP; +# END $$;""")) +# finally: +# cur.close() +# con.close() +# elif cls.SCHEMA == 'mysql': +# for db in [DATABASE_NAME, DATABASE_NAME_2]: +# cls.create_db_if_needed(db) +# con = mysql.connector.connect(user='root', host='localhost', database=db, autocommit=True) +# cur = con.cursor(buffered=True) +# try: +# cur.execute("DROP DATABASE {};".format(db)) +# cur.execute("CREATE DATABASE {};".format(db)) +# finally: +# cur.close() +# con.close() def database_init(dbname=DATABASE_NAME): - - print(os.getenv('UNITTEST_DATABASE')) + close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': con = psycopg2.connect(user='postgres', host='localhost', password='postgres') con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) cur = con.cursor() - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( - sql.Identifier(dbname)) - ) + try: + # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) + cur.execute(sql.SQL("UPDATE pg_database SET datallowconn = 'false' WHERE datname = '{}'").format( + sql.Identifier(dbname))) + cur.execute(sql.SQL("SELECT pg_terminate_backend(pg_stat_activity.pid)" + "FROM pg_stat_activity WHERE pg_stat_activity.datname = '{}'" + "AND pid <> pg_backend_pid();").format(sql.Identifier(dbname))) + except Exception as e: + print(e) + # res = cur.execute(sql.SQL("SELECT sum(numbackends) FROM pg_stat_database")) + # print(res) + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) + # try: + # drop all tables + # cur.execute(sql.SQL(""" + # DO $$ DECLARE + # r RECORD; + # BEGIN + # FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP + # EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; + # END LOOP; + # END $$;""")) + # finally: + # cur.close() + # con.close() + con.commit() cur.close() con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname @@ -167,10 +190,10 @@ def database_init(dbname=DATABASE_NAME): class TestWalletCreate(unittest.TestCase): wallet = None + database_uri = None @classmethod def setUpClass(cls): - # cls.database_uri = database_init() cls.database_uri = database_init() cls.wallet = Wallet.create( name='test_wallet_create', witness_type='legacy', @@ -356,6 +379,11 @@ def test_wallet_create_bip38_segwit(self): w = wallet_create_or_open('kewallet', ke, password='hoihoi', network='bitcoin', db_uri=self.database_uri) self.assertEqual(k.private_hex, w.main_key.key_private.hex()) + @classmethod + def tearDownClass(cls): + del cls.database_uri + del cls.wallet + class TestWalletImport(unittest.TestCase): @@ -520,6 +548,7 @@ def test_wallet_import_master_key(self): class TestWalletExport(unittest.TestCase): + database_uri = None @classmethod def setUpClass(cls): @@ -760,6 +789,11 @@ def test_wallet_key_public_leaks(self): self.assertFalse(w2.main_key.is_private) self.assertIsNone(w2.main_key.key_private) + @classmethod + def tearDownClass(cls): + del cls.database_uri + del cls.wallet + class TestWalletElectrum(unittest.TestCase): wallet = None @@ -804,6 +838,10 @@ def test_wallet_electrum_p2sh_p2wsh(self): self.assertEqual(wlt.get_key().address, '3ArRVGXfqcjw68XzUZr4iCCemrPoFZxm7s') self.assertEqual(wlt.get_key_change().address, '3FZEUFf59C3psUUiKB8TFbjsFUGWD73QPY') + @classmethod + def tearDownClass(cls): + del cls.wallet + class TestWalletMultiCurrency(unittest.TestCase): wallet = None @@ -877,6 +915,10 @@ def test_wallet_multiple_networks_value(self): self.assertFalse(t.pushed) self.assertTrue(t.verified) + @classmethod + def tearDownClass(cls): + del cls.wallet + class TestWalletMultiNetworksMultiAccount(unittest.TestCase): @@ -2329,6 +2371,10 @@ def test_wallet_transaction_replace_by_fee(self): self.assertTrue(t2.replace_by_fee) self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) + @classmethod + def tearDownClass(cls): + del cls.wallet + class TestWalletSegwit(unittest.TestCase): From 78a5599bdb47dc0e3d7e1520459a1d9d07dce94c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 5 Feb 2024 13:42:27 +0100 Subject: [PATCH 066/207] Fix PermissionErrors in Windows wallet unittest --- tests/test_wallets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 927f44b6..4f1342db 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -184,7 +184,13 @@ def database_init(dbname=DATABASE_NAME): else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): - os.remove(dburi) + try: + os.remove(dburi) + except PermissionError: + db_obj = Db(dburi) + db_obj.drop_db(True) + db_obj.session.close() + db_obj.engine.dispose() return dburi From b930fe16cf47464400b029d73322578d89e5ad07 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 5 Feb 2024 13:53:04 +0100 Subject: [PATCH 067/207] Remove parameterized and use global database variable instead --- requirements.txt | 1 - setup.cfg | 1 - tests/test_services.py | 1 - tests/test_wallets.py | 76 +----------------------------------------- 4 files changed, 1 insertion(+), 78 deletions(-) diff --git a/requirements.txt b/requirements.txt index 151bfeed..d2d4cc5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ coveralls>=3.0.1 psycopg2>=2.9.2 mysql-connector-python>=8.0.27 mysqlclient>=2.1.0 -parameterized>=0.8.1 sphinx_rtd_theme>=1.0.0 Cython>=3.0.0 win-unicode-console;platform_system=="Windows" diff --git a/setup.cfg b/setup.cfg index 0bf33236..8e4234ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,7 +50,6 @@ dev = psycopg2 >= 2.9.2 mysql-connector-python >= 8.0.27 mysqlclient >= 2.1.0 - parameterized >= 0.8.1 sphinx_rtd_theme >= 1.0.0 Cython>=3.0.0 win-unicode-console;platform_system=="Windows" diff --git a/tests/test_services.py b/tests/test_services.py index cfc6409f..fdd2a558 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -22,7 +22,6 @@ import logging try: import mysql.connector - from parameterized import parameterized_class import psycopg2 from psycopg2 import sql from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 4f1342db..76611072 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Wallet Class -# © 2016 - 2023 May - 1200 Web Development +# © 2016 - 2024 February - 1200 Web Development # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -23,7 +23,6 @@ try: import mysql.connector - from parameterized import parameterized_class import psycopg2 from psycopg2 import sql from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT @@ -43,8 +42,6 @@ DATABASEFILE_UNITTESTS_2 = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest2.sqlite') DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' -# SQLITE_DATABASE_FILE = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % DATABASE_NAME) -# SQLITE_DATABASE_FILE2 = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % DATABASE_NAME_2) db_uris = ( ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) @@ -65,77 +62,6 @@ )) -# class TestWalletMixin: -# SCHEMA = None -# -# @classmethod -# def create_db_if_needed(cls, db): -# if cls.SCHEMA == 'postgresql': -# con = psycopg2.connect(user='postgres', host='localhost', password='postgres') -# con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) -# cur = con.cursor() -# try: -# cur.execute(sql.SQL("CREATE DATABASE {}").format( -# sql.Identifier(db)) -# ) -# except Exception: -# pass -# finally: -# cur.close() -# con.close() -# elif cls.SCHEMA == 'mysql': -# con = mysql.connector.connect(user='root', host='localhost') -# cur = con.cursor() -# cur.execute('CREATE DATABASE IF NOT EXISTS {}'.format(db)) -# con.commit() -# cur.close() -# con.close() -# -# @classmethod -# def db_remove(cls): -# close_all_sessions() -# if cls.SCHEMA == 'sqlite': -# for db in [DATABASEFILE_UNITTESTS, DATABASEFILE_UNITTESTS_2]: -# if os.path.isfile(db): -# try: -# os.remove(db) -# except PermissionError: -# db_obj = Db(db) -# db_obj.drop_db(True) -# db_obj.session.close() -# db_obj.engine.dispose() -# elif cls.SCHEMA == 'postgresql': -# for db in [DATABASE_NAME, DATABASE_NAME_2]: -# cls.create_db_if_needed(db) -# con = psycopg2.connect(user='postgres', host='localhost', password='postgres', database=db) -# con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) -# cur = con.cursor() -# try: -# # drop all tables -# cur.execute(sql.SQL(""" -# DO $$ DECLARE -# r RECORD; -# BEGIN -# FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP -# EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; -# END LOOP; -# END $$;""")) -# finally: -# cur.close() -# con.close() -# elif cls.SCHEMA == 'mysql': -# for db in [DATABASE_NAME, DATABASE_NAME_2]: -# cls.create_db_if_needed(db) -# con = mysql.connector.connect(user='root', host='localhost', database=db, autocommit=True) -# cur = con.cursor(buffered=True) -# try: -# cur.execute("DROP DATABASE {};".format(db)) -# cur.execute("CREATE DATABASE {};".format(db)) -# finally: -# cur.close() -# con.close() - - def database_init(dbname=DATABASE_NAME): close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': From 5d67936e793e4fec88a03f263cce8ae6f909421c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 5 Feb 2024 17:43:51 +0100 Subject: [PATCH 068/207] Fix issues with autoincrement key_id and postgresql --- bitcoinlib/db.py | 5 ++--- bitcoinlib/db_cache.py | 5 ++--- bitcoinlib/wallets.py | 20 ++++++++++++++++---- tests/test_tools.py | 18 +++++++++--------- tests/test_wallets.py | 10 ++++------ 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index f5d966d7..f8e9b2c2 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -23,7 +23,7 @@ ForeignKey, DateTime, LargeBinary, TypeDecorator) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.compiler import compiles -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions +from sqlalchemy.orm import sessionmaker, relationship, session from urllib.parse import urlparse from bitcoinlib.main import * from bitcoinlib.encoding import aes_encrypt, aes_decrypt @@ -97,7 +97,6 @@ def drop_db(self, yes_i_am_sure=False): if yes_i_am_sure: self.session.commit() self.session.close_all() - close_all_sessions() Base.metadata.drop_all(self.engine) @staticmethod @@ -286,7 +285,7 @@ class DbKey(Base): "depth=1 are the masterkeys children.") change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(33), index=True, doc="Bytes representation of public key") + public = Column(LargeBinary(65), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index 36df4cdb..18b99fd1 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -21,7 +21,7 @@ from sqlalchemy import create_engine from sqlalchemy import Column, Integer, BigInteger, String, Boolean, ForeignKey, DateTime, Enum, LargeBinary from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions +from sqlalchemy.orm import sessionmaker, relationship, session # try: # import mysql.connector # from parameterized import parameterized_class @@ -90,8 +90,7 @@ def __init__(self, db_uri=None): def drop_db(self): self.session.commit() - # self.session.close_all() - close_all_sessions() + self.session.close_all() Base.metadata.drop_all(self.engine) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index e41f7f50..b80fe327 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -212,6 +212,7 @@ def wallet_empty(wallet, db_uri=None, db_password=None): else: w = session.query(DbWallet).filter_by(name=wallet) if not w or not w.first(): + session.close() raise WalletError("Wallet '%s' not found" % wallet) wallet_id = w.first().id @@ -375,13 +376,20 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 encoding = get_encoding_from_witness(witness_type) script_type = script_type_default(witness_type, multisig) + if not new_key_id: + key_id_max = session.query(func.max(DbKey.id)).scalar() + new_key_id = key_id_max + 1 if key_id_max else None + commit = True + else: + commit = False + if not key_is_address: if key_type != 'single' and k.depth != len(path.split('/'))-1: if path == 'm' and k.depth > 1: path = "M" address = k.address(encoding=encoding, script_type=script_type) - if not new_key_id: + if commit: keyexists = session.query(DbKey).\ filter(DbKey.wallet_id == wallet_id, DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() @@ -422,10 +430,10 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, witness_type=witness_type) - if not new_key_id: + if commit: session.merge(DbNetwork(name=network)) session.add(nk) - if new_key_id is None: + if commit: session.commit() return WalletKey(nk.id, session, k) @@ -488,6 +496,9 @@ def __init__(self, key_id, session, hdkey_object=None): else: raise WalletError("Key with id %s not found" % key_id) + def __del__(self): + self._session.close() + def __repr__(self): return "" % (self.key_id, self.name, self.wif, self.path) @@ -1699,7 +1710,8 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, if not name: name = "Multisig Key " + '/'.join(public_key_ids) - multisig_key = DbKey( + new_key_id = (self._session.query(func.max(DbKey.id)).scalar() or 0) + 1 + multisig_key = DbKey(id=new_key_id, name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id, depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path, public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id, diff --git a/tests/test_tools.py b/tests/test_tools.py index 9f50b0f7..e3db1c84 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -13,24 +13,21 @@ try: import mysql.connector - import psycopg2 - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg import sql except ImportError: pass # Only necessary when mysql or postgres is used -# from bitcoinlib.main import UNITTESTS_FULL_DATABASE_TEST -from bitcoinlib.db import BCL_DATABASE_DIR +from bitcoinlib.db import BCL_DATABASE_DIR, session from bitcoinlib.encoding import normalize_string -SQLITE_DATABASE_FILE = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest.sqlite') DATABASE_NAME = 'bitcoinlib_unittest' def database_init(dbname=DATABASE_NAME): + session.close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() # try: cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( @@ -48,7 +45,10 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + try: + con = mysql.connector.connect(user='root', host='localhost') + except mysql.connector.errors.ProgrammingError: + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 76611072..a9890897 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -23,9 +23,8 @@ try: import mysql.connector - import psycopg2 - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg import sql except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support @@ -63,10 +62,9 @@ def database_init(dbname=DATABASE_NAME): - close_all_sessions() + session.close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() try: # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) From 154532490d0ae36f5fca63cf7bebfd8f800f69c8 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 09:25:25 +0100 Subject: [PATCH 069/207] Add unittests for mysql database --- .github/workflows/unittests-mysql.yaml | 44 ++++++++++++ .github/workflows/unittests-noscrypt.yaml | 2 +- .github/workflows/unittests.yaml | 4 +- .github/workflows/unittests_windows.yaml | 2 +- bitcoinlib/db.py | 3 +- bitcoinlib/db_cache.py | 2 +- tests/test_services.py | 9 ++- tests/test_wallets.py | 81 +++++++++++++---------- 8 files changed, 101 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/unittests-mysql.yaml diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml new file mode 100644 index 00000000..ca6b2afa --- /dev/null +++ b/.github/workflows/unittests-mysql.yaml @@ -0,0 +1,44 @@ +name: Bitcoinlib Tests Ubuntu MySQL +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python: ["3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install .[dev] + - name: Test with coverage + env: + BCL_CONFIG_FILE: config.ini.unittest + UNITTESTS_FULL_DATABASE_TEST: False + UNITTEST_DATABASE: mysql + run: coverage run --source=bitcoinlib -m unittest -v + + - name: Coveralls + uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + flag-name: Unit Test + debug: true + + coveralls_finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + debug: true diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 9dae687c..26ae6aaa 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Unittests Coveralls Ubuntu - No scrypt +name: Bitcoinlib Tests Ubuntu - No scrypt on: [push] jobs: diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 28ae94bc..8b5c1293 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Unittests Coveralls Ubuntu +name: Bitcoinlib Tests Ubuntu on: [push] jobs: @@ -7,7 +7,7 @@ jobs: strategy: matrix: - python: ["3.8", "3.10", "3.11"] + python: ["3.8", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index 560fc4e6..183c6451 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Windows Unittests +name: Bitcoinlib Tests Windows on: [push] jobs: diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index f8e9b2c2..b82cbeab 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -96,7 +96,8 @@ def __init__(self, db_uri=None, password=None): def drop_db(self, yes_i_am_sure=False): if yes_i_am_sure: self.session.commit() - self.session.close_all() + self.session.close() + session.close_all_sessions() Base.metadata.drop_all(self.engine) @staticmethod diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index 18b99fd1..98d9b7f3 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -59,7 +59,7 @@ class DbCache: """ Cache Database object. Initialize database and open session when creating database object. - Create new database if is doesn't exist yet + Create new database if it doesn't exist yet """ def __init__(self, db_uri=None): diff --git a/tests/test_services.py b/tests/test_services.py index fdd2a558..c29ce3ba 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -22,9 +22,9 @@ import logging try: import mysql.connector - import psycopg2 - from psycopg2 import sql - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + import psycopg + from psycopg import sql + # from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support @@ -818,8 +818,7 @@ def setUpClass(cls): pass try: - con = psycopg2.connect(user='postgres', host='localhost', password='postgres') - con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() cur.execute(sql.SQL("CREATE DATABASE {}").format( sql.Identifier('bitcoinlibcache.unittest')) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index a9890897..0fe0014f 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -25,6 +25,7 @@ import mysql.connector import psycopg from psycopg import sql + import testing.postgresql except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support @@ -42,46 +43,42 @@ DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' -db_uris = ( - ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) +# db_uris = ( +# ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) -print("UNITTESTS_FULL_DATABASE_TEST: %s" % UNITTESTS_FULL_DATABASE_TEST) +print("DATABASE USED: %s" % os.getenv('UNITTEST_DATABASE')) -if UNITTESTS_FULL_DATABASE_TEST: - db_uris += ( - ('mysql', 'mysql://root:root@localhost:3306/' + DATABASE_NAME, - 'mysql://root:root@localhost:3306/' + DATABASE_NAME_2), - ('postgresql', 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, - 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME_2), - ) - - -params = (('SCHEMA', 'DATABASE_URI', 'DATABASE_URI_2'), ( - db_uris -)) +# if UNITTESTS_FULL_DATABASE_TEST: +# db_uris += ( +# ('mysql', 'mysql://root:root@localhost:3306/' + DATABASE_NAME, +# 'mysql://root:root@localhost:3306/' + DATABASE_NAME_2), +# ('postgresql', 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, +# 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME_2), +# ) +# +# +# params = (('SCHEMA', 'DATABASE_URI', 'DATABASE_URI_2'), ( +# db_uris +# )) def database_init(dbname=DATABASE_NAME): session.close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': - con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) - cur = con.cursor() - try: - # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) - cur.execute(sql.SQL("UPDATE pg_database SET datallowconn = 'false' WHERE datname = '{}'").format( - sql.Identifier(dbname))) - cur.execute(sql.SQL("SELECT pg_terminate_backend(pg_stat_activity.pid)" - "FROM pg_stat_activity WHERE pg_stat_activity.datname = '{}'" - "AND pid <> pg_backend_pid();").format(sql.Identifier(dbname))) - except Exception as e: - print(e) + # con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + # cur = con.cursor() + # try: + # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) + # cur.execute(sql.SQL("UPDATE pg_database SET datallowconn = 'false' WHERE datname = '{}'").format( + # sql.Identifier(dbname))) + # cur.execute(sql.SQL("SELECT pg_terminate_backend(pg_stat_activity.pid)" + # "FROM pg_stat_activity WHERE pg_stat_activity.datname = '{}'" + # "AND pid <> pg_backend_pid();").format(sql.Identifier(dbname))) + # except Exception as e: + # print(e) # res = cur.execute(sql.SQL("SELECT sum(numbackends) FROM pg_stat_database")) # print(res) - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) - cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) - # try: - # drop all tables - # cur.execute(sql.SQL(""" + # res = cur.execute(sql.SQL(""" # DO $$ DECLARE # r RECORD; # BEGIN @@ -89,13 +86,27 @@ def database_init(dbname=DATABASE_NAME): # EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; # END LOOP; # END $$;""")) + # print(res) + # try: + # cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + # cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) + # except: + # pass + # try: + # # drop all tables # finally: # cur.close() # con.close() - con.commit() - cur.close() - con.close() - return 'postgresql://postgres:postgres@localhost:5432/' + dbname + # con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + # cur = con.cursor() + # cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) + # con.commit() + # cur.close() + # con.close() + # return 'postgresql://postgres:postgres@localhost:5432/' + dbname + # postgresql = testing.postgresql.Postgresql() + # return postgresql.url() + return 'testing.postgresql' elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() From 556e97f0afb89a7427ec8bb1d0f7609be3693098 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 09:59:07 +0100 Subject: [PATCH 070/207] Use other mysql login in tools unittest --- .github/workflows/unittests-mysql.yaml | 2 +- bitcoinlib/db_cache.py | 3 ++- tests/test_tools.py | 5 +---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index ca6b2afa..a6a96e87 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -1,4 +1,4 @@ -name: Bitcoinlib Tests Ubuntu MySQL +name: Bitcoinlib Tests Ubuntu - MySQL on: [push] jobs: diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index 98d9b7f3..124eb204 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -90,7 +90,8 @@ def __init__(self, db_uri=None): def drop_db(self): self.session.commit() - self.session.close_all() + self.session.close() + session.close_all_sessions() Base.metadata.drop_all(self.engine) diff --git a/tests/test_tools.py b/tests/test_tools.py index e3db1c84..dea88af0 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -45,10 +45,7 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - try: - con = mysql.connector.connect(user='root', host='localhost') - except mysql.connector.errors.ProgrammingError: - con = mysql.connector.connect(user='user', host='localhost', password='password') + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) From 18d3c22b710e7ef70ecccfbca3958d0fa234aa6e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 10:13:47 +0100 Subject: [PATCH 071/207] Use correct mysql database credentials --- bitcoinlib/db.py | 1 + bitcoinlib/db_cache.py | 11 ----------- tests/test_tools.py | 11 ++++++++--- tests/test_wallets.py | 9 +++++++-- tests/testing.postgresql | Bin 0 -> 245760 bytes 5 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 tests/testing.postgresql diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index b82cbeab..46dc0197 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -77,6 +77,7 @@ def __init__(self, db_uri=None, password=None): _logger.info("Using database: %s://%s:%s/%s" % (self.o.scheme or '', self.o.hostname or '', self.o.port or '', self.o.path or '')) self.db_uri = db_uri + print(db_uri) # VERIFY AND UPDATE DATABASE # Just a very simple database update script, without any external libraries for now diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index 124eb204..f339f1de 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -22,17 +22,6 @@ from sqlalchemy import Column, Integer, BigInteger, String, Boolean, ForeignKey, DateTime, Enum, LargeBinary from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship, session -# try: -# import mysql.connector -# from parameterized import parameterized_class -# import psycopg2 -# from psycopg2 import sql -# from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT -# except ImportError as e: -# print("Could not import all modules. Error: %s" % e) -# # from psycopg2cffi import compat # Use for PyPy support -# # compat.register() -# pass # Only necessary when mysql or postgres is used from urllib.parse import urlparse from bitcoinlib.main import * diff --git a/tests/test_tools.py b/tests/test_tools.py index dea88af0..659673f9 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -45,14 +45,19 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + try: + con = mysql.connector.connect(user='root', host='localhost') + credentials = 'root' + except mysql.connector.errors.ProgrammingError: + con = mysql.connector.connect(user='user', host='localhost', password='password') + credentials = 'user:password' cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://%s@localhost:3306/' % credentials + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): @@ -114,7 +119,7 @@ def test_tools_clw_create_multisig_wallet_one_key(self): (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) cmd_wlt_delete = "%s %s -w testms1 --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.database_uri) - output_wlt_create = "if you understood and wrote down your key: Receive address:" + output_wlt_create = "if you understood and wrote down your key" output_wlt_delete = "Wallet testms1 has been removed" process = Popen(cmd_wlt_create, stdin=PIPE, stdout=PIPE, shell=True) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 0fe0014f..102fb2a1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -108,14 +108,19 @@ def database_init(dbname=DATABASE_NAME): # return postgresql.url() return 'testing.postgresql' elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + try: + con = mysql.connector.connect(user='root', host='localhost') + credentials = 'root' + except mysql.connector.errors.ProgrammingError: + con = mysql.connector.connect(user='user', host='localhost', password='password') + credentials = 'user:password' cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://%s@localhost:3306/' % credentials + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): diff --git a/tests/testing.postgresql b/tests/testing.postgresql new file mode 100644 index 0000000000000000000000000000000000000000..10ff0e2c235716ea4fb35c2e2bcbd8ed27346b35 GIT binary patch literal 245760 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

S>iM3b4X3g4uY0iJ^lvEIa>yik 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 literal 0 HcmV?d00001 From 2c8b53f2e717db74cb6bfca195d5ca93ae47d6c3 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 10:34:31 +0100 Subject: [PATCH 072/207] Start mysql server when testing: --- .github/workflows/unittests-mysql.yaml | 3 +++ bitcoinlib/db.py | 1 - requirements.txt | 2 +- setup.cfg | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index a6a96e87..67e1399b 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -19,6 +19,9 @@ jobs: - name: Install dependencies run: | python -m pip install .[dev] + - name: Start mysql + run: | + sudo /etc/init.d/mysql start - name: Test with coverage env: BCL_CONFIG_FILE: config.ini.unittest diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 46dc0197..b82cbeab 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -77,7 +77,6 @@ def __init__(self, db_uri=None, password=None): _logger.info("Using database: %s://%s:%s/%s" % (self.o.scheme or '', self.o.hostname or '', self.o.port or '', self.o.path or '')) self.db_uri = db_uri - print(db_uri) # VERIFY AND UPDATE DATABASE # Just a very simple database update script, without any external libraries for now diff --git a/requirements.txt b/requirements.txt index d2d4cc5f..347edd35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ SQLAlchemy>=2.0.0 numpy>=1.22.0 sphinx>=6.0.0 coveralls>=3.0.1 -psycopg2>=2.9.2 +psycopg>=3.0.0 mysql-connector-python>=8.0.27 mysqlclient>=2.1.0 sphinx_rtd_theme>=1.0.0 diff --git a/setup.cfg b/setup.cfg index 8e4234ae..38b30dd6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,7 @@ dev = scrypt >= 0.8.18;platform_system!="Windows" sphinx >= 6.0.0 coveralls >= 3.0.1 - psycopg2 >= 2.9.2 + psycopg >= 3.0.0 mysql-connector-python >= 8.0.27 mysqlclient >= 2.1.0 sphinx_rtd_theme >= 1.0.0 From c0d06f61cc25b309775f84191d7612842e7f61ef Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 11:06:39 +0100 Subject: [PATCH 073/207] Try with different mariadb setup --- .github/workflows/unittests-mysql.yaml | 14 +++++++++++--- tests/test_tools.py | 9 ++------- tests/test_wallets.py | 9 ++------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index 67e1399b..dad3629e 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -4,6 +4,17 @@ on: [push] jobs: test: runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + ports: + - 3306 + env: + MARIADB_ROOT_PASSWORD: password + MARIADB_DATABASE: test + MARIADB_USER: user + MARIADB_PASSWORD: password + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 strategy: matrix: @@ -19,9 +30,6 @@ jobs: - name: Install dependencies run: | python -m pip install .[dev] - - name: Start mysql - run: | - sudo /etc/init.d/mysql start - name: Test with coverage env: BCL_CONFIG_FILE: config.ini.unittest diff --git a/tests/test_tools.py b/tests/test_tools.py index 659673f9..dffc6683 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -45,19 +45,14 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - try: - con = mysql.connector.connect(user='root', host='localhost') - credentials = 'root' - except mysql.connector.errors.ProgrammingError: - con = mysql.connector.connect(user='user', host='localhost', password='password') - credentials = 'user:password' + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://%s@localhost:3306/' % credentials + dbname + return 'mysql://user:password@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 102fb2a1..0fe0014f 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -108,19 +108,14 @@ def database_init(dbname=DATABASE_NAME): # return postgresql.url() return 'testing.postgresql' elif os.getenv('UNITTEST_DATABASE') == 'mysql': - try: - con = mysql.connector.connect(user='root', host='localhost') - credentials = 'root' - except mysql.connector.errors.ProgrammingError: - con = mysql.connector.connect(user='user', host='localhost', password='password') - credentials = 'user:password' + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://%s@localhost:3306/' % credentials + dbname + return 'mysql://user:password@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): From d78faaf424eab1369efad594b4272ef6f2155884 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 11:14:06 +0100 Subject: [PATCH 074/207] Try with different mariadb user --- tests/test_tools.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index dffc6683..667d3973 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -45,14 +45,17 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + try: + con = mysql.connector.connect(user='user', host='localhost', password='password') + except: + con = mysql.connector.connect(user='root', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://root:password@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): From 6c1bb7bb0710c4363f1e076f63dbb8ee27d70547 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 11:44:01 +0100 Subject: [PATCH 075/207] Try with different mariadb user 2 --- .github/workflows/unittests-mysql.yaml | 6 ++---- tests/test_tools.py | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index dad3629e..0148f79c 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -10,10 +10,8 @@ jobs: ports: - 3306 env: - MARIADB_ROOT_PASSWORD: password - MARIADB_DATABASE: test - MARIADB_USER: user - MARIADB_PASSWORD: password + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true + MARIADB_MYSQL_LOCALHOST_USER: 1 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 strategy: diff --git a/tests/test_tools.py b/tests/test_tools.py index 667d3973..370d9f54 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -48,14 +48,17 @@ def database_init(dbname=DATABASE_NAME): try: con = mysql.connector.connect(user='user', host='localhost', password='password') except: - con = mysql.connector.connect(user='root', host='localhost', password='password') + try: + con = mysql.connector.connect(user='root', host='localhost', password='password') + except: + con = mysql.connector.connect(user='root', host='localhost') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://root:password@localhost:3306/' + dbname + return 'mysql://root@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): From 742b55d47454d1aef6041f1729d15d0f99d4dfef Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 6 Feb 2024 11:53:54 +0100 Subject: [PATCH 076/207] Remove mysql unittests --- .github/workflows/unittests-mysql.yaml | 53 -------------------------- .github/workflows/unittests.yaml | 2 +- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 .github/workflows/unittests-mysql.yaml diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml deleted file mode 100644 index 0148f79c..00000000 --- a/.github/workflows/unittests-mysql.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Bitcoinlib Tests Ubuntu - MySQL -on: [push] - -jobs: - test: - runs-on: ubuntu-latest - services: - mariadb: - image: mariadb:latest - ports: - - 3306 - env: - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true - MARIADB_MYSQL_LOCALHOST_USER: 1 - options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 - - strategy: - matrix: - python: ["3.10"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python }} - architecture: 'x64' - - name: Install dependencies - run: | - python -m pip install .[dev] - - name: Test with coverage - env: - BCL_CONFIG_FILE: config.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False - UNITTEST_DATABASE: mysql - run: coverage run --source=bitcoinlib -m unittest -v - - - name: Coveralls - uses: AndreMiras/coveralls-python-action@develop - with: - parallel: true - flag-name: Unit Test - debug: true - - coveralls_finish: - needs: test - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true - debug: true diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 8b5c1293..44b24ae2 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -7,7 +7,7 @@ jobs: strategy: matrix: - python: ["3.8", "3.11"] + python: ["3.8", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 1bce12074d034c87fb5458f2f73cef4800394321 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 17:32:54 +0100 Subject: [PATCH 077/207] Update cache database code --- bitcoinlib/config/config.py | 12 ++--- bitcoinlib/db_cache.py | 23 +++++----- tests/test_services.py | 87 ++++++++++++++++++++++--------------- tests/test_tools.py | 16 +------ tests/test_wallets.py | 51 +++++----------------- 5 files changed, 79 insertions(+), 110 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index eaeed417..24baf365 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -211,9 +211,6 @@ # }, ] -# UNITTESTS -UNITTESTS_FULL_DATABASE_TEST = False - # CACHING SERVICE_CACHING_ENABLED = True @@ -235,7 +232,7 @@ def config_get(section, var, fallback, is_boolean=False): global ALLOW_DATABASE_THREADS, DEFAULT_DATABASE_CACHE global BCL_LOG_FILE, LOGLEVEL, ENABLE_BITCOINLIB_LOGGING global TIMEOUT_REQUESTS, DEFAULT_LANGUAGE, DEFAULT_NETWORK, DEFAULT_WITNESS_TYPE - global UNITTESTS_FULL_DATABASE_TEST, SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY + global SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY global SERVICE_MAX_ERRORS, BLOCK_COUNT_CACHE_TIME, MAX_TRANSACTIONS # Read settings from Configuration file provided in OS environment~/.bitcoinlib/ directory @@ -262,7 +259,8 @@ def config_get(section, var, fallback, is_boolean=False): DEFAULT_DATABASE = str(Path(BCL_DATABASE_DIR, default_databasefile)) default_databasefile_cache = DEFAULT_DATABASE_CACHE = \ config_get('locations', 'default_databasefile_cache', fallback='bitcoinlib_cache.sqlite') - if not default_databasefile_cache.startswith('postgresql') or default_databasefile_cache.startswith('mysql'): + if not (default_databasefile_cache.startswith('postgresql') or default_databasefile_cache.startswith('mysql') or + default_databasefile_cache.startswith('mariadb')): DEFAULT_DATABASE_CACHE = str(Path(BCL_DATABASE_DIR, default_databasefile_cache)) ALLOW_DATABASE_THREADS = config_get("common", "allow_database_threads", fallback=True, is_boolean=True) SERVICE_CACHING_ENABLED = config_get('common', 'service_caching_enabled', fallback=True, is_boolean=True) @@ -286,10 +284,6 @@ def config_get(section, var, fallback, is_boolean=False): DEFAULT_NETWORK = config_get('common', 'default_network', fallback=DEFAULT_NETWORK) DEFAULT_WITNESS_TYPE = config_get('common', 'default_witness_type', fallback=DEFAULT_WITNESS_TYPE) - full_db_test = os.environ.get('UNITTESTS_FULL_DATABASE_TEST') - if full_db_test in [1, True, 'True', 'true', 'TRUE']: - UNITTESTS_FULL_DATABASE_TEST = True - if not data: return False return True diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index f339f1de..ec8e85ff 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -27,15 +27,15 @@ _logger = logging.getLogger(__name__) -try: - dbcacheurl_obj = urlparse(DEFAULT_DATABASE_CACHE) - if dbcacheurl_obj.netloc: - dbcacheurl = dbcacheurl_obj.netloc.replace(dbcacheurl_obj.password, 'xxx') - else: - dbcacheurl = dbcacheurl_obj.path - _logger.info("Default Cache Database %s" % dbcacheurl) -except Exception: - _logger.warning("Default Cache Database: unable to parse URL") +# try: +# dbcacheurl_obj = urlparse(DEFAULT_DATABASE_CACHE) +# if dbcacheurl_obj.netloc: +# dbcacheurl = dbcacheurl_obj.netloc.replace(dbcacheurl_obj.password, 'xxx') +# else: +# dbcacheurl = dbcacheurl_obj.path +# _logger.info("Default Cache Database %s" % dbcacheurl) +# except Exception: +# _logger.warning("Default Cache Database: unable to parse URL") Base = declarative_base() @@ -66,8 +66,9 @@ def __init__(self, db_uri=None): db_uri += "&" if "?" in db_uri else "?" db_uri += "check_same_thread=False" if self.o.scheme == 'mysql': - db_uri += "&" if "?" in db_uri else "?" - db_uri += 'binary_prefix=true' + raise NotImplementedError("MySQL does not allow indexing on LargeBinary fields, so caching is not possible") + # db_uri += "&" if "?" in db_uri else "?" + # db_uri += 'binary_prefix=true' self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') Session = sessionmaker(bind=self.engine) diff --git a/tests/test_services.py b/tests/test_services.py index c29ce3ba..fa763e35 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -38,19 +38,22 @@ MAXIMUM_ESTIMATED_FEE_DIFFERENCE = 3.00 # Maximum difference from average estimated fee before test_estimatefee fails. # Use value above >0, and 1 for 100% -DATABASEFILE_CACHE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlibcache.unittest.sqlite') -DATABASEFILE_CACHE_UNITTESTS2 = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlibcache2.unittest.sqlite') -DATABASE_CACHE_POSTGRESQL = 'postgresql://postgres:postgres@localhost:5432/bitcoinlibcache.unittest' -# FIXME: MySQL databases are not supported. Not allowed to create indexes/primary keys on binary fields -DATABASE_CACHE_MYSQL = 'mysql://root:root@localhost:3306/bitcoinlibcache.unittest' +CACHE_DBNAME1 = 'bitcoinlib_cache_unittest1' +CACHE_DBNAME2 = 'bitcoinlib_cache_unittest2' +if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': + DATABASE_CACHE_UNITTESTS = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME1 + DATABASE_CACHE_UNITTESTS2 = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME2 +elif os.getenv('UNITTEST_DATABASE') == 'postgresql': + DATABASE_CACHE_UNITTESTS = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 + DATABASE_CACHE_UNITTESTS2 = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 +else: + DATABASE_CACHE_UNITTESTS = 'sqlite://' + os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME1) + '.sqlite' + DATABASE_CACHE_UNITTESTS2 = 'sqlite://' + os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME2) + '.sqlite' + DATABASES_CACHE = [ - DATABASEFILE_CACHE_UNITTESTS2, + DATABASE_CACHE_UNITTESTS, + DATABASE_CACHE_UNITTESTS2, ] -if UNITTESTS_FULL_DATABASE_TEST: - DATABASES_CACHE += [ - DATABASE_CACHE_POSTGRESQL, - DATABASE_CACHE_MYSQL - ] TIMEOUT_TEST = 3 @@ -59,7 +62,7 @@ class ServiceTest(Service): def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, providers=None, - timeout=TIMEOUT_TEST, cache_uri=DATABASEFILE_CACHE_UNITTESTS, ignore_priority=True, + timeout=TIMEOUT_TEST, cache_uri=DATABASE_CACHE_UNITTESTS, ignore_priority=True, exclude_providers=None, max_errors=SERVICE_MAX_ERRORS, strict=True): super(self.__class__, self).__init__(network, min_providers, max_providers, providers, timeout, cache_uri, ignore_priority, exclude_providers, max_errors, strict) @@ -803,33 +806,47 @@ def test_service_transaction_unconfirmed(self): class TestServiceCache(unittest.TestCase): - # TODO: Add mysql support @classmethod def setUpClass(cls): + session.close_all_sessions() try: - if os.path.isfile(DATABASEFILE_CACHE_UNITTESTS2): - os.remove(DATABASEFILE_CACHE_UNITTESTS2) - except Exception: - pass - try: - DbCache(DATABASE_CACHE_POSTGRESQL).drop_db() - # DbCache(DATABASEFILE_CACHE_MYSQL).drop_db() - except Exception: + DbCache(CACHE_DBNAME1).drop_db() + DbCache(CACHE_DBNAME2).drop_db() + except: pass - try: - con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + if os.getenv('UNITTEST_DATABASE') == 'postgresql': + try: + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + cur = con.cursor() + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier('bitcoinlibcache.unittest')) + ) + cur.close() + con.close() + except Exception: + pass + elif os.getenv('UNITTEST_DATABASE') == 'mysql': + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier('bitcoinlibcache.unittest')) - ) + cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME1)) + cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME2)) + cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME1)) + cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME2)) + con.commit() cur.close() con.close() - except Exception: - pass + else: + if os.path.isfile(DATABASE_CACHE_UNITTESTS): + try: + os.remove(DATABASE_CACHE_UNITTESTS) + os.remove(DATABASE_CACHE_UNITTESTS2) + except: + pass + def test_service_cache_transactions(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) address = '1JQ7ybfFBoWhPJpjoihezpeAjd2xv9nXaN' # Get 2 transactions, nothing in cache res = srv.gettransactions(address, limit=2) @@ -853,7 +870,7 @@ def test_service_cache_transactions(self): # FIXME: Disabled, lack of providers # def test_service_cache_gettransaction(self): - # srv = ServiceTest(network='litecoin_testnet', cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + # srv = ServiceTest(network='litecoin_testnet', cache_uri=DATABASE_CACHE_UNITTESTS2) # txid = 'b6533d361daac291f64fff32a5c157a4785b423ce36e2eac27117879f93973da' # # t = srv.gettransaction(txid) @@ -878,7 +895,7 @@ def test_service_cache_transactions(self): def test_service_cache_transactions_after_txid(self): # Do not store anything in cache if after_txid is used - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2, exclude_providers=['mempool']) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2, exclude_providers=['mempool']) address = '12spqcvLTFhL38oNJDDLfW1GpFGxLdaLCL' res = srv.gettransactions(address, after_txid='5f31da8f47a5bd92a6929179082c559e8acc270a040b19838230aab26309cf2d') @@ -898,7 +915,7 @@ def test_service_cache_transactions_after_txid(self): self.assertGreaterEqual(srv.results_cache_n, 1) def test_service_cache_transaction_coinbase(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2, exclude_providers=['bitaps', 'bitgo']) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2, exclude_providers=['bitaps', 'bitgo']) t = srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') if t: self.assertGreaterEqual(srv.results_cache_n, 0) @@ -922,7 +939,7 @@ def test_service_cache_transaction_segwit_database(self): self.assertEqual(t.raw_hex(), rawtx) def test_service_cache_with_latest_tx_query(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) address = 'bc1qxfrgfhs49d7dtcfzlhp7f7cwsp8zpp60hywp0f' after_txid = '13401ad121c8ae91e18b4bb0db5d8f350a2b0b5ddd5ca26165137bf07fefad90' srv.gettransaction('4156e78f347e47d2ccdd4a19614d958c6e4502d09a68f63ed0c72691f63a5028') @@ -932,7 +949,7 @@ def test_service_cache_with_latest_tx_query(self): self.assertGreaterEqual(len(txs), 5) def test_service_cache_correctly_update_spent_info(self): - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) srv.gettransactions('1KoAvaL3wfpcNvGCQYkqFJG9Ccqm52sZHa', limit=1) txs = srv.gettransactions('1KoAvaL3wfpcNvGCQYkqFJG9Ccqm52sZHa') self.assertTrue(txs[0].outputs[0].spent) @@ -974,7 +991,7 @@ def test_service_cache_disabled(self): def test_service_cache_transaction_p2sh_p2wpkh_input(self): txid = '6ab6432a6b7b04ecc335c6e8adccc45c25f46e33752478f0bcacaf3f1b61ad92' - srv = ServiceTest(cache_uri=DATABASEFILE_CACHE_UNITTESTS2) + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) t = srv.gettransaction(txid) self.assertEqual(t.size, 249) self.assertEqual(srv.results_cache_n, 0) diff --git a/tests/test_tools.py b/tests/test_tools.py index 370d9f54..f3379a81 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -29,36 +29,24 @@ def database_init(dbname=DATABASE_NAME): if os.getenv('UNITTEST_DATABASE') == 'postgresql': con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() - # try: cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format( sql.Identifier(dbname)) ) cur.execute(sql.SQL("CREATE DATABASE {}").format( sql.Identifier(dbname)) ) - # except Exception: - # pass - # finally: - # cur.close() - # con.close() cur.close() con.close() return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - try: - con = mysql.connector.connect(user='user', host='localhost', password='password') - except: - try: - con = mysql.connector.connect(user='root', host='localhost', password='password') - except: - con = mysql.connector.connect(user='root', host='localhost') + con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) con.commit() cur.close() con.close() - return 'mysql://root@localhost:3306/' + dbname + return 'mysql://user:password@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 0fe0014f..303f6365 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -43,30 +43,13 @@ DATABASE_NAME = 'bitcoinlib_test' DATABASE_NAME_2 = 'bitcoinlib2_test' -# db_uris = ( -# ('sqlite', 'sqlite:///' + DATABASEFILE_UNITTESTS, 'sqlite:///' + DATABASEFILE_UNITTESTS_2),) - print("DATABASE USED: %s" % os.getenv('UNITTEST_DATABASE')) -# if UNITTESTS_FULL_DATABASE_TEST: -# db_uris += ( -# ('mysql', 'mysql://root:root@localhost:3306/' + DATABASE_NAME, -# 'mysql://root:root@localhost:3306/' + DATABASE_NAME_2), -# ('postgresql', 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME, -# 'postgresql://postgres:postgres@localhost:5432/' + DATABASE_NAME_2), -# ) -# -# -# params = (('SCHEMA', 'DATABASE_URI', 'DATABASE_URI_2'), ( -# db_uris -# )) - - def database_init(dbname=DATABASE_NAME): session.close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': - # con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) - # cur = con.cursor() + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + cur = con.cursor() # try: # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) # cur.execute(sql.SQL("UPDATE pg_database SET datallowconn = 'false' WHERE datname = '{}'").format( @@ -77,7 +60,6 @@ def database_init(dbname=DATABASE_NAME): # except Exception as e: # print(e) # res = cur.execute(sql.SQL("SELECT sum(numbackends) FROM pg_stat_database")) - # print(res) # res = cur.execute(sql.SQL(""" # DO $$ DECLARE # r RECORD; @@ -86,27 +68,14 @@ def database_init(dbname=DATABASE_NAME): # EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; # END LOOP; # END $$;""")) - # print(res) - # try: - # cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) - # cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) - # except: - # pass - # try: - # # drop all tables - # finally: - # cur.close() - # con.close() - # con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) - # cur = con.cursor() - # cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) - # con.commit() - # cur.close() - # con.close() - # return 'postgresql://postgres:postgres@localhost:5432/' + dbname - # postgresql = testing.postgresql.Postgresql() - # return postgresql.url() - return 'testing.postgresql' + try: + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) + except: + pass + cur.close() + con.close() + return 'postgresql://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() From 4546bc1519cb93852f0c30e63e3385444aa887d1 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 18:29:49 +0100 Subject: [PATCH 078/207] Fix link to sqlite database for testing --- bitcoinlib/config/config.py | 3 ++- bitcoinlib_cache_unittest1 | Bin 0 -> 57344 bytes bitcoinlib_cache_unittest2 | Bin 0 -> 57344 bytes tests/bitcoinlib_cache_unittest1 | Bin 0 -> 57344 bytes tests/bitcoinlib_cache_unittest2 | Bin 0 -> 57344 bytes tests/test_services.py | 13 +++++++------ 6 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 bitcoinlib_cache_unittest1 create mode 100644 bitcoinlib_cache_unittest2 create mode 100644 tests/bitcoinlib_cache_unittest1 create mode 100644 tests/bitcoinlib_cache_unittest2 diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 24baf365..f55ddff1 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -255,7 +255,8 @@ def config_get(section, var, fallback, is_boolean=False): BCL_DATABASE_DIR.mkdir(parents=True, exist_ok=True) default_databasefile = DEFAULT_DATABASE = \ config_get('locations', 'default_databasefile', fallback='bitcoinlib.sqlite') - if not default_databasefile.startswith('postgresql') or default_databasefile.startswith('mysql'): + if not (default_databasefile.startswith('postgresql') or default_databasefile.startswith('mysql') or + default_databasefile.startswith('mariadb')): DEFAULT_DATABASE = str(Path(BCL_DATABASE_DIR, default_databasefile)) default_databasefile_cache = DEFAULT_DATABASE_CACHE = \ config_get('locations', 'default_databasefile_cache', fallback='bitcoinlib_cache.sqlite') diff --git a/bitcoinlib_cache_unittest1 b/bitcoinlib_cache_unittest1 new file mode 100644 index 0000000000000000000000000000000000000000..406de24c9e2e598c4244252b3500e9183fa6a7a4 GIT binary patch literal 57344 zcmeIvu}Z^G6b9g%wpLBN#s}z-#nnY{anM;@#KGA?!9fJ+%eWOEs4w7qNO1C0+qFYV z#nLaApPLZQ$vt0gZ?2!l56$Dt>+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8LN(NL1%Fh2WJNb2N9$%<5qm2zJTu`!O2r?*A6Ka zOTS!xZbCRG_k6j%xqccyG>49lrmrB^|sXK!5;&EfuKS{@Ip}ZfpV_1?o0^b}ZfCvY7f} zh2z(4@wP&_(sc>MfBjll@BiI5CO_|d!rpP`QiTKv5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7csfj<^Fi0|ddY_B3^WIwVO j*^LY%JCQ-8AL&J01PBlyK!5-N0t5&UAV7e?h6sEEJvAB$ literal 0 HcmV?d00001 diff --git a/tests/bitcoinlib_cache_unittest2 b/tests/bitcoinlib_cache_unittest2 new file mode 100644 index 0000000000000000000000000000000000000000..b103c21d46cdcc8eb8fb26fae8c7a6a5a1dfb5b2 GIT binary patch literal 57344 zcmeIvu}Z^G6b9g%wpLBN#z)BF>LN(NL1%Fh2WJNb2N9$%<5qm2zJTu`!O2r?*A6Ka zOTS!xZbCRG_k6j%xqccyG>49lrmrB^|sXK!5;&EfuKS{@Ip}ZfpV_1?o0^b}ZfCvY7f} zh2z(4@wP&_(sc>MfBjll@BiI5CO_|d!rpP`QiTKv5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7csfj<^Fi0|ddY_B3^WIwVO j*^LY%JCQ-8AL&J01PBlyK!5-N0t5&UAV7e?h6sEEJvAB$ literal 0 HcmV?d00001 diff --git a/tests/test_services.py b/tests/test_services.py index fa763e35..99835e93 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -40,15 +40,16 @@ CACHE_DBNAME1 = 'bitcoinlib_cache_unittest1' CACHE_DBNAME2 = 'bitcoinlib_cache_unittest2' -if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': - DATABASE_CACHE_UNITTESTS = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME1 - DATABASE_CACHE_UNITTESTS2 = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME2 -elif os.getenv('UNITTEST_DATABASE') == 'postgresql': +# FIXME: Allow mariadb for cache database +# if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': +# DATABASE_CACHE_UNITTESTS = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME1 +# DATABASE_CACHE_UNITTESTS2 = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME2 +if os.getenv('UNITTEST_DATABASE') == 'postgresql': DATABASE_CACHE_UNITTESTS = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 DATABASE_CACHE_UNITTESTS2 = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 else: - DATABASE_CACHE_UNITTESTS = 'sqlite://' + os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME1) + '.sqlite' - DATABASE_CACHE_UNITTESTS2 = 'sqlite://' + os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME2) + '.sqlite' + DATABASE_CACHE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME1) + '.sqlite' + DATABASE_CACHE_UNITTESTS2 = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME2) + '.sqlite' DATABASES_CACHE = [ DATABASE_CACHE_UNITTESTS, From 52e64fb435ef788483d6667dd91b103de41ab525 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 18:30:13 +0100 Subject: [PATCH 079/207] Fix link to sqlite database for testing --- bitcoinlib_cache_unittest1 | Bin 57344 -> 0 bytes bitcoinlib_cache_unittest2 | Bin 57344 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bitcoinlib_cache_unittest1 delete mode 100644 bitcoinlib_cache_unittest2 diff --git a/bitcoinlib_cache_unittest1 b/bitcoinlib_cache_unittest1 deleted file mode 100644 index 406de24c9e2e598c4244252b3500e9183fa6a7a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeIvu}Z^G6b9g%wpLBN#s}z-#nnY{anM;@#KGA?!9fJ+%eWOEs4w7qNO1C0+qFYV z#nLaApPLZQ$vt0gZ?2!l56$Dt>+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8 Date: Wed, 7 Feb 2024 19:43:02 +0100 Subject: [PATCH 080/207] Fix cache database for PostgreSQL --- tests/bitcoinlib_cache_unittest1 | Bin 57344 -> 57344 bytes tests/bitcoinlib_cache_unittest2 | Bin 57344 -> 57344 bytes tests/test_services.py | 10 +++++++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/bitcoinlib_cache_unittest1 b/tests/bitcoinlib_cache_unittest1 index b103c21d46cdcc8eb8fb26fae8c7a6a5a1dfb5b2..a4ac3cc18cf9af2d8fc9df470b1aeb8e4eac9c76 100644 GIT binary patch delta 35 kcmZoTz}#?vd4jZ{8v_Fa9~AQfX}5_w#*A(o6BgVD0E99JzW@LL delta 35 kcmZoTz}#?vd4jZ{1p@;E9~AQfX^V+E#*7vl6BgVD0D<%el>h($ diff --git a/tests/bitcoinlib_cache_unittest2 b/tests/bitcoinlib_cache_unittest2 index b103c21d46cdcc8eb8fb26fae8c7a6a5a1dfb5b2..a4ac3cc18cf9af2d8fc9df470b1aeb8e4eac9c76 100644 GIT binary patch delta 35 kcmZoTz}#?vd4jZ{8v_Fa9~AQfX}5_w#*A(o6BgVD0E99JzW@LL delta 35 kcmZoTz}#?vd4jZ{1p@;E9~AQfX^V+E#*7vl6BgVD0D<%el>h($ diff --git a/tests/test_services.py b/tests/test_services.py index 99835e93..ec2d6571 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -820,9 +820,13 @@ def setUpClass(cls): try: con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() - cur.execute(sql.SQL("CREATE DATABASE {}").format( - sql.Identifier('bitcoinlibcache.unittest')) - ) + try: + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(CACHE_DBNAME1))) + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(CACHE_DBNAME2))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(CACHE_DBNAME1))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(CACHE_DBNAME2))) + except: + pass cur.close() con.close() except Exception: From e9dc5c1161db77e2bc0e61bca82c4d0dae740adc Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 21:26:36 +0100 Subject: [PATCH 081/207] Enable unittests for PostgreSQL --- .github/workflows/unittests-noscrypt.yaml | 1 - .github/workflows/unittests-postgresql.yaml | 26 +++++++++++++++++++++ .github/workflows/unittests.yaml | 3 +-- .github/workflows/unittests_windows.yaml | 1 - tests/test_wallets.py | 23 ++++++++++-------- 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/unittests-postgresql.yaml diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 26ae6aaa..9ae4dfe6 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -20,6 +20,5 @@ jobs: - name: Test with coverage env: BCL_CONFIG_FILE: config_encryption.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False DB_FIELD_ENCRYPTION_KEY: 11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml new file mode 100644 index 00000000..e2090c17 --- /dev/null +++ b/.github/workflows/unittests-postgresql.yaml @@ -0,0 +1,26 @@ +name: Bitcoinlib Tests Ubuntu - PostgreSQL +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python: ["3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install .[dev] + - name: Test with coverage + env: + BCL_CONFIG_FILE: config.ini.unittest + UNITTEST_DATABASE: postgresql + run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 44b24ae2..98693087 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -7,7 +7,7 @@ jobs: strategy: matrix: - python: ["3.8", "3.10", "3.11"] + python: ["3.8", "3.11"] steps: - uses: actions/checkout@v3 @@ -22,7 +22,6 @@ jobs: - name: Test with coverage env: BCL_CONFIG_FILE: config.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False run: coverage run --source=bitcoinlib -m unittest -v - name: Coveralls diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index 183c6451..b5575167 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -19,6 +19,5 @@ jobs: - name: Test with coverage env: BCL_CONFIG_FILE: config.ini.unittest - UNITTESTS_FULL_DATABASE_TEST: False PYTHONUTF8: 1 run: coverage run --source=bitcoinlib -m unittest -v diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 303f6365..e9e96154 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -71,7 +71,8 @@ def database_init(dbname=DATABASE_NAME): try: cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) - except: + except Exception as e: + print("Error exception %s" % str(e)) pass cur.close() con.close() @@ -140,7 +141,9 @@ def test_wallet_create_key(self): def test_wallets_list(self): wallets = wallets_list(db_uri=self.database_uri) - self.assertEqual(wallets[0]['name'], 'test_wallet_create') + wallet_names = [w['name'] for w in wallets] + # self.assertEqual(wallets[0]['name'], 'test_wallet_create') + self.assertIn('test_wallet_create', wallet_names) def test_delete_wallet(self): Wallet.create( @@ -467,7 +470,7 @@ def setUpClass(cls): def test_wallet_export_hdwifs(self): # p2wpkh p = 'garage million cheese nephew original subject pass reward month practice advance decide' - w = Wallet.create("wif_import_p2wpkh", p, network='bitcoin', witness_type='segwit', + w = Wallet.create("wif_export_p2wpkh", p, network='bitcoin', witness_type='segwit', db_uri=self.database_uri) wif = 'zpub6s7HTSrGmNUWSgfbDMhYbXVuxA14yNnycS25v6ogicEauzUrRUkuCLQUWbJXP1NyXNqGmwpU6hZw7vr22a4yspwH8XQFjjwRmx' \ 'CKkXdDAXN' @@ -476,7 +479,7 @@ def test_wallet_export_hdwifs(self): # # p2sh_p2wpkh p = 'cluster census trash van rack skill feed inflict mixture vocal crew sea' - w = Wallet.create("wif_import_p2sh_p2wpkh", p, network='bitcoin', witness_type='p2sh-segwit', + w = Wallet.create("wif_export_p2sh_p2wpkh", p, network='bitcoin', witness_type='p2sh-segwit', db_uri=self.database_uri) wif = 'ypub6YMgBd4GfQjtxUf8ExorFUQEpBfUYTDz7E1tvfNgDqZeDEUuNNVXSNfsebis2cyeqWYXx6yaBBEQV7sJW3NGoXw5wsp9kkEsB2' \ 'DqiVquYLE' @@ -490,7 +493,7 @@ def test_wallet_export_hdwifs(self): 'Ba5zbv9', 'Zpub74JTMKMB9cTWwE9Hs4UVaHvddqPtR51D99x2B5EGyXyxEg3PW77vfmD15RZ86TVdwwwuUaCueBtvaL921mgyKe9Ya6LHCaMXnEp1' 'PMw4vDy'] - w = Wallet.create("wif_import_p2wsh", [p1, p2], witness_type='segwit', network='bitcoin', cosigner_id=0, + w = Wallet.create("wif_export_p2wsh", [p1, p2], witness_type='segwit', network='bitcoin', cosigner_id=0, db_uri=self.database_uri) for wif in w.wif(is_private=False): self.assertIn(wif, wifs) @@ -506,7 +509,7 @@ def test_wallet_export_hdwifs(self): 'P8nxY8N', 'Ypub6jVwyh6yYiRoA5zAnGY1g88G5LdaxkHX65d2kSW97yTBAF1RQwAs3UGPz8bX7LvQfg8tc9MQz7eZ79qVigELqSJzfFbGmPak4PZr' 'vW8fZXy'] - w = Wallet.create("wif_import_p2sh_p2wsh", [p1, p2, p3], sigs_required=2, witness_type='p2sh-segwit', + w = Wallet.create("wif_export_p2sh_p2wsh", [p1, p2, p3], sigs_required=2, witness_type='p2sh-segwit', network='bitcoin', cosigner_id=0, db_uri=self.database_uri) for wif in w.wif(is_private=False): self.assertIn(wif, wifs) @@ -1292,8 +1295,8 @@ def test_wallet_multisig_sign_with_external_single_key(self): HDKey(network=network), hdkey.public() ] - wallet = Wallet.create('Multisig-2-of-3-example', key_list, sigs_required=2, network=network, - db_uri=self.database_uri) + wallet = Wallet.create('test_wallet_multisig_sign_with_external_single_key', + key_list, sigs_required=2, network=network, db_uri=self.database_uri) wallet.new_key() wallet.utxos_update() wt = wallet.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, offline=False) @@ -2464,7 +2467,7 @@ def test_wallet_segwit_litecoin_sweep(self): def test_wallet_segwit_litecoin_multisig(self): p1 = 'only sing document speed outer gauge stand govern way column material odor' p2 = 'oyster pelican debate mass scene title pipe lock recipe flavor razor accident' - w = wallet_create_or_open('ltcswms', [p1, p2], network='litecoin', witness_type='segwit', + w = wallet_create_or_open('ltc_segwit_ms', [p1, p2], network='litecoin', witness_type='segwit', cosigner_id=0, db_uri=self.database_uri) w.get_keys(number_of_keys=2) w.utxo_add('ltc1qkewaz7lxn75y6wppvqlsfhrnq5p5mksmlp26n8xsef0556cdfzqq2uhdrt', 2100000000000001, @@ -2512,7 +2515,7 @@ def test_wallet_segwit_multiple_account_paths(self): def test_wallet_segwit_multiple_networks_accounts(self): pk1 = 'surround vacant shoot aunt acoustic liar barely you expand rug industry grain' pk2 = 'defy push try brush ceiling sugar film present goat settle praise toilet' - wallet = Wallet.create(keys=[pk1, pk2], network='bitcoin', name='test_wallet_multicurrency', + wallet = Wallet.create(keys=[pk1, pk2], network='bitcoin', name='test_wallet_segwit_multicurrency', witness_type='segwit', cosigner_id=0, encoding='base58', db_uri=self.database_uri) wallet.new_account(network='litecoin') From 409aa7969c22c947c2aebc1a7f0617430d787d83 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 21:45:18 +0100 Subject: [PATCH 082/207] Start PostgreSQL server before unittesting --- .github/workflows/unittests-postgresql.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index e2090c17..ed8070ef 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -5,6 +5,23 @@ jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres + + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: bitcoinlib_test + + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + strategy: matrix: python: ["3.10"] From dd6086d594c600520804b6ad6574030f513ad95c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 22:07:42 +0100 Subject: [PATCH 083/207] Use psycopg3 everywhere for PostgreSQL database --- bitcoinlib/data/config.ini | 11 ++++++----- bitcoinlib/db.py | 2 ++ tests/test_services.py | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bitcoinlib/data/config.ini b/bitcoinlib/data/config.ini index b2bfe197..39610d9d 100644 --- a/bitcoinlib/data/config.ini +++ b/bitcoinlib/data/config.ini @@ -16,13 +16,14 @@ # Default directory for database files. Relative paths will be based in user or bitcoinlib installation directory. Only used for sqlite files. ;database_dir=database -# Default database file for wallets, keys and transactions. Relative paths will be based in 'database_dir' +# Default database file for wallets, keys and transactions. Relative paths for sqlite will be based in 'database_dir'. +# You can use SQLite, PostgreSQL and MariaDB (MySQL) databases ;default_databasefile=bitcoinlib.sqlite -;default_databasefile_cache=bitcoinlib_cache.sqlite +;default_databasefile=postgresql+psycopg://postgres:bitcoinlib@localhost:5432/bitcoinlib -# You can also use PostgreSQL or MySQL databases, for instance: -;default_databasefile=postgresql://postgres:bitcoinlib@localhost:5432/bitcoinlib -;default_databasefile_cache==postgresql://postgres:bitcoinlib@localhost:5432/bitcoinlib_cache +# For caching SQLite or PostgreSQL databases can be used. +;default_databasefile_cache=bitcoinlib_cache.sqlite +;default_databasefile_cache==postgresql+psycopg://postgres:bitcoinlib@localhost:5432/bitcoinlib_cache [common] # Allow database threads in SQLite databases diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index b82cbeab..0223f8b7 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -67,6 +67,8 @@ def __init__(self, db_uri=None, password=None): if self.o.scheme == 'mysql': db_uri += "&" if "?" in db_uri else "?" db_uri += 'binary_prefix=true' + if self.o.scheme == 'postgresql': + db_uri = self.o._replace(scheme="postgresql+psycopg").geturl() self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') Session = sessionmaker(bind=self.engine) diff --git a/tests/test_services.py b/tests/test_services.py index ec2d6571..5187bc91 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -40,13 +40,13 @@ CACHE_DBNAME1 = 'bitcoinlib_cache_unittest1' CACHE_DBNAME2 = 'bitcoinlib_cache_unittest2' -# FIXME: Allow mariadb for cache database +# FIXME: Mariadb for cache database does not work due to problem with BLOB indexing # if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': # DATABASE_CACHE_UNITTESTS = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME1 # DATABASE_CACHE_UNITTESTS2 = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME2 if os.getenv('UNITTEST_DATABASE') == 'postgresql': - DATABASE_CACHE_UNITTESTS = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 - DATABASE_CACHE_UNITTESTS2 = 'postgresql://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 + DATABASE_CACHE_UNITTESTS = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 + DATABASE_CACHE_UNITTESTS2 = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 else: DATABASE_CACHE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME1) + '.sqlite' DATABASE_CACHE_UNITTESTS2 = os.path.join(str(BCL_DATABASE_DIR), CACHE_DBNAME2) + '.sqlite' From f97e73332a09e9869f29d9069cb44465964f2aad Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 7 Feb 2024 22:32:13 +0100 Subject: [PATCH 084/207] Use latest version of github actions --- .github/workflows/unittests-noscrypt.yaml | 4 ++-- .github/workflows/unittests-postgresql.yaml | 12 ++++-------- .github/workflows/unittests.yaml | 4 ++-- .github/workflows/unittests_windows.yaml | 4 ++-- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 9ae4dfe6..0f6ecb1c 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.10' architecture: 'x64' diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index ed8070ef..817c1e1a 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -22,17 +22,13 @@ jobs: ports: - 5432:5432 - strategy: - matrix: - python: ["3.10"] - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} - architecture: 'x64' + python-version: '3.10' + architecture: 'x64' - name: Install dependencies run: | python -m pip install .[dev] diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 98693087..a5363a42 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -10,9 +10,9 @@ jobs: python: ["3.8", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} architecture: 'x64' diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index b5575167..529574ae 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -7,9 +7,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.11' architecture: 'x64' From 2871358b5ed1b6a778ca90215b9ecf4285b946d8 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 8 Feb 2024 08:33:59 +0100 Subject: [PATCH 085/207] Try with same database --- bitcoinlib/db_cache.py | 9 --------- tests/test_wallets.py | 20 +------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index ec8e85ff..c386606a 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -27,15 +27,6 @@ _logger = logging.getLogger(__name__) -# try: -# dbcacheurl_obj = urlparse(DEFAULT_DATABASE_CACHE) -# if dbcacheurl_obj.netloc: -# dbcacheurl = dbcacheurl_obj.netloc.replace(dbcacheurl_obj.password, 'xxx') -# else: -# dbcacheurl = dbcacheurl_obj.path -# _logger.info("Default Cache Database %s" % dbcacheurl) -# except Exception: -# _logger.warning("Default Cache Database: unable to parse URL") Base = declarative_base() diff --git a/tests/test_wallets.py b/tests/test_wallets.py index e9e96154..c2f86dde 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -50,26 +50,8 @@ def database_init(dbname=DATABASE_NAME): if os.getenv('UNITTEST_DATABASE') == 'postgresql': con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() - # try: - # cur.execute(sql.SQL("ALTER DATABASE {} allow_connections = off").format(sql.Identifier(dbname))) - # cur.execute(sql.SQL("UPDATE pg_database SET datallowconn = 'false' WHERE datname = '{}'").format( - # sql.Identifier(dbname))) - # cur.execute(sql.SQL("SELECT pg_terminate_backend(pg_stat_activity.pid)" - # "FROM pg_stat_activity WHERE pg_stat_activity.datname = '{}'" - # "AND pid <> pg_backend_pid();").format(sql.Identifier(dbname))) - # except Exception as e: - # print(e) - # res = cur.execute(sql.SQL("SELECT sum(numbackends) FROM pg_stat_database")) - # res = cur.execute(sql.SQL(""" - # DO $$ DECLARE - # r RECORD; - # BEGIN - # FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP - # EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE'; - # END LOOP; - # END $$;""")) try: - cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + # cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) except Exception as e: print("Error exception %s" % str(e)) From 838624272d90ad0ea13378f2d2fde69d32a613a7 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Thu, 8 Feb 2024 18:11:17 +0100 Subject: [PATCH 086/207] Use biginteger for output_n in tx output --- bitcoinlib/db.py | 2 +- tests/bitcoinlib_cache_unittest1 | Bin 57344 -> 0 bytes tests/bitcoinlib_cache_unittest2 | Bin 57344 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 tests/bitcoinlib_cache_unittest1 delete mode 100644 tests/bitcoinlib_cache_unittest2 diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 0223f8b7..9e1b8cc7 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -477,7 +477,7 @@ class DbTransactionOutput(Base): doc="Transaction ID of parent transaction") transaction = relationship("DbTransaction", back_populates='outputs', doc="Link to transaction object") - output_n = Column(Integer, primary_key=True, doc="Sequence number of transaction output") + output_n = Column(BigInteger, primary_key=True, doc="Sequence number of transaction output") key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this transaction output") key = relationship("DbKey", back_populates="transaction_outputs", doc="List of DbKey object used in this output") address = Column(String(255), diff --git a/tests/bitcoinlib_cache_unittest1 b/tests/bitcoinlib_cache_unittest1 deleted file mode 100644 index a4ac3cc18cf9af2d8fc9df470b1aeb8e4eac9c76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeIvu}Z^G6b9g%wpLBN#z)BF>LR!}=+r@6#KGA?!9fJ+%eWOEs4w7qNO1C0+qFYV z#nLaApPLZQ$vt0gZ?2!l56$Dt>+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8LR!}=+r@6#KGA?!9fJ+%eWOEs4w7qNO1C0+qFYV z#nLaApPLZQ$vt0gZ?2!l56$Dt>+}7%IZa2YDAHxqq?AUH!+G-jc@X>JDi$|O_E9>y zyP6e8 Date: Thu, 8 Feb 2024 22:34:14 +0100 Subject: [PATCH 087/207] Add encrypted field test for mysql and postgresql --- tests/test_security.py | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index 6cb23f20..ea242ce3 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -25,16 +25,39 @@ from bitcoinlib.wallets import Wallet from bitcoinlib.config.config import DATABASE_ENCRYPTION_ENABLED -DATABASEFILE_UNITTESTS_ENCRYPTED = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest_security.sqlite') -# DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql://postgres:postgres@localhost:5432/bitcoinlib_security' +try: + import mysql.connector + import psycopg + from psycopg import sql +except ImportError: + pass # Only necessary when mysql or postgres is used -class TestSecurity(TestCase): - @classmethod - def setUpClass(cls): - if os.path.isfile(DATABASEFILE_UNITTESTS_ENCRYPTED): - os.remove(DATABASEFILE_UNITTESTS_ENCRYPTED) +if os.getenv('UNITTEST_DATABASE') == 'postgresql': + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + cur = con.cursor() + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier('bitcoinlib_security'))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier('bitcoinlib_security'))) + cur.close() + con.close() + DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql://postgres:postgres@localhost:5432/bitcoinlib_security' +elif os.getenv('UNITTEST_DATABASE') == 'mysql': + con = mysql.connector.connect(user='user', host='localhost', password='password') + cur = con.cursor() + cur.execute("DROP DATABASE IF EXISTS {}".format('bitcoinlib_security')) + cur.execute("CREATE DATABASE {}".format('bitcoinlib_security')) + con.commit() + cur.close() + con.close() + DATABASEFILE_UNITTESTS_ENCRYPTED = 'mysql://user:password@localhost:3306/bitcoinlib_security' +else: + DATABASEFILE_UNITTESTS_ENCRYPTED = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest_security.sqlite') + if os.path.isfile(DATABASEFILE_UNITTESTS_ENCRYPTED): + os.remove(DATABASEFILE_UNITTESTS_ENCRYPTED) + + +class TestSecurity(TestCase): def test_security_wallet_field_encryption(self): pk = 'xprv9s21ZrQH143K2HrtPWvqgD8mUhMrrfE1ZME43baM8ti3hWgJwWX1wjHc25y2x11seT5G3KeHFY28MyTRxceeW22kMDAWsMDn7' \ @@ -54,7 +77,10 @@ def test_security_wallet_field_encryption(self): wallet.new_key() self.assertEqual(wallet.main_key.wif, pk) - db_query = text('SELECT wif, private FROM keys WHERE id=%d' % wallet._dbwallet.main_key_id) + if os.getenv('UNITTEST_DATABASE') == 'mysql': + db_query = text("SELECT wif, private FROM `keys` WHERE id=%d" % wallet._dbwallet.main_key_id) + else: + db_query = text("SELECT wif, private FROM keys WHERE id=%d" % wallet._dbwallet.main_key_id) encrypted_main_key_wif = wallet._session.execute(db_query).fetchone()[0] encrypted_main_key_private = wallet._session.execute(db_query).fetchone()[1] self.assertIn(type(encrypted_main_key_wif), (bytes, memoryview), "Encryption of database private key failed!") From 9d7ff008d2b0c8b06e7d30ad79edcb447f93d2a2 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 11 Feb 2024 18:37:13 +0100 Subject: [PATCH 088/207] Unittest postgresql database --- tests/db_0_5.py | 445 ------------------------------------------ tests/test_db.py | 77 +++++--- tests/test_tools.py | 2 +- tests/test_wallets.py | 4 +- 4 files changed, 55 insertions(+), 473 deletions(-) delete mode 100644 tests/db_0_5.py diff --git a/tests/db_0_5.py b/tests/db_0_5.py deleted file mode 100644 index d3fba8d2..00000000 --- a/tests/db_0_5.py +++ /dev/null @@ -1,445 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# DataBase - SqlAlchemy database definitions -# © 2016 - 2020 February - 1200 Web Development -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -from sqlalchemy import create_engine -from sqlalchemy import (Column, Integer, BigInteger, UniqueConstraint, CheckConstraint, String, Boolean, Sequence, - ForeignKey, DateTime, LargeBinary) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.ext.compiler import compiles -from sqlalchemy.orm import sessionmaker, relationship, close_all_sessions -from urllib.parse import urlparse -from bitcoinlib.main import * - -_logger = logging.getLogger(__name__) -Base = declarative_base() - - -@compiles(LargeBinary, "mysql") -def compile_largebinary_mysql(type_, compiler, **kwargs): - length = type_.length - element = "BLOB" if not length else "VARBINARY(%d)" % length - return element - - -class Db: - """ - Bitcoinlib Database object used by Service() and HDWallet() class. Initialize database and open session when - creating database object. - - Create new database if is doesn't exist yet - - """ - def __init__(self, db_uri=None): - if db_uri is None: - db_uri = DEFAULT_DATABASE - self.o = urlparse(db_uri) - if not self.o.scheme or \ - len(self.o.scheme) < 2: # Dirty hack to avoid issues with urlparse on Windows confusing drive with scheme - db_uri = 'sqlite:///%s' % db_uri - if db_uri.startswith("sqlite://") and ALLOW_DATABASE_THREADS: - db_uri += "&" if "?" in db_uri else "?" - db_uri += "check_same_thread=False" - if self.o.scheme == 'mysql': - db_uri += "&" if "?" in db_uri else "?" - db_uri += 'binary_prefix=true' - self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') - - Session = sessionmaker(bind=self.engine) - Base.metadata.create_all(self.engine) - self._import_config_data(Session) - self.session = Session() - - _logger.info("Using Database %s" % db_uri) - self.db_uri = db_uri - - # VERIFY AND UPDATE DATABASE - # Just a very simple database update script, without any external libraries for now - # - version_db = self.session.query(DbConfig.value).filter_by(variable='version').scalar() - if version_db[:3] == '0.4' and BITCOINLIB_VERSION[:3] == '0.5': - raise ValueError("Old database version found (<0.4.19). Cannot to 0.5 version database automatically, " - "use db_update tool to update") - try: - if BITCOINLIB_VERSION != version_db: - _logger.warning("BitcoinLib database (%s) is from different version then library code (%s). " - "Let's try to update database." % (version_db, BITCOINLIB_VERSION)) - db_update(self, version_db, BITCOINLIB_VERSION) - - except Exception as e: - _logger.warning("Error when verifying version or updating database: %s" % e) - - def drop_db(self, yes_i_am_sure=False): - if yes_i_am_sure: - self.session.commit() - self.session.close_all() - close_all_sessions() - Base.metadata.drop_all(self.engine) - - @staticmethod - def _import_config_data(ses): - session = ses() - installation_date = session.query(DbConfig.value).filter_by(variable='installation_date').scalar() - if not installation_date: - session.merge(DbConfig(variable='version', value=BITCOINLIB_VERSION)) - session.merge(DbConfig(variable='installation_date', value=str(datetime.now()))) - url = '' - try: - url = str(session.bind.url) - except Exception: - pass - session.merge(DbConfig(variable='installation_url', value=url)) - session.commit() - session.close() - - -def add_column(engine, table_name, column): - """ - Used to add new column to database with migration and update scripts - - :param engine: - :param table_name: - :param column: - :return: - """ - column_name = column.compile(dialect=engine.dialect) - column_type = column.type.compile(engine.dialect) - engine.execute("ALTER TABLE %s ADD COLUMN %s %s" % (table_name, column_name, column_type)) - - -class DbConfig(Base): - """ - BitcoinLib configuration variables - - """ - __tablename__ = 'config' - variable = Column(String(30), primary_key=True) - value = Column(String(255)) - - -class DbWallet(Base): - """ - Database definitions for wallets in Sqlalchemy format - - Contains one or more keys. - - """ - __tablename__ = 'wallets' - id = Column(Integer, Sequence('wallet_id_seq'), primary_key=True, doc="Unique wallet ID") - name = Column(String(80), unique=True, doc="Unique wallet name") - owner = Column(String(50), doc="Wallet owner") - network_name = Column(String(20), ForeignKey('networks.name'), doc="Name of network, i.e.: bitcoin, litecoin") - network = relationship("DbNetwork", doc="Link to DbNetwork object") - purpose = Column(Integer, - doc="Wallet purpose ID. BIP-44 purpose field, indicating which key-scheme is used default is 44") - scheme = Column(String(25), doc="Key structure type, can be BIP-32 or single") - witness_type = Column(String(20), default='legacy', - doc="Wallet witness type. Can be 'legacy', 'segwit' or 'p2sh-segwit'. Default is legacy.") - encoding = Column(String(15), default='base58', - doc="Default encoding to use for address generation, i.e. base58 or bech32. Default is base58.") - main_key_id = Column(Integer, - doc="Masterkey ID for this wallet. All other keys are derived from the masterkey in a " - "HD wallet bip32 wallet") - keys = relationship("DbKey", back_populates="wallet", doc="Link to keys (DbKeys objects) in this wallet") - transactions = relationship("DbTransaction", back_populates="wallet", - doc="Link to transaction (DbTransactions) in this wallet") - multisig_n_required = Column(Integer, default=1, doc="Number of required signature for multisig, " - "only used for multisignature master key") - sort_keys = Column(Boolean, default=False, doc="Sort keys in multisig wallet") - parent_id = Column(Integer, ForeignKey('wallets.id'), doc="Wallet ID of parent wallet, used in multisig wallets") - children = relationship("DbWallet", lazy="joined", join_depth=2, - doc="Wallet IDs of children wallets, used in multisig wallets") - multisig = Column(Boolean, default=True, doc="Indicates if wallet is a multisig wallet. Default is True") - cosigner_id = Column(Integer, - doc="ID of cosigner of this wallet. Used in multisig wallets to differentiate between " - "different wallets") - key_path = Column(String(100), - doc="Key path structure used in this wallet. Key path for multisig wallet, use to create " - "your own non-standard key path. Key path must follow the following rules: " - "* Path start with masterkey (m) and end with change / address_index " - "* If accounts are used, the account level must be 3. I.e.: m/purpose/coin_type/account/ " - "* All keys must be hardened, except for change, address_index or cosigner_id " - " Max length of path is 8 levels") - default_account_id = Column(Integer, doc="ID of default account for this wallet if multiple accounts are used") - - __table_args__ = ( - CheckConstraint(scheme.in_(['single', 'bip32']), name='constraint_allowed_schemes'), - CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_default_address_encodings_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), name='wallet_constraint_allowed_types'), - ) - - def __repr__(self): - return "" % (self.name, self.network_name) - - -class DbKeyMultisigChildren(Base): - """ - Use many-to-many relationship for multisig keys. A multisig keys contains 2 or more child keys - and a child key can be used in more then one multisig key. - - """ - __tablename__ = 'key_multisig_children' - - parent_id = Column(Integer, ForeignKey('keys.id'), primary_key=True) - child_id = Column(Integer, ForeignKey('keys.id'), primary_key=True) - key_order = Column(Integer, Sequence('key_multisig_children_id_seq')) - - -class DbKey(Base): - """ - Database definitions for keys in Sqlalchemy format - - Part of a wallet, and used by transactions - - """ - __tablename__ = 'keys' - id = Column(Integer, Sequence('key_id_seq'), primary_key=True, doc="Unique Key ID") - parent_id = Column(Integer, Sequence('parent_id_seq'), doc="Parent Key ID. Used in HD wallets") - name = Column(String(80), index=True, doc="Key name string") - account_id = Column(Integer, index=True, doc="ID of account if key is part of a HD structure") - depth = Column(Integer, - doc="Depth of key if it is part of a HD structure. Depth=0 means masterkey, " - "depth=1 are the masterkeys children.") - change = Column(Integer, doc="Change or normal address: Normal=0, Change=1") - address_index = Column(BigInteger, doc="Index of address in HD key structure address level") - public = Column(LargeBinary(128), index=True, doc="Bytes representation of public key") - private = Column(LargeBinary(128), index=True, doc="Bytes representation of private key") - wif = Column(String(255), index=True, doc="Public or private WIF (Wallet Import Format) representation") - compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") - key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") - address = Column(String(255), index=True, - doc="Address representation of key. An cryptocurrency address is a hash of the public key") - cosigner_id = Column(Integer, doc="ID of cosigner, used if key is part of HD Wallet") - encoding = Column(String(15), default='base58', doc='Encoding used to represent address: base58 or bech32') - purpose = Column(Integer, default=44, doc="Purpose ID, default is 44") - is_private = Column(Boolean, doc="Is key private or not?") - path = Column(String(100), doc="String of BIP-32 key path") - wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, doc="Wallet ID which contains this key") - wallet = relationship("DbWallet", back_populates="keys", doc="Related HDWallet object") - transaction_inputs = relationship("DbTransactionInput", cascade="all,delete", back_populates="key", - doc="All DbTransactionInput objects this key is part of") - transaction_outputs = relationship("DbTransactionOutput", cascade="all,delete", back_populates="key", - doc="All DbTransactionOutput objects this key is part of") - balance = Column(BigInteger, default=0, doc="Total balance of UTXO's linked to this key") - used = Column(Boolean, default=False, doc="Has key already been used on the blockchain in as input or output? " - "Default is False") - network_name = Column(String(20), ForeignKey('networks.name'), - doc="Name of key network, i.e. bitcoin, litecoin, dash") - latest_txid = Column(LargeBinary(32), doc="TxId of latest transaction downloaded from the blockchain") - network = relationship("DbNetwork", doc="DbNetwork object for this key") - multisig_parents = relationship("DbKeyMultisigChildren", backref='child_key', - primaryjoin=id == DbKeyMultisigChildren.child_id, - doc="List of parent keys") - multisig_children = relationship("DbKeyMultisigChildren", backref='parent_key', - order_by="DbKeyMultisigChildren.key_order", - primaryjoin=id == DbKeyMultisigChildren.parent_id, - doc="List of children keys") - - __table_args__ = ( - CheckConstraint(key_type.in_(['single', 'bip32', 'multisig']), name='constraint_key_types_allowed'), - CheckConstraint(encoding.in_(['base58', 'bech32']), name='constraint_address_encodings_allowed'), - UniqueConstraint('wallet_id', 'public', name='constraint_wallet_pubkey_unique'), - UniqueConstraint('wallet_id', 'private', name='constraint_wallet_privkey_unique'), - UniqueConstraint('wallet_id', 'wif', name='constraint_wallet_wif_unique'), - UniqueConstraint('wallet_id', 'address', name='constraint_wallet_address_unique'), - ) - - def __repr__(self): - return "" % (self.id, self.name, self.wif) - - -class DbNetwork(Base): - """ - Database definitions for networks in Sqlalchemy format - - Most network settings and variables can be found outside the database in the libraries configurations settings. - Use the bitcoinlib/data/networks.json file to view and manage settings. - - """ - __tablename__ = 'networks' - name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin, dash") - description = Column(String(50)) - - def __repr__(self): - return "" % (self.name, self.description) - - -# class TransactionType(enum.Enum): -# """ -# Incoming or Outgoing transaction Enumeration -# """ -# incoming = 1 -# outgoing = 2 - - -class DbTransaction(Base): - """ - Database definitions for transactions in Sqlalchemy format - - Refers to 1 or more keys which can be part of a wallet - - """ - __tablename__ = 'transactions' - id = Column(Integer, Sequence('transaction_id_seq'), primary_key=True, - doc="Unique transaction index for internal usage") - txid = Column(LargeBinary(32), index=True, doc="Bytes representation of transaction ID") - wallet_id = Column(Integer, ForeignKey('wallets.id'), index=True, - doc="ID of wallet which contains this transaction") - account_id = Column(Integer, index=True, doc="ID of account") - wallet = relationship("DbWallet", back_populates="transactions", - doc="Link to HDWallet object which contains this transaction") - witness_type = Column(String(20), default='legacy', doc="Is this a legacy or segwit transaction?") - version = Column(BigInteger, default=1, - doc="Tranaction version. Default is 1 but some wallets use another version number") - locktime = Column(BigInteger, default=0, - doc="Transaction level locktime. Locks the transaction until a specified block " - "(value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970)." - " Default value is 0 for transactions without locktime") - date = Column(DateTime, default=datetime.utcnow, - doc="Date when transaction was confirmed and included in a block. " - "Or when it was created when transaction is not send or confirmed") - coinbase = Column(Boolean, default=False, doc="Is True when this is a coinbase transaction, default is False") - confirmations = Column(Integer, default=0, - doc="Number of confirmation when this transaction is included in a block. " - "Default is 0: unconfirmed") - block_height = Column(Integer, index=True, doc="Number of block this transaction is included in") - size = Column(Integer, doc="Size of the raw transaction in bytes") - fee = Column(BigInteger, doc="Transaction fee") - inputs = relationship("DbTransactionInput", cascade="all,delete", - doc="List of all inputs as DbTransactionInput objects") - outputs = relationship("DbTransactionOutput", cascade="all,delete", - doc="List of all outputs as DbTransactionOutput objects") - status = Column(String(20), default='new', - doc="Current status of transaction, can be one of the following: new', " - "'unconfirmed', 'confirmed'. Default is 'new'") - is_complete = Column(Boolean, default=True, doc="Allow to store incomplete transactions, for instance if not all " - "inputs are known when retrieving UTXO's") - input_total = Column(BigInteger, default=0, - doc="Total value of the inputs of this transaction. Input total = Output total + fee. " - "Default is 0") - output_total = Column(BigInteger, default=0, - doc="Total value of the outputs of this transaction. Output total = Input total - fee") - network_name = Column(String(20), ForeignKey('networks.name'), doc="Blockchain network name of this transaction") - network = relationship("DbNetwork", doc="Link to DbNetwork object") - raw = Column(LargeBinary, - doc="Raw transaction hexadecimal string. Transaction is included in raw format on the blockchain") - verified = Column(Boolean, default=False, doc="Is transaction verified. Default is False") - - __table_args__ = ( - UniqueConstraint('wallet_id', 'txid', name='constraint_wallet_transaction_hash_unique'), - CheckConstraint(status.in_(['new', 'unconfirmed', 'confirmed']), - name='constraint_status_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit']), name='transaction_constraint_allowed_types'), - ) - - def __repr__(self): - return "" % (self.txid, self.confirmations) - - -class DbTransactionInput(Base): - """ - Transaction Input Table - - Relates to Transaction table and Key table - - """ - __tablename__ = 'transaction_inputs' - transaction_id = Column(Integer, ForeignKey('transactions.id'), primary_key=True, - doc="Input is part of transaction with this ID") - transaction = relationship("DbTransaction", back_populates='inputs', doc="Related DbTransaction object") - index_n = Column(Integer, primary_key=True, doc="Index number of transaction input") - key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this input") - key = relationship("DbKey", back_populates="transaction_inputs", doc="Related DbKey object") - address = Column(String(255), - doc="Address string of input, used if no key is associated. " - "An cryptocurrency address is a hash of the public key or a redeemscript") - witness_type = Column(String(20), default='legacy', - doc="Type of transaction, can be legacy, segwit or p2sh-segwit. Default is legacy") - prev_txid = Column(LargeBinary(32), - doc="Transaction hash of previous transaction. Previous unspent outputs (UTXO) is spent " - "in this input") - output_n = Column(BigInteger, doc="Output_n of previous transaction output that is spent in this input") - script = Column(LargeBinary, doc="Unlocking script to unlock previous locked output") - script_type = Column(String(20), default='sig_pubkey', - doc="Unlocking script type. Can be 'coinbase', 'sig_pubkey', 'p2sh_multisig', 'signature', " - "'unknown', 'p2sh_p2wpkh' or 'p2sh_p2wsh'. Default is sig_pubkey") - sequence = Column(BigInteger, doc="Transaction sequence number. Used for timelock transaction inputs") - value = Column(BigInteger, default=0, doc="Value of transaction input") - double_spend = Column(Boolean, default=False, - doc="Indicates if a service provider tagged this transaction as double spend") - - __table_args__ = (CheckConstraint(script_type.in_(['', 'coinbase', 'sig_pubkey', 'p2sh_multisig', - 'signature', 'unknown', 'p2sh_p2wpkh', 'p2sh_p2wsh']), - name='transactioninput_constraint_script_types_allowed'), - CheckConstraint(witness_type.in_(['legacy', 'segwit', 'p2sh-segwit']), - name='transactioninput_constraint_allowed_types'), - UniqueConstraint('transaction_id', 'index_n', name='constraint_transaction_input_unique')) - - -class DbTransactionOutput(Base): - """ - Transaction Output Table - - Relates to Transaction and Key table - - When spent is False output is considered an UTXO - - """ - __tablename__ = 'transaction_outputs' - transaction_id = Column(Integer, ForeignKey('transactions.id'), primary_key=True, - doc="Transaction ID of parent transaction") - transaction = relationship("DbTransaction", back_populates='outputs', - doc="Link to transaction object") - output_n = Column(Integer, primary_key=True, doc="Sequence number of transaction output") - key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this transaction output") - key = relationship("DbKey", back_populates="transaction_outputs", doc="List of DbKey object used in this output") - address = Column(String(255), - doc="Address string of output, used if no key is associated. " - "An cryptocurrency address is a hash of the public key or a redeemscript") - script = Column(LargeBinary, doc="Locking script which locks transaction output") - script_type = Column(String(20), default='p2pkh', - doc="Locking script type. Can be one of these values: 'p2pkh', 'multisig', 'p2sh', 'p2pk', " - "'nulldata', 'unknown', 'p2wpkh' or 'p2wsh'. Default is p2pkh") - value = Column(BigInteger, default=0, doc="Total transaction output value") - spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction") - spending_txid = Column(LargeBinary(32), doc="Transaction hash of input which spends this output") - spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output") - - __table_args__ = (CheckConstraint(script_type.in_(['', 'p2pkh', 'multisig', 'p2sh', 'p2pk', 'nulldata', - 'unknown', 'p2wpkh', 'p2wsh']), - name='transactionoutput_constraint_script_types_allowed'), - UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique')) - - -def db_update_version_id(db, version): - _logger.info("Updated BitcoinLib database to version %s" % version) - db.session.query(DbConfig).filter(DbConfig.variable == 'version').update( - {DbConfig.value: version}) - db.session.commit() - return version - - -def db_update(db, version_db, code_version=BITCOINLIB_VERSION): - # Database changes from version 0.5+ - # - # Older databases cannnot be updated this way, use updatedb.py to copy keys and recreate database. - # - - version_db = db_update_version_id(db, code_version) - return version_db diff --git a/tests/test_db.py b/tests/test_db.py index c4b23d83..bfbe23b6 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -20,55 +20,82 @@ import unittest from sqlalchemy.exc import OperationalError -from tests.db_0_5 import Db as DbInitOld from bitcoinlib.db import * from bitcoinlib.db_cache import * from bitcoinlib.wallets import Wallet, WalletError from bitcoinlib.services.services import Service +try: + import mysql.connector + import psycopg + from psycopg import sql + import testing.postgresql +except ImportError as e: + print("Could not import all modules. Error: %s" % e) -DATABASEFILE_UNITTESTS = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest.sqlite') -DATABASEFILE_TMP = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.tmp.sqlite') -DATABASEFILE_CACHE_TMP = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib_cache.tmp.sqlite') +DATABASE_NAME = 'bitcoinlib_tmp' +DATABASE_CACHE_NAME = 'bitcoinlib_cache_tmp' +def database_init(dbname=DATABASE_NAME): + session.close_all_sessions() + if os.getenv('UNITTEST_DATABASE') == 'postgresql': + con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) + cur = con.cursor() + try: + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) + except Exception as e: + print("Error exception %s" % str(e)) + pass + cur.close() + con.close() + return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname + elif os.getenv('UNITTEST_DATABASE') == 'mysql': + con = mysql.connector.connect(user='user', host='localhost', password='password') + cur = con.cursor() + cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) + cur.execute("CREATE DATABASE {}".format(dbname)) + con.commit() + cur.close() + con.close() + return 'mysql://user:password@localhost:3306/' + dbname + else: + dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) + if os.path.isfile(dburi): + try: + os.remove(dburi) + except PermissionError: + db_obj = Db(dburi) + db_obj.drop_db(True) + db_obj.session.close() + db_obj.engine.dispose() + return dburi class TestDb(unittest.TestCase): @classmethod def setUpClass(cls): - if os.path.isfile(DATABASEFILE_TMP): - os.remove(DATABASEFILE_TMP) - if os.path.isfile(DATABASEFILE_CACHE_TMP): - os.remove(DATABASEFILE_CACHE_TMP) - - def test_database_upgrade(self): - if os.path.isfile(DATABASEFILE_UNITTESTS): - os.remove(DATABASEFILE_UNITTESTS) - dbold = DbInitOld(DATABASEFILE_UNITTESTS) - - # self.assertFalse('latest_txid' in dbold.engine.execute("SELECT * FROM keys").keys()) - # self.assertFalse('address' in dbold.engine.execute("SELECT * FROM transaction_inputs").keys()) - # version_db = dbold.session.query(DbConfig.value).filter_by(variable='version').scalar() - # self.assertEqual(version_db, '0.4.10') + cls.database_uri = database_init(DATABASE_NAME) + cls.database_cache_uri = database_init(DATABASE_CACHE_NAME) def test_database_create_drop(self): - dbtmp = Db(DATABASEFILE_TMP) - Wallet.create("tmpwallet", db_uri=DATABASEFILE_TMP) + dbtmp = Db(self.database_uri) + Wallet.create("tmpwallet", db_uri=self.database_uri) self.assertRaisesRegex(WalletError, "Wallet with name 'tmpwallet' already exists", - Wallet.create, 'tmpwallet', db_uri=DATABASEFILE_TMP) + Wallet.create, 'tmpwallet', db_uri=self.database_uri) dbtmp.drop_db(yes_i_am_sure=True) - Wallet.create("tmpwallet", db_uri=DATABASEFILE_TMP) + Wallet.create("tmpwallet", db_uri=self.database_uri) def test_database_cache_create_drop(self): - dbtmp = DbCache(DATABASEFILE_CACHE_TMP) - srv = Service(cache_uri=DATABASEFILE_CACHE_TMP, exclude_providers=['bitaps', 'bitgo']) + dbtmp = DbCache(self.database_cache_uri) + srv = Service(cache_uri=self.database_cache_uri, exclude_providers=['bitaps', 'bitgo']) t = srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') if t: self.assertGreaterEqual(srv.results_cache_n, 0) srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') self.assertGreaterEqual(srv.results_cache_n, 1) dbtmp.drop_db() - self.assertRaisesRegex(OperationalError, "", srv.gettransaction, + self.assertRaisesRegex(Exception, "", srv.gettransaction, '68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') diff --git a/tests/test_tools.py b/tests/test_tools.py index f3379a81..aec3443a 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -37,7 +37,7 @@ def database_init(dbname=DATABASE_NAME): ) cur.close() con.close() - return 'postgresql://postgres:postgres@localhost:5432/' + dbname + return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c2f86dde..0e3d96c6 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -51,14 +51,14 @@ def database_init(dbname=DATABASE_NAME): con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) cur = con.cursor() try: - # cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) + cur.execute(sql.SQL("DROP DATABASE IF EXISTS {}").format(sql.Identifier(dbname))) cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(dbname))) except Exception as e: print("Error exception %s" % str(e)) pass cur.close() con.close() - return 'postgresql://postgres:postgres@localhost:5432/' + dbname + return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='user', host='localhost', password='password') cur = con.cursor() From b75274949705e7a5494aeb5b10a76c342d64bc0c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 11 Feb 2024 19:32:49 +0100 Subject: [PATCH 089/207] Add unittest for database field sizes --- bitcoinlib_cache_unittest1 | Bin 0 -> 57344 bytes bitcoinlib_cache_unittest2 | Bin 0 -> 57344 bytes tests/test_db.py | 15 +++++++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 bitcoinlib_cache_unittest1 create mode 100644 bitcoinlib_cache_unittest2 diff --git a/bitcoinlib_cache_unittest1 b/bitcoinlib_cache_unittest1 new file mode 100644 index 0000000000000000000000000000000000000000..18649ca7d97ba74f236cb3a50df89878fe030aec GIT binary patch literal 57344 zcmeIvF-ikL6b9hgBu3qJvL0a@TN}Z~LTj-R3u}uA79vP4V;4M7F5o?6vGPR?Sf$ty zG349fX9kw{cHXx)*H;gdadZFl{CGEMPSRm2igeL5DW%b`|l6Ki1XzfA@{a&pO|*chtF5Aprse2oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{j|C3mdpR=St4JBykL*Qu hBg4o}WDw~`dJzu+0t5&UAV7cs0RjXF5FoH20-q7p8H)e_ literal 0 HcmV?d00001 diff --git a/bitcoinlib_cache_unittest2 b/bitcoinlib_cache_unittest2 new file mode 100644 index 0000000000000000000000000000000000000000..18649ca7d97ba74f236cb3a50df89878fe030aec GIT binary patch literal 57344 zcmeIvF-ikL6b9hgBu3qJvL0a@TN}Z~LTj-R3u}uA79vP4V;4M7F5o?6vGPR?Sf$ty zG349fX9kw{cHXx)*H;gdadZFl{CGEMPSRm2igeL5DW%b`|l6Ki1XzfA@{a&pO|*chtF5Aprse2oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{j|C3mdpR=St4JBykL*Qu hBg4o}WDw~`dJzu+0t5&UAV7cs0RjXF5FoH20-q7p8H)e_ literal 0 HcmV?d00001 diff --git a/tests/test_db.py b/tests/test_db.py index bfbe23b6..7cca65d4 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -19,10 +19,10 @@ # import unittest -from sqlalchemy.exc import OperationalError from bitcoinlib.db import * from bitcoinlib.db_cache import * -from bitcoinlib.wallets import Wallet, WalletError +from bitcoinlib.wallets import Wallet, WalletError, WalletTransaction +from bitcoinlib.transactions import Input, Output from bitcoinlib.services.services import Service try: import mysql.connector @@ -98,6 +98,17 @@ def test_database_cache_create_drop(self): self.assertRaisesRegex(Exception, "", srv.gettransaction, '68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') + def test_database_transaction_integers(self): + db = Db(self.database_uri) + w = Wallet.create('StrangeTransactions', account_id=0x7fffffff, db_uri=db.db_uri) + inp = Input('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13', 0x7fffffff, + value=0xffffffff, index_n=0x7fffffff, sequence=0xffffffff) + outp = Output(0xffffffff, '37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z', output_n=0xffffffff) + wt = WalletTransaction(w, 0x7fffffff, locktime=0xffffffff, fee=0xffffffff, confirmations=0x7fffffff, + input_total= 2100000000001000, block_height=0x7fffffff, version=0x7fffffff, + output_total=2100000000000000, size=0x07fffffff, inputs=[inp], outputs=[outp]) + self.assertTrue(wt.store()) + if __name__ == '__main__': unittest.main() From 59746a027b862453c46124aa693820b321adc773 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 11 Feb 2024 21:04:23 +0100 Subject: [PATCH 090/207] Fix created database in wrong directory during unittests --- bitcoinlib_cache_unittest1 | Bin 57344 -> 0 bytes bitcoinlib_cache_unittest2 | Bin 57344 -> 0 bytes tests/test_db.py | 1 - tests/test_services.py | 27 ++++++++++----------------- tests/test_wallets.py | 1 - 5 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 bitcoinlib_cache_unittest1 delete mode 100644 bitcoinlib_cache_unittest2 diff --git a/bitcoinlib_cache_unittest1 b/bitcoinlib_cache_unittest1 deleted file mode 100644 index 18649ca7d97ba74f236cb3a50df89878fe030aec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeIvF-ikL6b9hgBu3qJvL0a@TN}Z~LTj-R3u}uA79vP4V;4M7F5o?6vGPR?Sf$ty zG349fX9kw{cHXx)*H;gdadZFl{CGEMPSRm2igeL5DW%b`|l6Ki1XzfA@{a&pO|*chtF5Aprse2oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{j|C3mdpR=St4JBykL*Qu hBg4o}WDw~`dJzu+0t5&UAV7cs0RjXF5FoH20-q7p8H)e_ diff --git a/bitcoinlib_cache_unittest2 b/bitcoinlib_cache_unittest2 deleted file mode 100644 index 18649ca7d97ba74f236cb3a50df89878fe030aec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeIvF-ikL6b9hgBu3qJvL0a@TN}Z~LTj-R3u}uA79vP4V;4M7F5o?6vGPR?Sf$ty zG349fX9kw{cHXx)*H;gdadZFl{CGEMPSRm2igeL5DW%b`|l6Ki1XzfA@{a&pO|*chtF5Aprse2oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkL{j|C3mdpR=St4JBykL*Qu hBg4o}WDw~`dJzu+0t5&UAV7cs0RjXF5FoH20-q7p8H)e_ diff --git a/tests/test_db.py b/tests/test_db.py index 7cca65d4..d7be72ff 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -28,7 +28,6 @@ import mysql.connector import psycopg from psycopg import sql - import testing.postgresql except ImportError as e: print("Could not import all modules. Error: %s" % e) diff --git a/tests/test_services.py b/tests/test_services.py index 5187bc91..cf19c583 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -809,13 +809,6 @@ class TestServiceCache(unittest.TestCase): @classmethod def setUpClass(cls): - session.close_all_sessions() - try: - DbCache(CACHE_DBNAME1).drop_db() - DbCache(CACHE_DBNAME2).drop_db() - except: - pass - if os.getenv('UNITTEST_DATABASE') == 'postgresql': try: con = psycopg.connect(user='postgres', host='localhost', password='postgres', autocommit=True) @@ -831,16 +824,16 @@ def setUpClass(cls): con.close() except Exception: pass - elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') - cur = con.cursor() - cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME1)) - cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME2)) - cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME1)) - cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME2)) - con.commit() - cur.close() - con.close() + # elif os.getenv('UNITTEST_DATABASE') == 'mysql': + # con = mysql.connector.connect(user='user', host='localhost', password='password') + # cur = con.cursor() + # cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME1)) + # cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME2)) + # cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME1)) + # cur.execute("CREATE DATABASE {}".format(CACHE_DBNAME2)) + # con.commit() + # cur.close() + # con.close() else: if os.path.isfile(DATABASE_CACHE_UNITTESTS): try: diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 0e3d96c6..71f7f124 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -25,7 +25,6 @@ import mysql.connector import psycopg from psycopg import sql - import testing.postgresql except ImportError as e: print("Could not import all modules. Error: %s" % e) # from psycopg2cffi import compat # Use for PyPy support From 1f8fec5c7888db3721490f46a27bf54dc8ca84da Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 10:23:38 +0100 Subject: [PATCH 091/207] Add unittests for mysql database --- .github/workflows/unittests-mysql.yaml | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/unittests-mysql.yaml diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml new file mode 100644 index 00000000..b90e728c --- /dev/null +++ b/.github/workflows/unittests-mysql.yaml @@ -0,0 +1,30 @@ +name: Bitcoinlib Tests Ubuntu - MySQL +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: 'x64' + - name: Set up MySQL + env: + DB_DATABASE: bitcoinlib_test + DB_USER: user + DB_PASSWORD: password + run: | + sudo /etc/init.d/mysql start + mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} + - name: Install dependencies + run: | + python -m pip install .[dev] + - name: Test with coverage + env: + BCL_CONFIG_FILE: config.ini.unittest + UNITTEST_DATABASE: postgresql + run: coverage run --source=bitcoinlib -m unittest -v From b404b7f62caf4d2e14067893ae8550d0388a331b Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 10:29:39 +0100 Subject: [PATCH 092/207] Change mysql password --- .github/workflows/unittests-mysql.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index b90e728c..376d7dc4 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -15,8 +15,8 @@ jobs: - name: Set up MySQL env: DB_DATABASE: bitcoinlib_test - DB_USER: user - DB_PASSWORD: password + DB_USER: root + DB_PASSWORD: root run: | sudo /etc/init.d/mysql start mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} From 4d9de6c65c69942d79089718ac0cef1f3ad58ed1 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 10:32:55 +0100 Subject: [PATCH 093/207] Update mysql login in unittests --- tests/test_db.py | 2 +- tests/test_security.py | 2 +- tests/test_services.py | 4 ++-- tests/test_tools.py | 2 +- tests/test_wallets.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index d7be72ff..4b103479 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -57,7 +57,7 @@ def database_init(dbname=DATABASE_NAME): con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://root:root@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): diff --git a/tests/test_security.py b/tests/test_security.py index ea242ce3..3eab20d3 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -50,7 +50,7 @@ con.commit() cur.close() con.close() - DATABASEFILE_UNITTESTS_ENCRYPTED = 'mysql://user:password@localhost:3306/bitcoinlib_security' + DATABASEFILE_UNITTESTS_ENCRYPTED = 'mysql://root:root@localhost:3306/bitcoinlib_security' else: DATABASEFILE_UNITTESTS_ENCRYPTED = os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib.unittest_security.sqlite') if os.path.isfile(DATABASEFILE_UNITTESTS_ENCRYPTED): diff --git a/tests/test_services.py b/tests/test_services.py index cf19c583..9533f8e6 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -42,8 +42,8 @@ CACHE_DBNAME2 = 'bitcoinlib_cache_unittest2' # FIXME: Mariadb for cache database does not work due to problem with BLOB indexing # if os.getenv('UNITTEST_DATABASE') == 'mysql' or os.getenv('UNITTEST_DATABASE') == 'mariadb': -# DATABASE_CACHE_UNITTESTS = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME1 -# DATABASE_CACHE_UNITTESTS2 = 'mariadb://user:password@localhost:3306/%s' % CACHE_DBNAME2 +# DATABASE_CACHE_UNITTESTS = 'mariadb://root:root@localhost:3306/%s' % CACHE_DBNAME1 +# DATABASE_CACHE_UNITTESTS2 = 'mariadb://root:root@localhost:3306/%s' % CACHE_DBNAME2 if os.getenv('UNITTEST_DATABASE') == 'postgresql': DATABASE_CACHE_UNITTESTS = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME1 DATABASE_CACHE_UNITTESTS2 = 'postgresql+psycopg://postgres:postgres@localhost:5432/%s' % CACHE_DBNAME2 diff --git a/tests/test_tools.py b/tests/test_tools.py index aec3443a..964bb3ec 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -46,7 +46,7 @@ def database_init(dbname=DATABASE_NAME): con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://root:root@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 71f7f124..ce82d6b4 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -66,7 +66,7 @@ def database_init(dbname=DATABASE_NAME): con.commit() cur.close() con.close() - return 'mysql://user:password@localhost:3306/' + dbname + return 'mysql://root:root@localhost:3306/' + dbname else: dburi = os.path.join(str(BCL_DATABASE_DIR), '%s.sqlite' % dbname) if os.path.isfile(dburi): From d46f1b667f7579c6c427fb03cf5d01630d05d035 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 10:46:06 +0100 Subject: [PATCH 094/207] Fix: Use correct database when unittesting --- .github/workflows/unittests-mysql.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index 376d7dc4..204bdff9 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -26,5 +26,5 @@ jobs: - name: Test with coverage env: BCL_CONFIG_FILE: config.ini.unittest - UNITTEST_DATABASE: postgresql + UNITTEST_DATABASE: mysql run: coverage run --source=bitcoinlib -m unittest -v From 197331f6f0c6b700c522969bd45b6b943d024bca Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 10:53:35 +0100 Subject: [PATCH 095/207] Use correct password for mysql server --- tests/test_security.py | 2 +- tests/test_services.py | 2 +- tests/test_tools.py | 2 +- tests/test_wallets.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index 3eab20d3..279bca63 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -43,7 +43,7 @@ con.close() DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql://postgres:postgres@localhost:5432/bitcoinlib_security' elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + con = mysql.connector.connect(user='root', host='localhost', password='root') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format('bitcoinlib_security')) cur.execute("CREATE DATABASE {}".format('bitcoinlib_security')) diff --git a/tests/test_services.py b/tests/test_services.py index 9533f8e6..47f3b5a2 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -825,7 +825,7 @@ def setUpClass(cls): except Exception: pass # elif os.getenv('UNITTEST_DATABASE') == 'mysql': - # con = mysql.connector.connect(user='user', host='localhost', password='password') + # con = mysql.connector.connect(user='root', host='localhost', password='root') # cur = con.cursor() # cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME1)) # cur.execute("DROP DATABASE IF EXISTS {}".format(CACHE_DBNAME2)) diff --git a/tests/test_tools.py b/tests/test_tools.py index 964bb3ec..03be68b9 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -39,7 +39,7 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + con = mysql.connector.connect(user='root', host='localhost', password='root') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index ce82d6b4..3e8f55dd 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -59,7 +59,7 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + con = mysql.connector.connect(user='root', host='localhost', password='root') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) From 929856c43dbe29ee279d0aa003a7caa20e7e2f9e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 11:05:36 +0100 Subject: [PATCH 096/207] And update mysql pass for latest unittest --- tests/test_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_db.py b/tests/test_db.py index 4b103479..747d9bdc 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -50,7 +50,7 @@ def database_init(dbname=DATABASE_NAME): con.close() return 'postgresql+psycopg://postgres:postgres@localhost:5432/' + dbname elif os.getenv('UNITTEST_DATABASE') == 'mysql': - con = mysql.connector.connect(user='user', host='localhost', password='password') + con = mysql.connector.connect(user='root', host='localhost', password='root') cur = con.cursor() cur.execute("DROP DATABASE IF EXISTS {}".format(dbname)) cur.execute("CREATE DATABASE {}".format(dbname)) From 54a255357884a98249f6c668c858c30a33bc87cf Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 11:20:22 +0100 Subject: [PATCH 097/207] Skip mysql cache unittest --- tests/test_db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_db.py b/tests/test_db.py index 747d9bdc..fa6f6bbf 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -86,6 +86,8 @@ def test_database_create_drop(self): Wallet.create("tmpwallet", db_uri=self.database_uri) def test_database_cache_create_drop(self): + if os.getenv('UNITTEST_DATABASE') == 'mysql': + self.skipTest('MySQL does not allow indexing on LargeBinary fields, so caching is not possible') dbtmp = DbCache(self.database_cache_uri) srv = Service(cache_uri=self.database_cache_uri, exclude_providers=['bitaps', 'bitgo']) t = srv.gettransaction('68104dbd6819375e7bdf96562f89290b41598df7b002089ecdd3c8d999025b13') From ab039deefdcd02d78d5f99ce57e4ef9609809bc3 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 14:16:57 +0100 Subject: [PATCH 098/207] Update database and encryption documentation --- docs/_static/manuals.databases.rst | 28 ++++++++++------ docs/_static/manuals.sqlcipher.rst | 52 ++++++++++++++++++++++++++---- tests/test_security.py | 2 +- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/docs/_static/manuals.databases.rst b/docs/_static/manuals.databases.rst index cdb94cb6..06df201b 100644 --- a/docs/_static/manuals.databases.rst +++ b/docs/_static/manuals.databases.rst @@ -5,12 +5,12 @@ Bitcoinlib uses the SQLite database by default, because it easy to use and requi But you can also use other databases. At this moment Bitcoinlib is tested with MySQL and PostgreSQL. +The database URI can be passed to the Wallet or Service object, or you can set the database URI for wallets and / or cache in configuration file at ~/.bitcoinlib/config.ini -Using MySQL database --------------------- +Using MariaDB / MySQL database +------------------------------ -We assume you have a MySQL server at localhost. Unlike with the SQLite database MySQL databases are not created -automatically, so create one from the mysql command prompt: +We assume you have a MySQL server at localhost. Unlike with the SQLite database MySQL databases are not created automatically, so create one from the mysql command prompt: .. code-block:: mysql @@ -32,6 +32,7 @@ In your application you can create a database link. The database tables are crea w = wallet_create_or_open('wallet_mysql', db_uri=db_uri) w.info() +At the moment it is not possible to use MySQL database for `caching `_, because the BLOB transaction ID's are used as primary key. For caching you need to use a PostgreSQL or SQLite database. Using PostgreSQL database ------------------------- @@ -54,14 +55,23 @@ And assume you unwisely have chosen the password 'secret' you can use the databa .. code-block:: python - db_uri = 'postgresql://bitcoinlib:secret@localhost:5432/' + db_uri = 'postgresql+psycopg://bitcoinlib:secret@localhost:5432/' w = wallet_create_or_open('wallet_mysql', db_uri=db_uri) w.info() +Please note 'postgresql+psycopg' has to be used as scheme, because SQLalchemy uses the latest version 3 of psycopg, if not provided it will use psycopg2. -Encrypt database ----------------- +PostgreSQL can also be used for `caching `_ of service requests. The URI can be passed to the Service object or provided in the configuration file (~/.bitcoiinlib/config.ini) -If you are using wallets with private keys it is advised to use an encrypted database. +.. code-block:: python + + srv = Service(cache_uri='postgresql+psycopg://postgres:postgres@localhost:5432/) + res = srv.gettransactions('12spqcvLTFhL38oNJDDLfW1GpFGxLdaLCL') + + +Encrypt database or private keys +-------------------------------- + +If you are using wallets with private keys it is advised to use an encrypted database and / or to encrypt the private key fields. -Please read `Using Encrypted Databases `_ for more information. \ No newline at end of file +Please read `Encrypt Database or Private Keys `_ for more information. diff --git a/docs/_static/manuals.sqlcipher.rst b/docs/_static/manuals.sqlcipher.rst index b4ddaf2a..7ae5e6fd 100644 --- a/docs/_static/manuals.sqlcipher.rst +++ b/docs/_static/manuals.sqlcipher.rst @@ -1,5 +1,13 @@ -Using SQLCipher encrypted database -================================== +Encrypt Database or Private Keys +================================ + +If you database contains private keys it is a good idea to encrypt your data. This will not be done automatically. At the moment you have 2 options: + +- Encrypt the database with SQLCipher. The database is fully encrypted and you need to provide the password in the Database URI when opening the database. +- Use a normal database but all private key data will be stored AES encrypted in the database. A key to encrypt and decrypt need to be provided in the Environment. + +Encrypt database with SQLCipher +------------------------------- To protect your data such as the private keys you can use SQLCipher to encrypt the full database. SQLCipher is a SQLite extension which uses 256-bit AES encryption and also works together with SQLAlchemy. @@ -14,8 +22,7 @@ your system might require other packages. Please read https://www.zetetic.net/sq # Previous, but now unmaintained: $ pip install pysqlcipher3 -Create an Encrypted Database for your Wallet --------------------------------------------- +**Create an Encrypted Database for your Wallet** Now you can simply create and use an encrypted database by supplying a password as argument to the Wallet object: @@ -26,8 +33,7 @@ Now you can simply create and use an encrypted database by supplying a password wlt = wallet_create_or_open('bcltestwlt4', network='bitcoinlib_test', db_uri=db_uri, db_password=password) -Encrypt using Database URI --------------------------- +**Encrypt using Database URI** You can also use a SQLCipher database URI to create and query a encrypted database: @@ -44,3 +50,37 @@ If you look at the contents of the SQLite database you can see it is encrypted. $ cat ~/.bitcoinlib/database/bcl_encrypted.db + + +Encrypt private key fields with AES +----------------------------------- + +Enable database encryption in Bitcoinlib configuration settings at ~/.bitcoinlib/config.ini + +.. code-block:: text + + # Encrypt private key field in database using symmetrically EAS encryption. + # You need to set the password in the DB_FIELD_ENCRYPTION_KEY environment variable. + database_encryption_enabled=True + +Now generate a secure 32 bytes encryption key. You can use Bitcoinlib to do this: + +.. code-block:: python + + >>> from bitcoinlib.keys import Key + >>> Key().private_hex() + '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +This key needs to be stored in the environment when creating or accessing a wallet. No extra arguments have to be provided to the Wallet class, the data is encrypted an decrypted at database level. + +There are several ways to store the key in an Environment variable, on Linux you can do: + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_KEY='2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +And in Windows: + +.. code-block:: bash + + $ setx DB_FIELD_ENCRYPTION_KEY '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' diff --git a/tests/test_security.py b/tests/test_security.py index 279bca63..48e400c7 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -41,7 +41,7 @@ cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier('bitcoinlib_security'))) cur.close() con.close() - DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql://postgres:postgres@localhost:5432/bitcoinlib_security' + DATABASEFILE_UNITTESTS_ENCRYPTED = 'postgresql+psycopg://postgres:postgres@localhost:5432/bitcoinlib_security' elif os.getenv('UNITTEST_DATABASE') == 'mysql': con = mysql.connector.connect(user='root', host='localhost', password='root') cur = con.cursor() From cf76b274e03702f34c488f6cd51010070b39d475 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 14:32:06 +0100 Subject: [PATCH 099/207] Add encryption warnings in docs --- docs/_static/manuals.security.rst | 2 +- docs/_static/manuals.sqlcipher.rst | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/_static/manuals.security.rst b/docs/_static/manuals.security.rst index c7c9a8c5..9cde36d2 100644 --- a/docs/_static/manuals.security.rst +++ b/docs/_static/manuals.security.rst @@ -13,7 +13,7 @@ Ten tips for more privacy and security when using Bitcoin and Bitcoinlib: 4. Use a random number of change outputs and shuffle order of inputs and outputs. This way it is not visible which output is the change output. In the Wallet object you can set the number_of_change_outputs to zero to generate a random number of change outputs. -5. `Encrypt your database `_ with SQLCipher. +5. `Encrypt your database or private keys `_ with SQLCipher or AES. 6. Use password protected private keys. For instance use a password when `creating wallets `_. 7. Backup private keys and passwords! I have no proof but I assume more bitcoins are lost because of lost private keys then there are lost due to hacking... diff --git a/docs/_static/manuals.sqlcipher.rst b/docs/_static/manuals.sqlcipher.rst index 7ae5e6fd..12ecf770 100644 --- a/docs/_static/manuals.sqlcipher.rst +++ b/docs/_static/manuals.sqlcipher.rst @@ -84,3 +84,7 @@ And in Windows: .. code-block:: bash $ setx DB_FIELD_ENCRYPTION_KEY '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' + +Enviroment variables can also be stored in an .env key, in a virtual enviroment or in Python code itself. However anyone with access to the key can decrypt your private keys. + +Please make sure to remember and backup your encryption key, if you loose your key the private keys can not be recovered! \ No newline at end of file From 8ce0c589cf7dd313d76c905dc74e852be3da4a7f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 12 Feb 2024 21:17:12 +0100 Subject: [PATCH 100/207] Add tool to migrate wallets from old bitcoinlib database --- bitcoinlib/tools/import_database.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 bitcoinlib/tools/import_database.py diff --git a/bitcoinlib/tools/import_database.py b/bitcoinlib/tools/import_database.py new file mode 100644 index 00000000..e02d3904 --- /dev/null +++ b/bitcoinlib/tools/import_database.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# IMPORT DATABASE - Extract wallet keys and information from old Bitcoinlib database and move to actual database +# © 2024 Februari - 1200 Web Development +# +# TODO: Currently skips multisig wallets + + +import sqlalchemy as sa +from sqlalchemy.sql import text +from bitcoinlib.main import * +from bitcoinlib.wallets import Wallet, wallet_create_or_open + + +DATABASE_TO_IMPORT = 'sqlite:///' + os.path.join(str(BCL_DATABASE_DIR), 'bitcoinlib_test.sqlite') + +def import_database(): + print(DATABASE_TO_IMPORT) + engine = sa.create_engine(DATABASE_TO_IMPORT) + con = engine.connect() + + wallets = con.execute(text( + 'SELECT w.name, k.private, w.owner, w.network_name, k.account_id, k.address, w.witness_type FROM wallets AS w ' + 'INNER JOIN keys AS k ON w.main_key_id = k.id WHERE multisig=0')).fetchall() + + for wallet in wallets: + print("Import wallet %s" % wallet[0]) + w = wallet_create_or_open(wallet[0], wallet[1], wallet[2], wallet[3], wallet[4], witness_type=wallet[6]) + + +if __name__ == '__main__': + import_database() From 71296b2f55d5479cf5b72d083c54e3b1b4ca499f Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Tue, 13 Feb 2024 15:35:55 +0100 Subject: [PATCH 101/207] Store order of transactions in block in Transaction objects --- bitcoinlib/db.py | 1 + bitcoinlib/services/services.py | 3 ++- bitcoinlib/transactions.py | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 9e1b8cc7..821d0df0 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -413,6 +413,7 @@ class DbTransaction(Base): raw = Column(LargeBinary, doc="Raw transaction hexadecimal string. Transaction is included in raw format on the blockchain") verified = Column(Boolean, default=False, doc="Is transaction verified. Default is False") + order_n = Column(Integer, doc="Order of transaction in block") __table_args__ = ( UniqueConstraint('wallet_id', 'txid', name='constraint_wallet_transaction_hash_unique'), diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index db2c4c32..2219e29c 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -716,7 +716,8 @@ def commit(self): def _parse_db_transaction(db_tx): t = Transaction(locktime=db_tx.locktime, version=db_tx.version, network=db_tx.network_name, fee=db_tx.fee, txid=db_tx.txid.hex(), date=db_tx.date, confirmations=db_tx.confirmations, - block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value) + block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value, + order_n=db_tx.order_n) for n in db_tx.nodes: if n.is_input: if n.ref_txid == b'\00' * 32: diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 3ae0b055..a4b703e6 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1065,7 +1065,8 @@ def load(txid=None, filename=None): def __init__(self, inputs=None, outputs=None, locktime=0, version=None, network=DEFAULT_NETWORK, fee=None, fee_per_kb=None, size=None, txid='', txhash='', date=None, confirmations=None, block_height=None, block_hash=None, input_total=0, output_total=0, rawtx=b'', - status='new', coinbase=False, verified=False, witness_type='segwit', flag=None, replace_by_fee=False): + status='new', coinbase=False, verified=False, witness_type='segwit', flag=None, replace_by_fee=False, + order_n=None): """ Create a new transaction class with provided inputs and outputs. @@ -1118,6 +1119,8 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, :type witness_type: str :param flag: Transaction flag to indicate version, for example for SegWit :type flag: bytes, str + :param order_n: Order of transaction in block. Used when parsing blocks + :type order_n: int """ @@ -1178,6 +1181,7 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, self.witness_type = witness_type self.replace_by_fee = replace_by_fee self.change = 0 + self.order_n = order_n self.calc_weight_units() if self.witness_type not in ['legacy', 'segwit']: raise TransactionError("Please specify a valid witness type: legacy or segwit") From a3ae4158ed4726a5d7e6ca29867969e0edc920fd Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 13 Feb 2024 20:40:47 +0100 Subject: [PATCH 102/207] Add index, transaction order in block --- bitcoinlib/blocks.py | 4 +- bitcoinlib/db.py | 2 +- bitcoinlib/db_cache.py | 2 +- bitcoinlib/services/bcoin.py | 1 + bitcoinlib/services/services.py | 32 +++++----- bitcoinlib/transactions.py | 12 ++-- tests/benchmark.py | 104 -------------------------------- tests/test_blocks.py | 1 + tests/test_keys.py | 16 ++--- tests/test_services.py | 40 +++++++----- tests/test_transactions.py | 2 - 11 files changed, 61 insertions(+), 155 deletions(-) delete mode 100644 tests/benchmark.py diff --git a/bitcoinlib/blocks.py b/bitcoinlib/blocks.py index f051e61c..d0526d13 100644 --- a/bitcoinlib/blocks.py +++ b/bitcoinlib/blocks.py @@ -251,11 +251,13 @@ def parse_bytesio(cls, raw, block_hash=None, height=None, parse_transactions=Fal raw.seek(tx_start_pos) transactions = [] + index = 0 while parse_transactions and raw.tell() < txs_data_size: if limit != 0 and len(transactions) >= limit: break - t = Transaction.parse_bytesio(raw, strict=False) + t = Transaction.parse_bytesio(raw, strict=False, index=index) transactions.append(t) + index += 1 # TODO: verify transactions, need input value from previous txs # if verify and not t.verify(): # raise ValueError("Could not verify transaction %s in block %s" % (t.txid, block_hash)) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 821d0df0..c8965d0e 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -413,7 +413,7 @@ class DbTransaction(Base): raw = Column(LargeBinary, doc="Raw transaction hexadecimal string. Transaction is included in raw format on the blockchain") verified = Column(Boolean, default=False, doc="Is transaction verified. Default is False") - order_n = Column(Integer, doc="Order of transaction in block") + index = Column(Integer, doc="Index of transaction in block") __table_args__ = ( UniqueConstraint('wallet_id', 'txid', name='constraint_wallet_transaction_hash_unique'), diff --git a/bitcoinlib/db_cache.py b/bitcoinlib/db_cache.py index c386606a..811e6119 100644 --- a/bitcoinlib/db_cache.py +++ b/bitcoinlib/db_cache.py @@ -136,7 +136,7 @@ class DbCacheTransaction(Base): fee = Column(BigInteger, doc="Transaction fee") nodes = relationship("DbCacheTransactionNode", cascade="all,delete", doc="List of all inputs and outputs as DbCacheTransactionNode objects") - order_n = Column(Integer, doc="Order of transaction in block") + index = Column(Integer, doc="Index of transaction in block") witness_type = Column(Enum(WitnessTypeTransactions), default=WitnessTypeTransactions.legacy, doc="Transaction type enum: legacy or segwit") diff --git a/bitcoinlib/services/bcoin.py b/bitcoinlib/services/bcoin.py index bfe33ed8..5a06f77c 100644 --- a/bitcoinlib/services/bcoin.py +++ b/bitcoinlib/services/bcoin.py @@ -59,6 +59,7 @@ def _parse_transaction(self, tx): t.block_height = tx['height'] if tx['height'] > 0 else None t.block_hash = tx['block'] t.status = status + t.index = tx['index'] if not t.coinbase: for i in t.inputs: i.value = tx['inputs'][t.inputs.index(i)]['coin']['value'] diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 2219e29c..dd5a22cc 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -382,11 +382,11 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): if len(txs): last_txid = bytes.fromhex(txs[-1:][0].txid) if len(self.results): - order_n = 0 + index = 0 for t in txs: if t.confirmations != 0: - res = self.cache.store_transaction(t, order_n, commit=False) - order_n += 1 + res = self.cache.store_transaction(t, index, commit=False) + index += 1 # Failure to store transaction: stop caching transaction and store last tx block height - 1 if res is False: if t.block_height: @@ -556,11 +556,11 @@ def getblock(self, blockid, parse_transactions=True, page=1, limit=None): block.page = page if parse_transactions and self.min_providers <= 1: - order_n = (page-1)*limit + index = (page-1)*limit for tx in block.transactions: if isinstance(tx, Transaction): - self.cache.store_transaction(tx, order_n, commit=False) - order_n += 1 + self.cache.store_transaction(tx, index, commit=False) + index += 1 self.cache.commit() self.complete = True if len(block.transactions) == block.tx_count else False self.cache.store_block(block) @@ -717,7 +717,7 @@ def _parse_db_transaction(db_tx): t = Transaction(locktime=db_tx.locktime, version=db_tx.version, network=db_tx.network_name, fee=db_tx.fee, txid=db_tx.txid.hex(), date=db_tx.date, confirmations=db_tx.confirmations, block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value, - order_n=db_tx.order_n) + index=db_tx.index) for n in db_tx.nodes: if n.is_input: if n.ref_txid == b'\00' * 32: @@ -794,7 +794,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): filter(DbCacheTransactionNode.address == address, DbCacheTransaction.block_height >= after_tx.block_height, DbCacheTransaction.block_height <= db_addr.last_block).\ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n).all() + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all() db_txs2 = [] for d in db_txs: db_txs2.append(d) @@ -806,7 +806,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): else: db_txs = self.session.query(DbCacheTransaction).join(DbCacheTransactionNode). \ filter(DbCacheTransactionNode.address == address). \ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n).all() + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all() for db_tx in db_txs: t = self._parse_db_transaction(db_tx) if t: @@ -836,8 +836,8 @@ def getblocktransactions(self, height, page, limit): n_from = (page-1) * limit n_to = page * limit db_txs = self.session.query(DbCacheTransaction).\ - filter(DbCacheTransaction.block_height == height, DbCacheTransaction.order_n >= n_from, - DbCacheTransaction.order_n < n_to).all() + filter(DbCacheTransaction.block_height == height, DbCacheTransaction.index >= n_from, + DbCacheTransaction.index < n_to).all() txs = [] for db_tx in db_txs: t = self._parse_db_transaction(db_tx) @@ -881,7 +881,7 @@ def getutxos(self, address, after_txid=''): DbCacheTransactionNode.value, DbCacheTransaction.confirmations, DbCacheTransaction.block_height, DbCacheTransaction.fee, DbCacheTransaction.date, DbCacheTransaction.txid).join(DbCacheTransaction). \ - order_by(DbCacheTransaction.block_height, DbCacheTransaction.order_n). \ + order_by(DbCacheTransaction.block_height, DbCacheTransaction.index). \ filter(DbCacheTransactionNode.address == address, DbCacheTransactionNode.is_input == False, DbCacheTransaction.network_name == self.network.name).all() utxos = [] @@ -991,14 +991,14 @@ def store_blockcount(self, blockcount): self.session.merge(dbvar) self.commit() - def store_transaction(self, t, order_n=None, commit=True): + def store_transaction(self, t, index=None, commit=True): """ Store transaction in cache. Use order number to determine order in a block :param t: Transaction :type t: Transaction - :param order_n: Order in block - :type order_n: int + :param index: Order in block + :type index: int :param commit: Commit transaction to database. Default is True. Can be disabled if a larger number of transactions are added to cache, so you can commit outside this method. :type commit: bool @@ -1023,7 +1023,7 @@ def store_transaction(self, t, order_n=None, commit=True): return new_tx = DbCacheTransaction(txid=txid, date=t.date, confirmations=t.confirmations, block_height=t.block_height, network_name=t.network.name, - fee=t.fee, order_n=order_n, version=t.version_int, + fee=t.fee, index=index, version=t.version_int, locktime=t.locktime, witness_type=t.witness_type) self.session.add(new_tx) for i in t.inputs: diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index a4b703e6..02fdf274 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -890,7 +890,7 @@ def parse(cls, rawtx, strict=True, network=DEFAULT_NETWORK): return cls.parse_bytesio(rawtx, strict, network) @classmethod - def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): + def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None): """ Parse a raw transaction and create a Transaction object @@ -997,7 +997,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK): raw_bytes = rawtx.read(raw_len) return Transaction(inputs, outputs, locktime, version, network, size=raw_len, output_total=output_total, - coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=raw_bytes) + coinbase=coinbase, flag=flag, witness_type=witness_type, rawtx=raw_bytes, index=index) @classmethod def parse_hex(cls, rawtx, strict=True, network=DEFAULT_NETWORK): @@ -1066,7 +1066,7 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, network=DEFAULT_NETWORK, fee=None, fee_per_kb=None, size=None, txid='', txhash='', date=None, confirmations=None, block_height=None, block_hash=None, input_total=0, output_total=0, rawtx=b'', status='new', coinbase=False, verified=False, witness_type='segwit', flag=None, replace_by_fee=False, - order_n=None): + index=None): """ Create a new transaction class with provided inputs and outputs. @@ -1119,8 +1119,8 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, :type witness_type: str :param flag: Transaction flag to indicate version, for example for SegWit :type flag: bytes, str - :param order_n: Order of transaction in block. Used when parsing blocks - :type order_n: int + :param index: Index of transaction in block. Used when parsing blocks + :type index: int """ @@ -1181,7 +1181,7 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, self.witness_type = witness_type self.replace_by_fee = replace_by_fee self.change = 0 - self.order_n = order_n + self.index = index self.calc_weight_units() if self.witness_type not in ['legacy', 'segwit']: raise TransactionError("Please specify a valid witness type: legacy or segwit") diff --git a/tests/benchmark.py b/tests/benchmark.py deleted file mode 100644 index c59ca846..00000000 --- a/tests/benchmark.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# Benchmark - test library speed -# © 2020 October - 1200 Web Development -# - - -import time -import random -from bitcoinlib.keys import * -from bitcoinlib.wallets import * -from bitcoinlib.transactions import * -from bitcoinlib.mnemonic import * - -try: - wallet_method = Wallet -except NameError: - wallet_method = HDWallet - -try: - BITCOINLIB_VERSION -except: - BITCOINLIB_VERSION = '<0.4.10' - - -class Benchmark: - - def __init__(self): - wallet_delete_if_exists('wallet_multisig_huge', force=True) - - @staticmethod - def benchmark_bip38(): - # Encrypt and decrypt BIP38 key - k = Key() - bip38_key = k.encrypt(password='satoshi') - k2 = Key(bip38_key, password='satoshi') - assert(k.wif() == k2.wif()) - - @staticmethod - def benchmark_encoding(): - # Convert very large numbers to and from base58 / bech32 - pk = random.randint(0, 10 ** 10240) - large_b58 = change_base(pk, 10, 58, 6000) - large_b32 = change_base(pk, 10, 32, 7000) - assert(change_base(large_b58, 58, 10) == pk) - assert(change_base(large_b32, 32, 10) == pk) - - @staticmethod - def benchmark_mnemonic(): - # Generate Mnemonic passphrases - for i in range(100): - m = Mnemonic().generate(256) - Mnemonic().to_entropy(m) - - @staticmethod - def benchmark_transactions(): - # Deserialize transaction and verify - raw_hex = "02000000000101b7006080d9d1d2928f70be1140d4af199d6ba4f9a7b0096b6461d7d4d16a96470600000000fdffffff11205c0600000000001976a91416e7a7d921edff13eaf5831eefd6aaca5728d7fb88acad960700000000001600140dd69a4ce74f03342cd46748fc40a877c7ccef0e808b08000000000017a914bd27a59ba92179389515ecea6b87824a42e002ee873efb0b0000000000160014b4a3a8da611b66123c19408c289faa04c71818d178b21100000000001976a914496609abfa498b6edbbf83e93fd45c1934e05b9888ac34d01900000000001976a9144d1ce518b35e19f413963172bd2c84bd90f8f23488ace06e1f00000000001976a914440d99e9e2879c1b0f8e9a1d5a288a4b6cfcc15288acff762c000000000016001401429b4b17e97f8d4419b4594ffe9f54e85037e7241e4500000000001976a9146083df8eb862f759ea0f1c04d3f13a3dfa9aff5888acf09056000000000017a9144fcaf4edac9da6890c09a819d0d7b8f300edbe478740fa97000000000017a9147431dcb6061217b0c80c6fa0c0256c1221d74b4a87208e9c000000000017a914a3e1e764fefa92fc5befa179b2b80afd5a9c20bd87ecf09f000000000017a9142ca7dc95f76530521a1edfc439586866997a14828754900101000000001976a9142e6c1941e2f9c47b535d0cf5dc4be5038e02336588acc0996d01000000001976a91492268fb9d7b8a3c825a4efc486a0679dbf006fae88acd790ae0300000000160014fe350625e2887e9bc984a69a7a4f60439e7ee7152182c81300000000160014f60834ef165253c571b11ce9fa74e46692fc5ec10248304502210081cb31e1b53a36409743e7c785e00d5df7505ca2373a1e652fec91f00c15746b02203167d7cc1fa43e16d411c620b90d9516cddac31d9e44e452651f50c950dc94150121026e5628506ecd33242e5ceb5fdafe4d3066b5c0f159b3c05a621ef65f177ea28600000000" - for i in range(100): - t = Transaction.import_raw(raw_hex) - t.inputs[0].value = 485636658 - t.verify() - assert(t.verified is True) - - @staticmethod - def benchmark_wallets_multisig(): - # Create large multisig wallet - network = 'bitcoinlib_test' - n_keys = 8 - sigs_req = 5 - key_list = [HDKey(network=network) for _ in range(0, n_keys)] - pk_n = random.randint(0, n_keys - 1) - key_list_cosigners = [k.public_master(multisig=True) for k in key_list if k is not key_list[pk_n]] - key_list_wallet = [key_list[pk_n]] + key_list_cosigners - w = wallet_method.create('wallet_multisig_huge', keys=key_list_wallet, sigs_required=sigs_req, network=network) - w.get_keys(number_of_keys=2) - w.utxos_update() - to_address = HDKey(network=network).address() - t = w.sweep(to_address, offline=True) - key_pool = [i for i in range(0, n_keys - 1) if i != pk_n] - while len(t.inputs[0].signatures) < sigs_req: - co_id = random.choice(key_pool) - t.sign(key_list[co_id]) - key_pool.remove(co_id) - assert(t.verify() is True) - - def run(self): - start_time = time.time() - print("Running BitcoinLib benchmarks speed test for version %s" % BITCOINLIB_VERSION) - - benchmark_methods = [m for m in dir(self) if callable(getattr(self, m)) if m.startswith('benchmark_')] - for method in benchmark_methods: - m_start_time = time.time() - getattr(self, method)() - m_duration = time.time() - m_start_time - print("%s, %.5f seconds" % (method, m_duration)) - - duration = time.time() - start_time - print("Total running time: %.5f seconds" % duration) - - -if __name__ == '__main__': - Benchmark().run() diff --git a/tests/test_blocks.py b/tests/test_blocks.py index 92d5bad3..e107cf9c 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -150,6 +150,7 @@ def test_blocks_parse_block_and_transactions_2(self): self.assertEqual(b.tx_count, 81) self.assertEqual(b.transactions[0].txid, 'dfd63430f8d14f6545117d74b20da63efd4a75c7e28f723b3dead431b88469ee') self.assertEqual(b.transactions[4].txid, '717bc8b42f12baf771b6719c2e3b2742925fe3912917c716abef03e35fe49020') + self.assertEqual(b.transactions[4].index, 4) self.assertEqual(len(b.transactions), 5) b.parse_transactions(70) self.assertEqual(len(b.transactions), 75) diff --git a/tests/test_keys.py b/tests/test_keys.py index 529f7ad2..e5efb0a8 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -566,7 +566,7 @@ def test_encrypt_private_key(self): return for v in self.vectors["valid"]: k = Key(v['wif']) - print("Check %s + %s = %s " % (v['wif'], v['passphrase'], v['bip38'])) + # print("Check %s + %s = %s " % (v['wif'], v['passphrase'], v['bip38'])) self.assertEqual(str(v['bip38']), k.encrypt(str(v['passphrase']))) def test_decrypt_bip38_key(self): @@ -574,14 +574,14 @@ def test_decrypt_bip38_key(self): return for v in self.vectors["valid"]: k = Key(v['bip38'], password=str(v['passphrase'])) - print("Check %s - %s = %s " % (v['bip38'], v['passphrase'], v['wif'])) + # print("Check %s - %s = %s " % (v['bip38'], v['passphrase'], v['wif'])) self.assertEqual(str(v['wif']), k.wif()) def test_bip38_invalid_keys(self): if not USING_MODULE_SCRYPT: return for v in self.vectors["invalid"]["verify"]: - print("Checking invalid key %s" % v['base58']) + # print("Checking invalid key %s" % v['base58']) self.assertRaisesRegex(Exception, "", Key, str(v['base58'])) def test_bip38_other_networks(self): @@ -622,8 +622,8 @@ def test_hdkey_derive_from_public_and_private_index(self): for i in range(BULKTESTCOUNT): pub_with_pubparent = self.K.child_public(i).address() pub_with_privparent = self.k.child_private(i).address() - if pub_with_privparent != pub_with_pubparent: - print("Error index %4d: pub-child %s, priv-child %s" % (i, pub_with_privparent, pub_with_pubparent)) + # if pub_with_privparent != pub_with_pubparent: + # print("Error index %4d: pub-child %s, priv-child %s" % (i, pub_with_privparent, pub_with_pubparent)) self.assertEqual(pub_with_pubparent, pub_with_privparent) def test_hdkey_derive_from_public_and_private_random(self): @@ -635,9 +635,9 @@ def test_hdkey_derive_from_public_and_private_random(self): pubk = HDKey(k.wif_public()) pub_with_pubparent = pubk.child_public().address() pub_with_privparent = k.child_private().address() - if pub_with_privparent != pub_with_pubparent: - print("Error random key: %4d: pub-child %s, priv-child %s" % - (i, pub_with_privparent, pub_with_pubparent)) + # if pub_with_privparent != pub_with_pubparent: + # print("Error random key: %4d: pub-child %s, priv-child %s" % + # (i, pub_with_privparent, pub_with_pubparent)) self.assertEqual(pub_with_pubparent, pub_with_privparent) diff --git a/tests/test_services.py b/tests/test_services.py index 47f3b5a2..b6a23817 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -111,7 +111,7 @@ def test_service_sendrawtransaction(self): except ServiceError: pass for provider in srv.errors: - print("Provider %s" % provider) + # print("Provider %s" % provider) prov_error = str(srv.errors[provider]) if isinstance(srv.errors[provider], Exception) or 'response [429]' in prov_error \ or 'response [503]' in prov_error: @@ -128,7 +128,7 @@ def test_service_get_balance(self): if len(srv.results) < 2: self.fail("Only 1 or less service providers found, nothing to compare. Errors %s" % srv.errors) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) balance = srv.results[provider] if prev is not None and balance != prev: self.fail("Different address balance from service providers: %d != %d" % (balance, prev)) @@ -142,7 +142,7 @@ def test_service_get_balance_litecoin(self): if len(srv.results) < 2: self.skipTest("Only 1 or less service providers found, nothing to compare. Errors %s" % srv.errors) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) balance = srv.results[provider] if prev is not None and balance != prev: self.fail("Different address balance from service providers: %d != %d" % (balance, prev)) @@ -166,7 +166,7 @@ def test_service_get_utxos(self): srv = ServiceTest(min_providers=3) srv.getutxos('1Mxww5Q2AK3GxG4R2KyCEao6NJXyoYgyAx') for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) self.assertDictEqualExt(srv.results[provider][0], expected_dict, ['date', 'block_height']) def test_service_get_utxos_after_txid(self): @@ -175,7 +175,7 @@ def test_service_get_utxos_after_txid(self): srv.getutxos('1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1', after_txid='9293869acee7d90661ee224135576b45b4b0dbf2b61e4ce30669f1099fecac0c') for provider in srv.results: - print("Testing provider %s" % provider) + # print("Testing provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_get_utxos_litecoin(self): @@ -183,7 +183,7 @@ def test_service_get_utxos_litecoin(self): srv.getutxos('Lct7CEpiN7e72rUXmYucuhqnCy5F5Vc6Vg') txid = '832518d58e9678bcdb9fe0e417a138daeb880c3a2ee1fb1659f1179efc383c25' for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_get_utxos_litecoin_after_txid(self): @@ -192,7 +192,7 @@ def test_service_get_utxos_litecoin_after_txid(self): srv.getutxos('Lfx4mFjhRvqyRKxXKqn6jyb17D6NDmosEV', after_txid='b328a91dd15b8b82fef5b01738aaf1f486223d34ee54357e1430c22e46ddd04e') for provider in srv.results: - print("Comparing provider %s" % provider) + # print("Comparing provider %s" % provider) self.assertEqual(srv.results[provider][0]['txid'], txid) def test_service_estimatefee(self): @@ -206,7 +206,7 @@ def test_service_estimatefee(self): # Normalize with dust amount, to avoid errors on small differences dust = Network().dust_amount for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) if srv.results[provider] < average_fee and average_fee - srv.results[provider] > dust: srv.results[provider] += dust elif srv.results[provider] > average_fee and srv.results[provider] - average_fee > dust: @@ -264,7 +264,7 @@ def test_service_gettransactions(self): srv = ServiceTest(min_providers=3) srv.gettransactions(address) for provider in srv.results: - print("Testing: %s" % provider) + # print("Testing: %s" % provider) res = srv.results[provider] t = [r for r in res if r.txid == txid][0] @@ -419,7 +419,7 @@ def test_service_gettransaction(self): srv.gettransaction('2ae77540ec3ef7b5001de90194ed0ade7522239fe0fc57c12c772d67274e2700') for provider in srv.results: - print("Comparing provider %s" % provider) + # print("Comparing provider %s" % provider) self.assertTrue(srv.results[provider].verify()) self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, ['block_hash', 'block_height', 'spent', 'value']) @@ -444,7 +444,7 @@ def test_service_gettransactions_litecoin(self): srv = ServiceTest(min_providers=3, network='litecoin') srv.gettransactions(address) for provider in srv.results: - print("Provider %s" % provider) + # print("Provider %s" % provider) res = srv.results[provider] txs = [r for r in res if r.txid == txid] t = txs[0] @@ -571,7 +571,7 @@ def test_service_gettransaction_segwit_p2wpkh(self): srv.gettransaction('299dab85f10c37c6296d4fb10eaa323fb456a5e7ada9adf41389c447daa9c0e4') for provider in srv.results: - print("\nComparing provider %s" % provider) + # print("\nComparing provider %s" % provider) self.assertDictEqualExt(srv.results[provider].as_dict(), expected_dict, ['block_hash', 'block_height', 'spent', 'value', 'flag']) @@ -675,7 +675,7 @@ def test_service_getblock_id(self): def test_service_getblock_height(self): srv = ServiceTest(timeout=TIMEOUT_TEST, cache_uri='') b = srv.getblock(599999, parse_transactions=True, limit=3) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(b.height, 599999) self.assertEqual(to_hexstring(b.block_hash), '00000000000000000003ecd827f336c6971f6f77a0b9fba362398dd867975645') self.assertEqual(to_hexstring(b.merkle_root), 'ca13ce7f21619f73fb5a062696ec06a4427c6ad9e523e7bc1cf5287c137ddcea') @@ -701,7 +701,7 @@ def test_service_getblock_height(self): def test_service_getblock_parse_tx_paging(self): srv = ServiceTest(timeout=TIMEOUT_TEST, cache_uri='') b = srv.getblock(120000, parse_transactions=True, limit=25, page=2) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(to_hexstring(b.block_hash), '0000000000000e07595fca57b37fea8522e95e0f6891779cfd34d7e537524471') self.assertEqual(b.height, 120000) @@ -720,7 +720,7 @@ def test_service_getblock_parse_tx_paging_last_page(self): def test_service_getblock_litecoin(self): srv = ServiceTest(timeout=TIMEOUT_TEST, network='litecoin', cache_uri='') b = srv.getblock(1000000, parse_transactions=True, limit=2) - print("Test getblock using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock using provider %s" % list(srv.results.keys())[0]) self.assertEqual(b.height, 1000000) self.assertEqual(to_hexstring(b.block_hash), '8ceae698f0a2d338e39b213eb9c253a91a270ca6451a4d9bba7bf2c9e637dfda') self.assertEqual(to_hexstring(b.merkle_root), @@ -971,7 +971,7 @@ def check_block_128594(b): for cache_db in DATABASES_CACHE: srv = ServiceTest(cache_uri=cache_db, exclude_providers=['blockchair', 'bitcoind']) b = srv.getblock('0000000000001a7dcac3c01bf10c5d5fe53dc8cc4b9c94001662e9d7bd36f6cc', limit=1) - print("Test getblock with hash using provider %s" % list(srv.results.keys())[0]) + # print("Test getblock with hash using provider %s" % list(srv.results.keys())[0]) check_block_128594(b) self.assertEqual(srv.results_cache_n, 0) @@ -996,3 +996,11 @@ def test_service_cache_transaction_p2sh_p2wpkh_input(self): t2 = srv.gettransaction(txid) self.assertEqual(t2.size, 249) self.assertEqual(srv.results_cache_n, 1) + + def test_service_cache_transaction_index(self): + srv = ServiceTest(cache_uri=DATABASE_CACHE_UNITTESTS2) + srv.getblock(104444, parse_transactions=True) + t = srv.gettransaction('d7795eb181ef87a35298e8689cabf852e831824ded4c23b1a7f711df119a6599') + if not srv.results_cache_n: + self.skipTest('Transaction not indexed for selected provider') + self.assertEqual(t.index, 5) \ No newline at end of file diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 063e0f99..9aabe40c 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1723,7 +1723,6 @@ def test_transactions_segwit_p2sh_p2wpkh(self): '64f3b0f4dd2bb3aa1ce8566d220cc74dda9df97d8490cc81d89d735c92e59fb6') t.sign([pk1], 0) self.assertTrue(t.verify()) - print(t.raw_hex()) t2 = Transaction.parse(t.raw()) t2.inputs[0].value = int(10 * 100000000) self.assertEqual(t2.signature_hash(0).hex(), @@ -1768,7 +1767,6 @@ def test_transaction_segwit_p2sh_p2wsh(self): t = Transaction(inputs, outputs, witness_type='segwit') t.sign(key2) self.assertTrue(t.verify()) - print(t.raw_hex()) self.assertEqual(t.signature_hash(0).hex(), '1926c08d8c0f54498382e97704e7b2d8b4181ffa524b4c0d8a43aba61e3fc656') self.assertEqual(t.txid, txid) From fa04fcb42df29097606144bff4f6e96e3b7ce805 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 13 Feb 2024 21:32:34 +0100 Subject: [PATCH 103/207] Add index when parsing block transactions as dict --- bitcoinlib/blocks.py | 7 +++++-- tests/test_blocks.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/blocks.py b/bitcoinlib/blocks.py index d0526d13..4384d646 100644 --- a/bitcoinlib/blocks.py +++ b/bitcoinlib/blocks.py @@ -300,11 +300,13 @@ def parse_transactions_dict(self): """ transactions_dict = [] txs_data_orig = deepcopy(self.txs_data) + index = 0 while self.txs_data and len(self.transactions) < self.tx_count: - tx = self.parse_transaction_dict() + tx = self.parse_transaction_dict(index) if not tx: break transactions_dict.append(tx) + index += 1 self.txs_data = txs_data_orig return transactions_dict @@ -321,7 +323,7 @@ def parse_transaction(self): return t return False - def parse_transaction_dict(self): + def parse_transaction_dict(self, index=None): """ Parse a single transaction from Block, if transaction data is available in txs_data attribute. Add Transaction object in Block and return the transaction @@ -407,6 +409,7 @@ def parse_transaction_dict(self): tx['txid'] = double_sha256(tx['version'][::-1] + raw_n_inputs + inputs_raw + raw_n_outputs + outputs_raw + tx_locktime)[::-1] tx['size'] = len(tx['rawtx']) + tx['index'] = index # TODO: tx['vsize'] = len(tx['rawtx']) return tx return False diff --git a/tests/test_blocks.py b/tests/test_blocks.py index e107cf9c..e4d0ecb3 100644 --- a/tests/test_blocks.py +++ b/tests/test_blocks.py @@ -208,5 +208,6 @@ def test_block_parse_transaction_dict(self): self.assertEqual(2668, len(b.transactions)) i = 0 for tx in tx_dict: + self.assertEqual(tx['index'], i) assert(tx['txid'].hex() == b.transactions[i].txid) i += 1 From bc0abb6c8427498a32cc6cf3b03cbbaeaa4a3fb4 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 13 Feb 2024 22:37:26 +0100 Subject: [PATCH 104/207] Move script definitions to config file --- bitcoinlib/config/config.py | 49 ++++++++++++++++++++----------------- bitcoinlib/scripts.py | 29 ---------------------- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index f55ddff1..e66bd195 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -22,7 +22,7 @@ import locale import platform import configparser -import enum +from .opcodes import * from pathlib import Path from datetime import datetime @@ -54,27 +54,32 @@ SERVICE_MAX_ERRORS = 4 # Fail service request when more then max errors occur for providers # Transactions -SCRIPT_TYPES_LOCKING = { - # Locking scripts / scriptPubKey (Output) - 'p2pkh': ['OP_DUP', 'OP_HASH160', 'hash-20', 'OP_EQUALVERIFY', 'OP_CHECKSIG'], - 'p2sh': ['OP_HASH160', 'hash-20', 'OP_EQUAL'], - 'p2wpkh': ['OP_0', 'hash-20'], - 'p2wsh': ['OP_0', 'hash-32'], - 'p2tr': ['op_n', 'hash-32'], - 'multisig': ['op_m', 'multisig', 'op_n', 'OP_CHECKMULTISIG'], - 'p2pk': ['public_key', 'OP_CHECKSIG'], - 'nulldata': ['OP_RETURN', 'return_data'], -} - -SCRIPT_TYPES_UNLOCKING = { - # Unlocking scripts / scriptSig (Input) - 'sig_pubkey': ['signature', 'SIGHASH_ALL', 'public_key'], - 'p2sh_multisig': ['OP_0', 'multisig', 'redeemscript'], - 'p2sh_p2wpkh': ['OP_0', 'OP_HASH160', 'redeemscript', 'OP_EQUAL'], - 'p2sh_p2wsh': ['OP_0', 'push_size', 'redeemscript'], - 'locktime_cltv': ['locktime_cltv', 'OP_CHECKLOCKTIMEVERIFY', 'OP_DROP'], - 'locktime_csv': ['locktime_csv', 'OP_CHECKSEQUENCEVERIFY', 'OP_DROP'], - 'signature': ['signature'] +SCRIPT_TYPES = { + # : (, , ) + 'p2pkh': ('locking', [op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), + 'p2pkh_drop': ('locking', ['data', op.op_drop, op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], + [32, 20]), + 'p2sh': ('locking', [op.op_hash160, 'data', op.op_equal], [20]), + 'p2wpkh': ('locking', [op.op_0, 'data'], [20]), + 'p2wsh': ('locking', [op.op_0, 'data'], [32]), + 'p2tr': ('locking', ['op_n', 'data'], [32]), + 'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), + 'p2pk': ('locking', ['key', op.op_checksig], []), + 'nulldata': ('locking', [op.op_return, 'data'], [0]), + 'nulldata_1': ('locking', [op.op_return, op.op_0], []), + 'nulldata_2': ('locking', [op.op_return], []), + 'sig_pubkey': ('unlocking', ['signature', 'key'], []), + # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), + 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), + 'p2tr_unlock': ('unlocking', ['data'], [64]), + 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), + 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), + 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), + 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + 'signature': ('unlocking', ['signature'], []), + 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), + 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), + 'locktime_csv': ('unlocking', ['locktime_csv', op.op_checksequenceverify, op.op_drop], []), } SIGHASH_ALL = 1 diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 9b66764e..1682e391 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -28,35 +28,6 @@ _logger = logging.getLogger(__name__) -SCRIPT_TYPES = { - # : (, , ) - 'p2pkh': ('locking', [op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), - 'p2pkh_drop': ('locking', ['data', op.op_drop, op.op_dup, op.op_hash160, 'data', op.op_equalverify, op.op_checksig], - [32, 20]), - 'p2sh': ('locking', [op.op_hash160, 'data', op.op_equal], [20]), - 'p2wpkh': ('locking', [op.op_0, 'data'], [20]), - 'p2wsh': ('locking', [op.op_0, 'data'], [32]), - 'p2tr': ('locking', ['op_n', 'data'], [32]), - 'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), - 'p2pk': ('locking', ['key', op.op_checksig], []), - 'nulldata': ('locking', [op.op_return, 'data'], [0]), - 'nulldata_1': ('locking', [op.op_return, op.op_0], []), - 'nulldata_2': ('locking', [op.op_return], []), - 'sig_pubkey': ('unlocking', ['signature', 'key'], []), - # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), - 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), - 'p2tr_unlock': ('unlocking', ['data'], [64]), - 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), - 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), - 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), - 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), - 'signature': ('unlocking', ['signature'], []), - 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), - 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), - 'locktime_csv': ('unlocking', ['locktime_csv', op.op_checksequenceverify, op.op_drop], []), -} - - class ScriptError(Exception): """ Handle Key class Exceptions From 0c3f242cab6268633a54df9f3bdb3b7688170d70 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 13 Feb 2024 22:39:38 +0100 Subject: [PATCH 105/207] Fix import enum --- bitcoinlib/config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index e66bd195..77b256a1 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -22,6 +22,7 @@ import locale import platform import configparser +import enum from .opcodes import * from pathlib import Path from datetime import datetime From 0e70f48ec8eb85145d37c872d405a01bce895f00 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Thu, 15 Feb 2024 21:51:41 +0100 Subject: [PATCH 106/207] Add bech32m unittest for valid addresses --- tests/test_encoding.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 6a062914..fa143098 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -266,15 +266,19 @@ def test_der_encode_sig(self): ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"], ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"], - # Invalid checksum according to new bech32m definition - # ["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", - # "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"], - # ["BC1SW50QA3JX3S", "6002751e"], - # ["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"], - ["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", - "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"], + ['bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y', + '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'], + ['BC1SW50QGDZ25J', '6002751e'], + ['bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs', '5210751e76e8199196d454941c45d1b3a323'], + ['tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy', + '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433'], + ['tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', + '5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433'], + ['bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0', + '512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'] ] + INVALID_ADDRESS = [ "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", @@ -374,10 +378,7 @@ def test_invalid_checksum(self): def test_valid_address(self): """Test whether valid addresses decode to the correct output.""" for (address, hexscript) in VALID_ADDRESS: - try: - scriptpubkey = addr_bech32_to_pubkeyhash(address, include_witver=True) - except EncodingError: - scriptpubkey = addr_bech32_to_pubkeyhash(address, prefix='tb', include_witver=True) + scriptpubkey = addr_bech32_to_pubkeyhash(address, include_witver=True) self.assertEqual(scriptpubkey, bytes.fromhex(hexscript)) addr = pubkeyhash_to_addr_bech32(scriptpubkey, address[:2].lower()) self.assertEqual(address.lower(), addr) From db8a1bf20a868c33eb9278ce24ab1ef1e0b3bb7b Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 16 Feb 2024 00:33:50 +0100 Subject: [PATCH 107/207] Remove duplicate bech32m tests --- bitcoinlib/encoding.py | 2 +- tests/test_encoding.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index 8b78e9d1..b16419af 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -772,7 +772,7 @@ def convertbits(data, frombits, tobits, pad=True): if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None + raise EncodingError("Invalid padding bits") return ret diff --git a/tests/test_encoding.py b/tests/test_encoding.py index fa143098..5c769cfa 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -266,19 +266,10 @@ def test_der_encode_sig(self): ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"], ["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"], - ['bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y', - '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'], - ['BC1SW50QGDZ25J', '6002751e'], - ['bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs', '5210751e76e8199196d454941c45d1b3a323'], - ['tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy', - '0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433'], - ['tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', - '5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433'], - ['bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0', - '512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'] + ["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"], ] - INVALID_ADDRESS = [ "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", From 3c0b58eb808da090a16334b230a25fe9fd497e82 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Fri, 16 Feb 2024 14:45:11 +0100 Subject: [PATCH 108/207] Check unittest asserts --- tests/test_encoding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_encoding.py b/tests/test_encoding.py index fa143098..17b162a2 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -402,26 +402,26 @@ def test_quantity_class(self): # Source: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki def test_bech32m_valid(self): for addr, pubkeyhash in BECH32M_VALID: - assert(pubkeyhash == addr_bech32_to_pubkeyhash(addr, include_witver=True).hex()) + assert pubkeyhash == addr_bech32_to_pubkeyhash(addr, include_witver=True).hex() prefix = addr.split('1')[0].lower() witver = change_base(addr.split('1')[1][0], 'bech32', 10) checksum_xor = addr_bech32_checksum(addr) addrc = pubkeyhash_to_addr_bech32(pubkeyhash, prefix, witver, checksum_xor=checksum_xor) - assert(addr.lower() == addrc) + assert addr.lower() == addrc def test_bech32_invalid(self): for addr, err in BECH32M_INVALID: try: addr_bech32_to_pubkeyhash(addr) except (EncodingError, TypeError) as e: - assert (str(e) == err) + assert str(e) == err def test_bech32_invalid_pubkeyhash(self): for pubkeyhash, err in BECH32M_INVALID_PUBKEYHASH: try: pubkeyhash_to_addr_bech32(pubkeyhash) except (EncodingError, TypeError) as e: - assert (str(e) == err) + assert str(e) == err class TestEncodingConfig(unittest.TestCase): From dd843dc64bfd17d5d0ca3e1d6df0e5d9d2f49ec2 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sat, 17 Feb 2024 12:55:46 +0100 Subject: [PATCH 109/207] Remove a to_bytes method in keys --- bitcoinlib/keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 0815b8e9..476e1002 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -2234,7 +2234,7 @@ def verify(self, txid=None, public_key=None): str(secp256k1_Gy) ) else: - transaction_to_sign = to_bytes(self.txid) + transaction_to_sign = bytes.fromhex(self.txid) signature = self.bytes() if len(transaction_to_sign) != 32: transaction_to_sign = double_sha256(transaction_to_sign) From bb73ba9247d3b9502157536c526380ec616685d9 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sat, 17 Feb 2024 13:12:15 +0100 Subject: [PATCH 110/207] Fix unittest Invalid padding bits warning --- tests/test_encoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_encoding.py b/tests/test_encoding.py index c444e41f..4b24f170 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -319,8 +319,8 @@ def test_der_encode_sig(self): ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav', 'Invalid decoded data length, must be between 2 and 40'), ('BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P', 'Invalid decoded data length, must be 20 or 32 bytes'), - ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', "cannot convert 'NoneType' object to bytes"), - ('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', "cannot convert 'NoneType' object to bytes"), + ('bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf', "Invalid padding bits"), + ('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j', "Invalid padding bits"), ('bc1gmk9yu', 'Invalid checksum (Bech32 instead of Bech32m)'), ] From d76117b6758776129dd4130570ecc1fbaaec8ab5 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Sun, 18 Feb 2024 18:04:19 +0100 Subject: [PATCH 111/207] Move wallet db session to separate method --- bitcoinlib/wallets.py | 174 ++++++++++++++++++++++------------------- tests/test_security.py | 4 +- tests/test_wallets.py | 21 ++--- 3 files changed, 105 insertions(+), 94 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index b80fe327..ed9c6f58 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # BitcoinLib - Python Cryptocurrency Library # WALLETS - HD wallet Class for Key and Transaction management -# © 2016 - 2023 May - 1200 Web Development +# © 2016 - 2024 February - 1200 Web Development # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -311,7 +311,7 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 >>> w = wallet_create_or_open('hdwalletkey_test') >>> wif = 'xprv9s21ZrQH143K2mcs9jcK4EjALbu2z1N9qsMTUG1frmnXM3NNCSGR57yLhwTccfNCwdSQEDftgjCGm96P29wGGcbBsPqZH85iqpoHA7LrqVy' - >>> wk = WalletKey.from_key('import_key', w.wallet_id, w._session, wif) + >>> wk = WalletKey.from_key('import_key', w.wallet_id, w.session, wif) >>> wk.address '1MwVEhGq6gg1eeSrEdZom5bHyPqXtJSnPg' >>> wk # doctest:+ELLIPSIS @@ -439,9 +439,9 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 def _commit(self): try: - self._session.commit() + self.session.commit() except Exception: - self._session.rollback() + self.session.rollback() raise def __init__(self, key_id, session, hdkey_object=None): @@ -457,7 +457,7 @@ def __init__(self, key_id, session, hdkey_object=None): """ - self._session = session + self.session = session wk = session.query(DbKey).filter_by(id=key_id).first() if wk: self._dbkey = wk @@ -497,7 +497,7 @@ def __init__(self, key_id, session, hdkey_object=None): raise WalletError("Key with id %s not found" % key_id) def __del__(self): - self._session.close() + self.session.close() def __repr__(self): return "" % (self.key_id, self.name, self.wif, self.path) @@ -692,7 +692,7 @@ def from_txid(cls, hdwallet, txid): :return WalletClass: """ - sess = hdwallet._session + sess = hdwallet.session # If txid is unknown add it to database, else update db_tx_query = sess.query(DbTransaction). \ filter(DbTransaction.wallet_id == hdwallet.wallet_id, DbTransaction.txid == to_bytes(txid)) @@ -838,7 +838,7 @@ def send(self, offline=False): # Update db: Update spent UTXO's, add transaction to database for inp in self.inputs: txid = inp.prev_txid - utxos = self.hdwallet._session.query(DbTransactionOutput).join(DbTransaction).\ + utxos = self.hdwallet.session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.txid == txid, DbTransactionOutput.output_n == inp.output_n_int, DbTransactionOutput.spent.is_(False)).all() @@ -857,7 +857,7 @@ def store(self): :return int: Transaction index number """ - sess = self.hdwallet._session + sess = self.hdwallet.session # If txid is unknown add it to database, else update db_tx_query = sess.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.hdwallet.wallet_id, DbTransaction.txid == bytes.fromhex(self.txid)) @@ -1013,7 +1013,7 @@ def delete(self): :return int: Number of deleted transactions """ - session = self.hdwallet._session + session = self.hdwallet.session txid = bytes.fromhex(self.txid) tx_query = session.query(DbTransaction).filter_by(txid=txid) tx = tx_query.scalar() @@ -1113,9 +1113,9 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ def _commit(self): try: - self._session.commit() + self.session.commit() except Exception: - self._session.rollback() + self.session.rollback() raise @classmethod @@ -1331,10 +1331,10 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) hdpm.cosigner.append(w) wlt_cos_id += 1 - # hdpm._dbwallet = hdpm._session.query(DbWallet).filter(DbWallet.id == hdpm.wallet_id) + # hdpm._dbwallet = hdpm.session.query(DbWallet).filter(DbWallet.id == hdpm.wallet_id) # hdpm._dbwallet.update({DbWallet.cosigner_id: hdpm.cosigner_id}) # hdpm._dbwallet.update({DbWallet.key_path: hdpm.key_path}) - # hdpm._session.commit() + # hdpm.session.commit() return hdpm @@ -1357,18 +1357,20 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke :type main_key_object: HDKey """ + self._session = None if session: self._session = session - else: - dbinit = Db(db_uri=db_uri, password=db_password) - self._session = dbinit.session - self._engine = dbinit.engine + # else: + # dbinit = Db(db_uri=db_uri, password=db_password) + # self.session = dbinit.session + # self._engine = dbinit.engine + self._db_password = db_password self.db_uri = db_uri self.db_cache_uri = db_cache_uri if isinstance(wallet, int) or wallet.isdigit(): - db_wlt = self._session.query(DbWallet).filter_by(id=wallet).scalar() + db_wlt = self.session.query(DbWallet).filter_by(id=wallet).scalar() else: - db_wlt = self._session.query(DbWallet).filter_by(name=wallet).scalar() + db_wlt = self.session.query(DbWallet).filter_by(name=wallet).scalar() if db_wlt: self._dbwallet = db_wlt self.wallet_id = db_wlt.id @@ -1383,12 +1385,12 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.main_key = None self._default_account_id = db_wlt.default_account_id self.multisig_n_required = db_wlt.multisig_n_required - co_sign_wallets = self._session.query(DbWallet).\ + co_sign_wallets = self.session.query(DbWallet).\ filter(DbWallet.parent_id == self.wallet_id).order_by(DbWallet.name).all() self.cosigner = [Wallet(w.id, db_uri=db_uri, db_cache_uri=db_cache_uri) for w in co_sign_wallets] self.sort_keys = db_wlt.sort_keys if db_wlt.main_key_id: - self.main_key = WalletKey(self.main_key_id, session=self._session, hdkey_object=main_key_object) + self.main_key = WalletKey(self.main_key_id, session=self.session, hdkey_object=main_key_object) if self._default_account_id is None: self._default_account_id = 0 if self.main_key: @@ -1420,14 +1422,14 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke def __exit__(self, exception_type, exception_value, traceback): try: - self._session.close() + self.session.close() self._engine.dispose() except Exception: pass def __del__(self): try: - self._session.close() + self.session.close() self._engine.dispose() except Exception: pass @@ -1465,7 +1467,7 @@ def _get_account_defaults(self, network=None, account_id=None, key_id=None): network = self.network.name if account_id is None and network == self.network.name: account_id = self.default_account_id - qr = self._session.query(DbKey).\ + qr = self.session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=self.purpose, depth=self.depth_public_master, network_name=network) if account_id is not None: @@ -1488,7 +1490,7 @@ def default_account_id(self): @default_account_id.setter def default_account_id(self, value): self._default_account_id = value - self._dbwallet = self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id). \ + self._dbwallet = self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id). \ update({DbWallet.default_account_id: value}) self._commit() @@ -1514,7 +1516,7 @@ def owner(self, value): """ self._owner = value - self._dbwallet = self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self._dbwallet = self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.owner: value}) self._commit() @@ -1542,14 +1544,22 @@ def name(self, value): if wallet_exists(value, db_uri=self.db_uri): raise WalletError("Wallet with name '%s' already exists" % value) self._name = value - self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).update({DbWallet.name: value}) + self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).update({DbWallet.name: value}) self._commit() + @property + def session(self): + if not self._session: + dbinit = Db(db_uri=self.db_uri, password=self._db_password) + self._session = dbinit.session + self._engine = dbinit.engine + return self._session + def default_network_set(self, network): if not isinstance(network, Network): network = Network(network) self.network = network - self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.network_name: network.name}) self._commit() @@ -1592,11 +1602,11 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): # self.key_path = ks[0]['key_path'] self.key_path, _, _ = get_key_structure_data(self.witness_type, self.multisig) self.main_key = WalletKey.from_key( - key=hdkey, name=name, session=self._session, wallet_id=self.wallet_id, network=network, + key=hdkey, name=name, session=self.session, wallet_id=self.wallet_id, network=network, account_id=account_id, purpose=self.purpose, key_type='bip32', witness_type=self.witness_type) self.main_key_id = self.main_key.key_id self._key_objects.update({self.main_key_id: self.main_key}) - self._session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ + self.session.query(DbWallet).filter(DbWallet.id == self.wallet_id).\ update({DbWallet.main_key_id: self.main_key_id}) for key in self.keys(is_private=False): @@ -1662,7 +1672,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t if key_type == 'single': # Create path for unrelated import keys hdkey.depth = self.key_depth - last_import_key = self._session.query(DbKey).filter(DbKey.path.like("import_key_%")).\ + last_import_key = self.session.query(DbKey).filter(DbKey.path.like("import_key_%")).\ order_by(DbKey.path.desc()).first() if last_import_key: ik_path = "import_key_" + str(int(last_import_key.path[-5:]) + 1).zfill(5) @@ -1673,7 +1683,7 @@ def import_key(self, key, account_id=0, name='', network=None, purpose=84, key_t mk = WalletKey.from_key( key=hdkey, name=name, wallet_id=self.wallet_id, network=network, key_type=key_type, - account_id=account_id, purpose=purpose, session=self._session, path=ik_path, + account_id=account_id, purpose=purpose, session=self.session, path=ik_path, witness_type=self.witness_type) self._key_objects.update({mk.key_id: mk}) if mk.key_id == self.main_key.key_id: @@ -1701,7 +1711,7 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, if witness_type == 'p2sh-segwit': script_type = 'p2sh_p2wsh' address = Address(redeemscript, script_type=script_type, network=network, witness_type=witness_type) - already_found_key = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id, + already_found_key = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id, address=address.address).first() if already_found_key: return self.key(already_found_key.id) @@ -1710,16 +1720,16 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, if not name: name = "Multisig Key " + '/'.join(public_key_ids) - new_key_id = (self._session.query(func.max(DbKey.id)).scalar() or 0) + 1 + new_key_id = (self.session.query(func.max(DbKey.id)).scalar() or 0) + 1 multisig_key = DbKey(id=new_key_id, name=name[:80], wallet_id=self.wallet_id, purpose=self.purpose, account_id=account_id, depth=depth, change=change, address_index=address_index, parent_id=0, is_private=False, path=path, public=address.hash_bytes, wif='multisig-%s' % address, address=address.address, cosigner_id=cosigner_id, key_type='multisig', witness_type=witness_type, network_name=network) - self._session.add(multisig_key) + self.session.add(multisig_key) self._commit() for child_id in public_key_ids: - self._session.add(DbKeyMultisigChildren(key_order=public_key_ids.index(child_id), parent_id=multisig_key.id, + self.session.add(DbKeyMultisigChildren(key_order=public_key_ids.index(child_id), parent_id=multisig_key.id, child_id=int(child_id))) self._commit() return self.key(multisig_key.id) @@ -1797,7 +1807,7 @@ def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness self.cosigner[cosigner_id].key_path == ['m'])): req_path = [] else: - prevkey = self._session.query(DbKey).\ + prevkey = self.session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ order_by(DbKey.address_index.desc()).first() @@ -1891,7 +1901,7 @@ def scan(self, scan_gap_limit=5, account_id=None, change=None, rescan_used=False self.transactions_update_confirmations() # Check unconfirmed transactions - db_txs = self._session.query(DbTransaction). \ + db_txs = self.session.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network, DbTransaction.confirmations == 0).all() for db_tx in db_txs: @@ -1939,14 +1949,14 @@ def _get_key(self, account_id=None, witness_type=None, network=None, cosigner_id (cosigner_id, len(self.cosigner))) witness_type = witness_type if witness_type else self.witness_type - last_used_qr = self._session.query(DbKey.id).\ + last_used_qr = self.session.query(DbKey.id).\ filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, used=True, change=change, depth=self.key_depth, witness_type=witness_type).\ order_by(DbKey.id.desc()).first() last_used_key_id = 0 if last_used_qr: last_used_key_id = last_used_qr.id - dbkey = (self._session.query(DbKey.id). + dbkey = (self.session.query(DbKey.id). filter_by(wallet_id=self.wallet_id, account_id=account_id, network_name=network, cosigner_id=cosigner_id, used=False, change=change, depth=self.key_depth, witness_type=witness_type). filter(DbKey.id > last_used_key_id). @@ -2099,7 +2109,7 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) # Determine account_id and name if account_id is None: account_id = 0 - qr = self._session.query(DbKey). \ + qr = self.session.query(DbKey). \ filter_by(wallet_id=self.wallet_id, witness_type=witness_type, network_name=network). \ order_by(DbKey.account_id.desc()).first() if qr: @@ -2277,7 +2287,7 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos wpath = ["M"] + fullpath[self.main_key.depth + 1:] dbkey = None while wpath and not dbkey: - qr = self._session.query(DbKey).filter_by(path=normalize_path('/'.join(wpath)), wallet_id=self.wallet_id) + qr = self.session.query(DbKey).filter_by(path=normalize_path('/'.join(wpath)), wallet_id=self.wallet_id) if recreate: qr = qr.filter_by(is_private=True) dbkey = qr.first() @@ -2325,7 +2335,7 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos nkey = WalletKey.from_key(key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=purpose, path=newpath, parent_id=parent_id, encoding=encoding, witness_type=witness_type, - cosigner_id=cosigner_id, network=network, session=self._session) + cosigner_id=cosigner_id, network=network, session=self.session) self._key_objects.update({nkey.key_id: nkey}) parent_id = nkey.key_id if nkey: @@ -2333,7 +2343,7 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos if len(new_keys) < number_of_keys: topkey = self._key_objects[new_keys[0].parent_id] parent_key = topkey.key() - new_key_id = self._session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 + new_key_id = self.session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1]) + len(new_keys), int(fullpath[-1]) + number_of_keys)] @@ -2346,8 +2356,8 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, change=change, purpose=purpose, path=newpath, parent_id=parent_id, encoding=encoding, witness_type=witness_type, new_key_id=new_key_id, - cosigner_id=cosigner_id, network=network, session=self._session)) - self._session.commit() + cosigner_id=cosigner_id, network=network, session=self.session)) + self.session.commit() return new_keys @@ -2393,7 +2403,7 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, :return list of DbKey: List of Keys """ - qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id).order_by(DbKey.id) + qr = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id).order_by(DbKey.id) if network is not None: qr = qr.filter(DbKey.network_name == network) if witness_type is not None: @@ -2612,7 +2622,7 @@ def key(self, term): """ dbkey = None - qr = self._session.query(DbKey).filter_by(wallet_id=self.wallet_id) + qr = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id) if isinstance(term, numbers.Number): dbkey = qr.filter_by(id=term).scalar() if not dbkey: @@ -2625,7 +2635,7 @@ def key(self, term): if dbkey.id in self._key_objects.keys(): return self._key_objects[dbkey.id] else: - hdwltkey = WalletKey(key_id=dbkey.id, session=self._session) + hdwltkey = WalletKey(key_id=dbkey.id, session=self.session) self._key_objects.update({dbkey.id: hdwltkey}) return hdwltkey else: @@ -2648,7 +2658,7 @@ def account(self, account_id): if "account'" not in self.key_path: raise WalletError("Accounts are not supported for this wallet. Account not found in key path %s" % self.key_path) - qr = self._session.query(DbKey).\ + qr = self.session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=self.purpose, network_name=self.network.name, account_id=account_id, depth=3).scalar() if not qr: @@ -2690,7 +2700,7 @@ def witness_types(self, account_id=None, network=None): """ # network, account_id, _ = self._get_account_defaults(network, account_id) - qr = self._session.query(DbKey.witness_type).filter_by(wallet_id=self.wallet_id) + qr = self.session.query(DbKey.witness_type).filter_by(wallet_id=self.wallet_id) if network is not None: qr = qr.filter(DbKey.network_name == network) if account_id is not None: @@ -2711,7 +2721,7 @@ def networks(self, as_dict=False): nw_list = [self.network] if self.multisig and self.cosigner: - keys_qr = self._session.query(DbKey.network_name).\ + keys_qr = self.session.query(DbKey.network_name).\ filter_by(wallet_id=self.wallet_id, depth=self.key_depth).\ group_by(DbKey.network_name).all() nw_list += [Network(nw[0]) for nw in keys_qr] @@ -2824,7 +2834,7 @@ def _balance_update(self, account_id=None, network=None, key_id=None, min_confir :return: Updated balance """ - qr = self._session.query(DbTransactionOutput, func.sum(DbTransactionOutput.value), DbTransaction.network_name, + qr = self.session.query(DbTransactionOutput, func.sum(DbTransactionOutput.value), DbTransaction.network_name, DbTransaction.account_id).\ join(DbTransaction). \ filter(DbTransactionOutput.spent.is_(False), @@ -2896,7 +2906,7 @@ def _balance_update(self, account_id=None, network=None, key_id=None, min_confir for kb in key_balance_list: if kb['id'] in self._key_objects: self._key_objects[kb['id']]._balance = kb['balance'] - self._session.bulk_update_mappings(DbKey, key_balance_list) + self.session.bulk_update_mappings(DbKey, key_balance_list) self._commit() _logger.info("Got balance for %d key(s)" % len(key_balance_list)) return self._balances @@ -2948,7 +2958,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d single_key = None if key_id: - single_key = self._session.query(DbKey).filter_by(id=key_id).scalar() + single_key = self.session.query(DbKey).filter_by(id=key_id).scalar() networks = [single_key.network_name] account_id = single_key.account_id rescan_all = False @@ -2963,14 +2973,14 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d for network in networks: # Remove current UTXO's if rescan_all: - cur_utxos = self._session.query(DbTransactionOutput). \ + cur_utxos = self.session.query(DbTransactionOutput). \ join(DbTransaction). \ filter(DbTransactionOutput.spent.is_(False), DbTransaction.account_id == account_id, DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network).all() for u in cur_utxos: - self._session.query(DbTransactionOutput).filter_by( + self.session.query(DbTransactionOutput).filter_by( transaction_id=u.transaction_id, output_n=u.output_n).update({DbTransactionOutput.spent: True}) self._commit() @@ -3008,7 +3018,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d for utxo in utxos: key = single_key if not single_key: - key = self._session.query(DbKey).\ + key = self.session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, address=utxo['address']).scalar() if not key: raise WalletError("Key with address %s not found in this wallet" % utxo['address']) @@ -3018,14 +3028,14 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d status = 'confirmed' # Update confirmations in db if utxo was already imported - transaction_in_db = self._session.query(DbTransaction).\ + transaction_in_db = self.session.query(DbTransaction).\ filter_by(wallet_id=self.wallet_id, txid=bytes.fromhex(utxo['txid']), network_name=network) - utxo_in_db = self._session.query(DbTransactionOutput).join(DbTransaction).\ + utxo_in_db = self.session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.txid == bytes.fromhex(utxo['txid']), DbTransactionOutput.output_n == utxo['output_n']) - spent_in_db = self._session.query(DbTransactionInput).join(DbTransaction).\ + spent_in_db = self.session.query(DbTransactionInput).join(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransactionInput.prev_txid == bytes.fromhex(utxo['txid']), DbTransactionInput.output_n == utxo['output_n']) @@ -3049,7 +3059,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d wallet_id=self.wallet_id, txid=bytes.fromhex(utxo['txid']), status=status, is_complete=False, block_height=block_height, account_id=account_id, confirmations=utxo['confirmations'], network_name=network) - self._session.add(new_tx) + self.session.add(new_tx) # TODO: Get unique id before inserting to increase performance for large utxo-sets self._commit() tid = new_tx.id @@ -3063,7 +3073,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d script=bytes.fromhex(utxo['script']), script_type=script_type, spent=bool(spent_in_db.count())) - self._session.add(new_utxo) + self.session.add(new_utxo) count_utxos += 1 self._commit() @@ -3100,7 +3110,7 @@ def utxos(self, account_id=None, network=None, min_confirms=0, key_id=None): first_key_id = key_id[0] network, account_id, acckey = self._get_account_defaults(network, account_id, first_key_id) - qr = self._session.query(DbTransactionOutput, DbKey.address, DbTransaction.confirmations, DbTransaction.txid, + qr = self.session.query(DbTransactionOutput, DbKey.address, DbTransaction.confirmations, DbTransaction.txid, DbKey.network_name).\ join(DbTransaction).join(DbKey). \ filter(DbTransactionOutput.spent.is_(False), @@ -3172,7 +3182,7 @@ def utxo_last(self, address): :return str: """ - to = self._session.query( + to = self.session.query( DbTransaction.txid, DbTransaction.confirmations). \ join(DbTransactionOutput).join(DbKey). \ filter(DbKey.address == address, DbTransaction.wallet_id == self.wallet_id, @@ -3189,11 +3199,11 @@ def transactions_update_confirmations(self): network = self.network.name srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() - db_txs = self._session.query(DbTransaction). \ + db_txs = self.session.query(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network, DbTransaction.block_height > 0).all() for db_tx in db_txs: - self._session.query(DbTransaction).filter_by(id=db_tx.id). \ + self.session.query(DbTransaction).filter_by(id=db_tx.id). \ update({DbTransaction.status: 'confirmed', DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self._commit() @@ -3227,7 +3237,7 @@ def transactions_update_by_txids(self, txids): utxo_set.update(utxos) for utxo in list(utxo_set): - tos = self._session.query(DbTransactionOutput).join(DbTransaction). \ + tos = self.session.query(DbTransactionOutput).join(DbTransaction). \ filter(DbTransaction.txid == bytes.fromhex(utxo[0]), DbTransactionOutput.output_n == utxo[1], DbTransactionOutput.spent.is_(False)).all() for u in tos: @@ -3270,11 +3280,11 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() - db_txs = self._session.query(DbTransaction).\ + db_txs = self.session.query(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network, DbTransaction.block_height > 0).all() for db_tx in db_txs: - self._session.query(DbTransaction).filter_by(id=db_tx.id).\ + self.session.query(DbTransaction).filter_by(id=db_tx.id).\ update({DbTransaction.status: 'confirmed', DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self._commit() @@ -3290,7 +3300,7 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N if txs and txs[-1].date and txs[-1].date < last_updated: last_updated = txs[-1].date if txs and txs[-1].confirmations: - dbkey = self._session.query(DbKey).filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id) + dbkey = self.session.query(DbKey).filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id) if not dbkey.update({DbKey.latest_txid: bytes.fromhex(txs[-1].txid)}): raise WalletError("Failed to update latest transaction id for key with address %s" % address) self._commit() @@ -3305,7 +3315,7 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N utxos = [(ti.prev_txid.hex(), ti.output_n_int) for ti in wt.inputs] utxo_set.update(utxos) for utxo in list(utxo_set): - tos = self._session.query(DbTransactionOutput).join(DbTransaction).\ + tos = self.session.query(DbTransactionOutput).join(DbTransaction).\ filter(DbTransaction.txid == bytes.fromhex(utxo[0]), DbTransactionOutput.output_n == utxo[1], DbTransactionOutput.spent.is_(False), DbTransaction.wallet_id == self.wallet_id).all() for u in tos: @@ -3326,7 +3336,7 @@ def transaction_last(self, address): :return str: """ - txid = self._session.query(DbKey.latest_txid).\ + txid = self.session.query(DbKey.latest_txid).\ filter(DbKey.address == address, DbKey.wallet_id == self.wallet_id).scalar() return '' if not txid else txid.hex() @@ -3357,7 +3367,7 @@ def transactions(self, account_id=None, network=None, include_new=False, key_id= network, account_id, acckey = self._get_account_defaults(network, account_id, key_id) # Transaction inputs - qr = self._session.query(DbTransactionInput, DbTransactionInput.address, DbTransaction.confirmations, + qr = self.session.query(DbTransactionInput, DbTransactionInput.address, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ join(DbTransaction).join(DbKey). \ filter(DbTransaction.account_id == account_id, @@ -3370,7 +3380,7 @@ def transactions(self, account_id=None, network=None, include_new=False, key_id= qr = qr.filter(or_(DbTransaction.status == 'confirmed', DbTransaction.status == 'unconfirmed')) txs = qr.all() # Transaction outputs - qr = self._session.query(DbTransactionOutput, DbTransactionOutput.address, DbTransaction.confirmations, + qr = self.session.query(DbTransactionOutput, DbTransactionOutput.address, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ join(DbTransaction).join(DbKey). \ filter(DbTransaction.account_id == account_id, @@ -3430,7 +3440,7 @@ def transactions_full(self, network=None, include_new=False, limit=0, offset=0): :return list of WalletTransaction: """ network, _, _ = self._get_account_defaults(network) - qr = self._session.query(DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ + qr = self.session.query(DbTransaction.txid, DbTransaction.network_name, DbTransaction.status). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network) if not include_new: @@ -3510,7 +3520,7 @@ def transaction_spent(self, txid, output_n): txid = to_bytes(txid) if isinstance(output_n, bytes): output_n = int.from_bytes(output_n, 'big') - qr = self._session.query(DbTransactionInput, DbTransaction.confirmations, + qr = self.session.query(DbTransactionInput, DbTransaction.confirmations, DbTransaction.txid, DbTransaction.status). \ join(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, @@ -3519,7 +3529,7 @@ def transaction_spent(self, txid, output_n): return qr.transaction.txid.hex() def _objects_by_key_id(self, key_id): - key = self._session.query(DbKey).filter_by(id=key_id).scalar() + key = self.session.query(DbKey).filter_by(id=key_id).scalar() if not key: raise WalletError("Key '%s' not found in this wallet" % key_id) if key.key_type == 'multisig': @@ -3571,7 +3581,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non if variance is None: variance = dust_amount - utxo_query = self._session.query(DbTransactionOutput).join(DbTransaction).join(DbKey). \ + utxo_query = self.session.query(DbTransactionOutput).join(DbTransaction).join(DbKey). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.account_id == account_id, DbTransaction.network_name == network, DbKey.public != b'', DbTransactionOutput.spent.is_(False), DbTransaction.confirmations >= min_confirms) @@ -3796,7 +3806,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco if not (key_id and value and unlocking_script_type): if not isinstance(output_n, TYPE_INT): output_n = int.from_bytes(output_n, 'big') - inp_utxo = self._session.query(DbTransactionOutput).join(DbTransaction). \ + inp_utxo = self.session.query(DbTransactionOutput).join(DbTransaction). \ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.txid == to_bytes(prev_txid), DbTransactionOutput.output_n == output_n).first() @@ -3809,7 +3819,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco else: _logger.info("UTXO %s not found in this wallet. Please update UTXO's if this is not an " "offline wallet" % to_hexstring(prev_txid)) - key_id = self._session.query(DbKey.id).\ + key_id = self.session.query(DbKey.id).\ filter(DbKey.wallet_id == self.wallet_id, DbKey.address == address).scalar() if not key_id: raise WalletError("UTXO %s and key with address %s not found in this wallet" % ( diff --git a/tests/test_security.py b/tests/test_security.py index 48e400c7..4235971c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -81,8 +81,8 @@ def test_security_wallet_field_encryption(self): db_query = text("SELECT wif, private FROM `keys` WHERE id=%d" % wallet._dbwallet.main_key_id) else: db_query = text("SELECT wif, private FROM keys WHERE id=%d" % wallet._dbwallet.main_key_id) - encrypted_main_key_wif = wallet._session.execute(db_query).fetchone()[0] - encrypted_main_key_private = wallet._session.execute(db_query).fetchone()[1] + encrypted_main_key_wif = wallet.session.execute(db_query).fetchone()[0] + encrypted_main_key_private = wallet.session.execute(db_query).fetchone()[1] self.assertIn(type(encrypted_main_key_wif), (bytes, memoryview), "Encryption of database private key failed!") self.assertEqual(encrypted_main_key_wif.hex(), pk_wif_enc_hex) self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3e8f55dd..cf40abaf 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -44,6 +44,7 @@ print("DATABASE USED: %s" % os.getenv('UNITTEST_DATABASE')) + def database_init(dbname=DATABASE_NAME): session.close_all_sessions() if os.getenv('UNITTEST_DATABASE') == 'postgresql': @@ -639,24 +640,24 @@ def test_wallet_key_create_from_key(self): k1 = HDKey(network='testnet') k2 = HDKey(network='testnet') w1 = Wallet.create('network_mixup_test_wallet', network='litecoin', db_uri=self.database_uri) - wk1 = WalletKey.from_key('key1', w1.wallet_id, w1._session, key=k1.address_obj) + wk1 = WalletKey.from_key('key1', w1.wallet_id, w1.session, key=k1.address_obj) self.assertEqual(wk1.network.name, 'testnet') self.assertRaisesRegex(WalletError, "Specified network and key network should be the same", - WalletKey.from_key, 'key2', w1.wallet_id, w1._session, key=k2.address_obj, + WalletKey.from_key, 'key2', w1.wallet_id, w1.session, key=k2.address_obj, network='bitcoin') w2 = Wallet.create('network_mixup_test_wallet2', network='litecoin', db_uri=self.database_uri) - wk2 = WalletKey.from_key('key1', w2.wallet_id, w2._session, key=k1) + wk2 = WalletKey.from_key('key1', w2.wallet_id, w2.session, key=k1) self.assertEqual(wk2.network.name, 'testnet') self.assertRaisesRegex(WalletError, "Specified network and key network should be the same", - WalletKey.from_key, 'key2', w2.wallet_id, w2._session, key=k2, + WalletKey.from_key, 'key2', w2.wallet_id, w2.session, key=k2, network='bitcoin') - wk3 = WalletKey.from_key('key3', w2.wallet_id, w2._session, key=k1) + wk3 = WalletKey.from_key('key3', w2.wallet_id, w2.session, key=k1) self.assertEqual(wk3.name, 'key1') - wk4 = WalletKey.from_key('key4', w2.wallet_id, w2._session, key=k1.address_obj) + wk4 = WalletKey.from_key('key4', w2.wallet_id, w2.session, key=k1.address_obj) self.assertEqual(wk4.name, 'key1') k = HDKey().public_master() w = Wallet.create('pmtest', network='litecoin', db_uri=self.database_uri) - wk1 = WalletKey.from_key('key', w.wallet_id, w._session, key=k) + wk1 = WalletKey.from_key('key', w.wallet_id, w.session, key=k) self.assertEqual(wk1.path, 'M') # Test __repr__ method self.assertIn(" Date: Sun, 18 Feb 2024 21:48:11 +0100 Subject: [PATCH 112/207] Remove references to Dash network --- bitcoinlib/config/config.py | 2 +- bitcoinlib/data/providers.examples.json | 66 ------ bitcoinlib/data/providers.json | 55 ----- bitcoinlib/db.py | 4 +- bitcoinlib/keys.py | 2 +- bitcoinlib/networks.py | 2 +- bitcoinlib/services/__init__.py | 2 - bitcoinlib/services/cryptoid.py | 2 + bitcoinlib/services/dashd.py | 293 ------------------------ bitcoinlib/services/insightdash.py | 196 ---------------- bitcoinlib/services/services.py | 2 +- bitcoinlib/wallets.py | 9 +- tests/test_wallets.py | 2 +- 13 files changed, 12 insertions(+), 625 deletions(-) delete mode 100644 bitcoinlib/services/dashd.py delete mode 100644 bitcoinlib/services/insightdash.py diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 77b256a1..15d5569c 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -137,7 +137,7 @@ # Keys / Addresses SUPPORTED_ADDRESS_ENCODINGS = ['base58', 'bech32'] -ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'tdash', 'tdash', 'blt'] +ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'blt'] DEFAULT_WITNESS_TYPE = 'segwit' BECH32M_CONST = 0x2bc830a3 diff --git a/bitcoinlib/data/providers.examples.json b/bitcoinlib/data/providers.examples.json index 6a700910..283733ea 100644 --- a/bitcoinlib/data/providers.examples.json +++ b/bitcoinlib/data/providers.examples.json @@ -153,39 +153,6 @@ "denominator": 100000000, "network_overrides": {"prefix_address_p2sh": "32"} }, - "dashd": { - "provider": "dashd", - "network": "dash", - "client_class": "DashdClient", - "url": "", - "provider_coin_id": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "dashd.testnet": { - "provider": "dashd", - "network": "dash_testnet", - "client_class": "DashdClient", - "url": "", - "provider_coin_id": "", - "api_key": "", - "priority": 20, - "denominator": 100000000, - "network_overrides": null - }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "litecoreio.litecoin": { "provider": "litecoreio", "network": "litecoin", @@ -329,17 +296,6 @@ "denominator": 100000000, "network_overrides": null }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "smartbit": { "provider": "smartbit", "network": "bitcoin", @@ -461,28 +417,6 @@ "denominator": 100000000, "network_overrides": null }, - "chainso.dash": { - "provider": "chainso", - "network": "dash", - "client_class": "ChainSo", - "provider_coin_id": "DASH", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, - "chainso.dash.testnet": { - "provider": "chainso", - "network": "dash_testnet", - "client_class": "ChainSo", - "provider_coin_id": "DASHTEST", - "url": "https://chain.so/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "chainso.dogecoin": { "provider": "chainso", "network": "dogecoin", diff --git a/bitcoinlib/data/providers.json b/bitcoinlib/data/providers.json index 16a9e036..2ba2be0a 100644 --- a/bitcoinlib/data/providers.json +++ b/bitcoinlib/data/providers.json @@ -87,17 +87,6 @@ "denominator": 100000000, "network_overrides": {"prefix_address_p2sh": "32"} }, - "cryptoid.dash": { - "provider": "cryptoid", - "network": "dash", - "client_class": "CryptoID", - "provider_coin_id": "dash", - "url": "https://chainz.cryptoid.info/", - "api_key": "api-key-needed", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockchair": { "provider": "blockchair", "network": "bitcoin", @@ -131,17 +120,6 @@ "denominator": 100000000, "network_overrides": null }, - "blockchair.dash": { - "provider": "blockchair", - "network": "dash", - "client_class": "BlockChairClient", - "provider_coin_id": "", - "url": "https://api.blockchair.com/dash/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockchair.dogecoin": { "provider": "blockchair", "network": "dogecoin", @@ -240,17 +218,6 @@ "priority": 10, "denominator": 100000000, "network_overrides": null - }, - "litecoinblockexplorer.dash": { - "provider": "litecoinblockexplorer", - "network": "dash", - "client_class": "LitecoinBlockexplorerClient", - "provider_coin_id": "", - "url": "https://dashblockexplorer.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null }, "litecoinblockexplorer.testnet2": { "provider": "litecoinblockexplorer", @@ -306,17 +273,6 @@ "priority": 10, "denominator": 100000000, "network_overrides": null - }, - "blockbook.dash": { - "provider": "blockbook", - "network": "dash", - "client_class": "BlockbookClient", - "provider_coin_id": "", - "url": "https://dashblockexplorer.com/api/v2/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null }, "blockbook.testnet2": { "provider": "blockbook", @@ -340,17 +296,6 @@ "denominator": 100000000, "network_overrides": null }, - "insightdash": { - "provider": "insightdash", - "network": "dash", - "client_class": "InsightDashClient", - "provider_coin_id": "", - "url": "https://insight.dash.org/api/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "blockstream": { "provider": "blockstream", "network": "bitcoin", diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index c8965d0e..33838e0d 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -310,7 +310,7 @@ class DbKey(Base): used = Column(Boolean, default=False, doc="Has key already been used on the blockchain in as input or output? " "Default is False") network_name = Column(String(20), ForeignKey('networks.name'), - doc="Name of key network, i.e. bitcoin, litecoin, dash") + doc="Name of key network, i.e. bitcoin, litecoin") latest_txid = Column(LargeBinary(33), doc="TxId of latest transaction downloaded from the blockchain") witness_type = Column(String(20), default='segwit', doc="Key witness type, only specify when using mixed wallets. Can be 'legacy', 'segwit' or " @@ -346,7 +346,7 @@ class DbNetwork(Base): """ __tablename__ = 'networks' - name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin, dash") + name = Column(String(20), unique=True, primary_key=True, doc="Network name, i.e.: bitcoin, litecoin") description = Column(String(50)) def __repr__(self): diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 476e1002..278bc224 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1424,7 +1424,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger key_type = 'private' if is_private else 'public' if witness_type is None: - witness_type = 'legacy' if network == 'dash' else DEFAULT_WITNESS_TYPE + witness_type = DEFAULT_WITNESS_TYPE self.script_type = script_type if script_type else script_type_default(witness_type, multisig) if not encoding: encoding = get_encoding_from_witness(witness_type) diff --git a/bitcoinlib/networks.py b/bitcoinlib/networks.py index 9e4214c2..e8651a98 100644 --- a/bitcoinlib/networks.py +++ b/bitcoinlib/networks.py @@ -157,7 +157,7 @@ def wif_prefix_search(wif, witness_type=None, multisig=None, network=None): Can return multiple items if no network is specified: >>> [nw['network'] for nw in wif_prefix_search('0488ADE4', multisig=True)] - ['bitcoin', 'regtest', 'dash', 'dogecoin'] + ['bitcoin', 'regtest', 'dogecoin'] :param wif: WIF string or prefix as hexadecimal string :type wif: str diff --git a/bitcoinlib/services/__init__.py b/bitcoinlib/services/__init__.py index 80b291ca..286584c4 100644 --- a/bitcoinlib/services/__init__.py +++ b/bitcoinlib/services/__init__.py @@ -23,7 +23,6 @@ import bitcoinlib.services.bitcoind import bitcoinlib.services.dogecoind import bitcoinlib.services.litecoind -import bitcoinlib.services.dashd import bitcoinlib.services.bitgo import bitcoinlib.services.blockchaininfo import bitcoinlib.services.blockcypher @@ -33,7 +32,6 @@ import bitcoinlib.services.bcoin import bitcoinlib.services.bitaps import bitcoinlib.services.litecoinblockexplorer -import bitcoinlib.services.insightdash import bitcoinlib.services.blockstream import bitcoinlib.services.blocksmurfer import bitcoinlib.services.mempool diff --git a/bitcoinlib/services/cryptoid.py b/bitcoinlib/services/cryptoid.py index a8a1da87..3fbd5d45 100644 --- a/bitcoinlib/services/cryptoid.py +++ b/bitcoinlib/services/cryptoid.py @@ -123,6 +123,8 @@ def gettransaction(self, txid): return t def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): + if not self.api_key: + raise ClientError("Method gettransactions() is not available for CryptoID without API key") address = self._address_convert(address) txs = [] txids = [] diff --git a/bitcoinlib/services/dashd.py b/bitcoinlib/services/dashd.py deleted file mode 100644 index 1178d626..00000000 --- a/bitcoinlib/services/dashd.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# dashd deamon -# © 2018-2022 Oct - 1200 Web Development -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# -# You can connect to a dash testnet deamon by adding the following provider to providers.json -# "dashd.testnet": { -# "provider": "dashd", -# "network": "dash_testnet", -# "client_class": "DashdClient", -# "url": "http://user:password@server_url:19998", -# "api_key": "", -# "priority": 11, -# "denominator": 100000000 -# } - -import configparser -from bitcoinlib.main import * -from bitcoinlib.services.authproxy import AuthServiceProxy -from bitcoinlib.services.baseclient import BaseClient -from bitcoinlib.transactions import Transaction - - -PROVIDERNAME = 'dashd' - -_logger = logging.getLogger(__name__) - - -class ConfigError(Exception): - def __init__(self, msg=''): - self.msg = msg - _logger.info(msg) - - def __str__(self): - return self.msg - - -class DashdClient(BaseClient): - """ - Class to interact with dashd, the Dash deamon - """ - - @staticmethod - def from_config(configfile=None, network='dash', **kargs): - """ - Read settings from dashd config file - - :param configfile: Path to config file. Leave empty to look in default places - :type: str - :param network: Dash mainnet or testnet. Default is dash mainnet - :type: str - - :return DashdClient: - """ - config = configparser.ConfigParser(strict=False) - if not configfile: - cfn = os.path.join(os.path.expanduser("~"), '.bitcoinlib/dash.conf') - if not os.path.isfile(cfn): - cfn = os.path.join(os.path.expanduser("~"), '.dashcore/dash.conf') - if not os.path.isfile(cfn): - raise ConfigError("Please install dash client and specify a path to config file if path is not " - "default. Or place a config file in .bitcoinlib/dash.conf to reference to " - "an external server.") - else: - cfn = os.path.join(BCL_DATA_DIR, 'config', configfile) - if not os.path.isfile(cfn): - raise ConfigError("Config file %s not found" % cfn) - with open(cfn, 'r') as f: - config_string = '[rpc]\n' + f.read() - config.read_string(config_string) - - try: - if int(config.get('rpc', 'testnet')): - network = 'testnet' - except configparser.NoOptionError: - pass - if config.get('rpc', 'rpcpassword') == 'specify_rpc_password': - raise ConfigError("Please update config settings in %s" % cfn) - try: - port = config.get('rpc', 'port') - except configparser.NoOptionError: - if network == 'testnet': - port = 19998 - else: - port = 9998 - server = '127.0.0.1' - if 'bind' in config['rpc']: - server = config.get('rpc', 'bind') - elif 'externalip' in config['rpc']: - server = config.get('rpc', 'externalip') - url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) - return DashdClient(network, url, **kargs) - - def __init__(self, network='dash', base_url='', denominator=100000000, **kargs): - """ - Open connection to dashcore node - - :param network: Dash mainnet or testnet. Default is dash mainnet - :type: str - :param base_url: Connection URL in format http(s)://user:password@host:port. - :type: str - :param denominator: Denominator for this currency. Should be always 100000000 (satoshis) for Dash - :type: str - """ - if not base_url: - bdc = self.from_config('', network) - base_url = bdc.base_url - network = bdc.network - _logger.info("Connect to dashd") - self.proxy = AuthServiceProxy(base_url) - super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, **kargs) - - def _parse_transaction(self, tx, block_height=None, get_input_values=True): - t = Transaction.parse_hex(tx['hex'], strict=self.strict, network=self.network) - t.confirmations = None if 'confirmations' not in tx else tx['confirmations'] - if t.confirmations or block_height: - t.status = 'confirmed' - t.verified = True - for i in t.inputs: - if i.prev_txid == b'\x00' * 32: - i.script_type = 'coinbase' - continue - if get_input_values: - txi = self.proxy.getrawtransaction(i.prev_txid.hex(), 1) - i.value = int(round(float(txi['vout'][i.output_n_int]['value']) / self.network.denominator)) - for o in t.outputs: - o.spent = None - t.block_height = block_height - t.version = tx['version'].to_bytes(4, 'big') - t.date = datetime.utcfromtimestamp(tx['blocktime']) - t.update_totals() - return t - - def gettransaction(self, txid): - tx = self.proxy.getrawtransaction(txid, 1) - return self._parse_transaction(tx) - - def getrawtransaction(self, txid): - res = self.proxy.getrawtransaction(txid) - return res - - def sendrawtransaction(self, rawtx): - res = self.proxy.sendrawtransaction(rawtx) - return { - 'txid': res, - 'response_dict': res - } - - def estimatefee(self, blocks): - try: - res = self.proxy.estimatesmartfee(blocks)['feerate'] - except KeyError: - res = self.proxy.estimatefee(blocks) - return int(res * self.units) - - def blockcount(self): - return self.proxy.getblockcount() - - def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - txs = [] - - txs_list = self.proxy.listunspent(0, 99999999, [address]) - for t in sorted(txs_list, key=lambda x: x['confirmations'], reverse=True): - txs.append({ - 'address': t['address'], - 'txid': t['txid'], - 'confirmations': t['confirmations'], - 'output_n': t['vout'], - 'input_n': -1, - 'block_height': None, - 'fee': None, - 'size': 0, - 'value': int(t['amount'] * self.units), - 'script': t['scriptPubKey'], - 'date': None, - }) - if t['txid'] == after_txid: - txs = [] - - return txs - - def getblock(self, blockid, parse_transactions=True, page=1, limit=None): - if isinstance(blockid, int): - blockid = self.proxy.getblockhash(blockid) - if not limit: - limit = 99999 - - txs = [] - if parse_transactions: - bd = self.proxy.getblock(blockid, 2) - for tx in bd['tx'][(page - 1) * limit:page * limit]: - # try: - tx['blocktime'] = bd['time'] - tx['blockhash'] = bd['hash'] - txs.append(self._parse_transaction(tx, block_height=bd['height'], get_input_values=False)) - # except Exception as e: - # _logger.error("Could not parse tx %s with error %s" % (tx['txid'], e)) - # txs += [tx['hash'] for tx in bd['tx'][len(txs):]] - else: - bd = self.proxy.getblock(blockid, 1) - txs = bd['tx'] - - block = { - 'bits': int(bd['bits'], 16), - 'depth': bd['confirmations'], - 'hash': bd['hash'], - 'height': bd['height'], - 'merkle_root': bd['merkleroot'], - 'nonce': bd['nonce'], - 'prev_block': bd['previousblockhash'], - 'time': bd['time'], - 'total_txs': bd['nTx'], - 'txs': txs, - 'version': bd['version'], - 'page': page, - 'pages': None, - 'limit': limit - } - return block - - def getrawblock(self, blockid): - if isinstance(blockid, int): - blockid = self.proxy.getblockhash(blockid) - return self.proxy.getblock(blockid, 0) - - def isspent(self, txid, index): - res = self.proxy.gettxout(txid, index) - if not res: - return 1 - return 0 - - def getinfo(self): - info = self.proxy.getmininginfo() - return { - 'blockcount': info['blocks'], - 'chain': info['chain'], - 'difficulty': int(info['difficulty']), - 'hashrate': int(info['networkhashps']), - 'mempool_size': int(info['pooledtx']), - } - - -if __name__ == '__main__': - # - # SOME EXAMPLES - # - - from pprint import pprint - - # 1. Connect by specifying connection URL - # base_url = 'http://dashrpcuser:passwd@host:9998' - # bdc = DashdClient(base_url=base_url) - - # 2. Or connect using default settings or settings from config file - bdc = DashdClient() - - print("\n=== SERVERINFO ===") - pprint(bdc.proxy.getnetworkinfo()) - - print("\n=== Best Block ===") - blockhash = bdc.proxy.getbestblockhash() - bestblock = bdc.proxy.getblock(blockhash) - bestblock['tx'] = '...' + str(len(bestblock['tx'])) + ' transactions...' - pprint(bestblock) - - print("\n=== Mempool ===") - rmp = bdc.proxy.getrawmempool() - pprint(rmp[:25]) - print('... truncated ...') - print("Mempool Size %d" % len(rmp)) - - print("\n=== Raw Transaction by txid ===") - t = bdc.getrawtransaction('c3d2a934ef8eb9b2291d113b330b9244c1521ef73df0a4b04c39e851112f01af') - pprint(t) - - print("\n=== Current network fees ===") - t = bdc.estimatefee(5) - pprint(t) diff --git a/bitcoinlib/services/insightdash.py b/bitcoinlib/services/insightdash.py deleted file mode 100644 index a4d1a57a..00000000 --- a/bitcoinlib/services/insightdash.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# Litecore.io Client -# © 2018-2022 October - 1200 Web Development -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -import logging -from datetime import datetime -from bitcoinlib.main import MAX_TRANSACTIONS -from bitcoinlib.services.baseclient import BaseClient -from bitcoinlib.transactions import Transaction - -PROVIDERNAME = 'insightdash' -REQUEST_LIMIT = 50 - -_logger = logging.getLogger(__name__) - - -class InsightDashClient(BaseClient): - - def __init__(self, network, base_url, denominator, *args): - super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, *args) - - def compose_request(self, category, data, cmd='', variables=None, method='get', offset=0): - url_path = category - if data: - url_path += '/' + data + '/' + cmd - if variables is None: - variables = {} - variables.update({'from': offset, 'to': offset+REQUEST_LIMIT}) - return self.request(url_path, variables, method=method) - - def _convert_to_transaction(self, tx): - if tx['confirmations']: - status = 'confirmed' - else: - status = 'unconfirmed' - fees = None if 'fees' not in tx else int(round(float(tx['fees']) * self.units, 0)) - value_in = 0 if 'valueIn' not in tx else tx['valueIn'] - isCoinbase = False - if 'isCoinBase' in tx and tx['isCoinBase']: - isCoinbase = True - txdate = None - if 'blocktime' in tx: - txdate = datetime.utcfromtimestamp(tx['blocktime']) - t = Transaction(locktime=tx['locktime'], version=tx['version'], network=self.network, - fee=fees, size=tx['size'], txid=tx['txid'], - date=txdate, confirmations=tx['confirmations'], - block_height=tx['blockheight'], status=status, - input_total=int(round(float(value_in) * self.units, 0)), coinbase=isCoinbase, - output_total=int(round(float(tx['valueOut']) * self.units, 0))) - for ti in tx['vin']: - if isCoinbase: - t.add_input(prev_txid=32 * b'\0', output_n=4*b'\xff', unlocking_script=ti['coinbase'], index_n=ti['n'], - script_type='coinbase', sequence=ti['sequence'], value=0) - else: - value = int(round(float(ti['value']) * self.units, 0)) - t.add_input(prev_txid=ti['txid'], output_n=ti['vout'], unlocking_script=ti['scriptSig']['hex'], - index_n=ti['n'], value=value, sequence=ti['sequence'], - double_spend=False if ti['doubleSpentTxID'] is None else ti['doubleSpentTxID'], - strict=self.strict) - for to in tx['vout']: - value = int(round(float(to['value']) * self.units, 0)) - t.add_output(value=value, lock_script=to['scriptPubKey']['hex'], - spent=True if to['spentTxId'] else False, output_n=to['n'], - spending_txid=None if not to['spentTxId'] else to['spentTxId'], - spending_index_n=None if not to['spentIndex'] else to['spentIndex'], strict=self.strict) - return t - - def getbalance(self, addresslist): - balance = 0 - addresslist = self._addresslist_convert(addresslist) - for a in addresslist: - res = self.compose_request('addr', a.address, 'balance') - balance += res - return balance - - def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - address = self._address_convert(address) - res = self.compose_request('addrs', address.address, 'utxo') - txs = [] - for tx in res: - if tx['txid'] == after_txid: - break - txs.append({ - 'address': address.address_orig, - 'txid': tx['txid'], - 'confirmations': tx['confirmations'], - 'output_n': tx['vout'], - 'input_n': 0, - 'block_height': tx['height'], - 'fee': None, - 'size': 0, - 'value': tx['satoshis'], - 'script': tx['scriptPubKey'], - 'date': None - }) - return txs[::-1][:limit] - - def gettransaction(self, tx_id): - tx = self.compose_request('tx', tx_id) - return self._convert_to_transaction(tx) - - def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): - address = self._address_convert(address) - res = self.compose_request('addrs', address.address, 'txs') - txs = [] - txs_dict = res['items'][::-1] - if after_txid: - txs_dict = txs_dict[[t['txid'] for t in txs_dict].index(after_txid) + 1:] - for tx in txs_dict[:limit]: - if tx['txid'] == after_txid: - break - txs.append(self._convert_to_transaction(tx)) - return txs - - def getrawtransaction(self, tx_id): - res = self.compose_request('rawtx', tx_id) - return res['rawtx'] - - def sendrawtransaction(self, rawtx): - res = self.compose_request('tx', 'send', variables={'rawtx': rawtx}, method='post') - return { - 'txid': res['txid'], - 'response_dict': res - } - - # def estimatefee - - def blockcount(self): - res = self.compose_request('status', '', variables={'q': 'getinfo'}) - return res['info']['blocks'] - - def mempool(self, txid): - res = self.compose_request('tx', txid) - if res['confirmations'] == 0: - return res['txid'] - return [] - - def getblock(self, blockid, parse_transactions, page, limit): - bd = self.compose_request('block', str(blockid)) - if parse_transactions: - txs = [] - for txid in bd['tx'][(page-1)*limit:page*limit]: - try: - txs.append(self.gettransaction(txid)) - except Exception as e: - _logger.error("Could not parse tx %s with error %s" % (txid, e)) - else: - txs = bd['tx'] - - block = { - 'bits': bd['bits'], - 'depth': bd['confirmations'], - 'hash': bd['hash'], - 'height': bd['height'], - 'merkle_root': bd['merkleroot'], - 'nonce': bd['nonce'], - 'prev_block': bd['previousblockhash'], - 'time': datetime.utcfromtimestamp(bd['time']), - 'total_txs': len(bd['tx']), - 'txs': txs, - 'version': bd['version'], - 'page': page, - 'pages': None if not limit else int(len(bd['tx']) // limit) + (len(bd['tx']) % limit > 0), - 'limit': limit - } - return block - - def isspent(self, txid, output_n): - t = self.gettransaction(txid) - return 1 if t.outputs[output_n].spent else 0 - - def getinfo(self): - info = self.compose_request('status', '')['info'] - return { - 'blockcount': info['blocks'], - 'chain': info['network'], - 'difficulty': int(float(info['difficulty'])), - 'hashrate': 0, - 'mempool_size': 0, - } diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index dd5a22cc..630f298f 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -161,7 +161,7 @@ def _provider_execute(self, method, *arguments): if self.resultcount >= self.max_providers: break try: - if sp not in ['bitcoind', 'litecoind', 'dashd', 'dogecoind', 'caching'] and not self.providers[sp]['url'] and \ + if sp not in ['bitcoind', 'litecoind', 'dogecoind', 'caching'] and not self.providers[sp]['url'] and \ self.network.name != 'bitcoinlib_test': continue client = getattr(services, self.providers[sp]['provider']) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index ed9c6f58..c5ce5abc 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1267,8 +1267,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 if network is None: network = DEFAULT_NETWORK if witness_type is None: - witness_type = DEFAULT_WITNESS_TYPE if network != 'dash' else 'legacy' - if network in ['dash', 'dash_testnet', 'dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': + witness_type = DEFAULT_WITNESS_TYPE + if network in ['dogecoin', 'dogecoin_testnet'] and witness_type != 'legacy': raise WalletError("Segwit is not supported for %s wallets" % network.capitalize()) elif network in ('dogecoin', 'dogecoin_testnet') and witness_type not in ('legacy', 'p2sh-segwit'): raise WalletError("Pure segwit addresses are not supported for Dogecoin wallets. " @@ -1358,12 +1358,9 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke """ self._session = None + self._engine = None if session: self._session = session - # else: - # dbinit = Db(db_uri=db_uri, password=db_password) - # self.session = dbinit.session - # self._engine = dbinit.engine self._db_password = db_password self.db_uri = db_uri self.db_cache_uri = db_cache_uri diff --git a/tests/test_wallets.py b/tests/test_wallets.py index cf40abaf..1259fbe5 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2696,7 +2696,7 @@ def test_wallet_mixed_witness_no_private_key(self): address = 'bc1qgf8fzfj65lcr5vae0sh77akurh4zc9s9m4uspm' w = Wallet.create('wallet_mix_no_private', keys=pub_master, db_uri=self.database_uri) self.assertEqual(address, w.get_key().address) - self.assertRaisesRegexp(WalletError, "This wallet has no private key, cannot use multiple witness types", + self.assertRaisesRegex(WalletError, "This wallet has no private key, cannot use multiple witness types", w.get_key, witness_type='legacy') def test_wallet_mixed_witness_type_create(self): From 9403f24db444c958d44e889f47783c900ae7a2b7 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Thu, 22 Feb 2024 16:22:37 +0100 Subject: [PATCH 113/207] Encrypt private key field with key or password in environment --- .github/workflows/unittests-postgresql.yaml | 1 + bitcoinlib/config/config.py | 4 +- bitcoinlib/db.py | 60 +++++++++++++-------- tests/test_security.py | 38 +++++++++++-- 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index 817c1e1a..5acc34d7 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -36,4 +36,5 @@ jobs: env: BCL_CONFIG_FILE: config.ini.unittest UNITTEST_DATABASE: postgresql + DB_FIELD_ENCRYPTION_PASSWORD: verybadpassword run: coverage run --source=bitcoinlib -m unittest -v diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 15d5569c..53d61092 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -47,6 +47,7 @@ ALLOW_DATABASE_THREADS = None DATABASE_ENCRYPTION_ENABLED = False DB_FIELD_ENCRYPTION_KEY = None +DB_FIELD_ENCRYPTION_PASSWORD = None # Services TIMEOUT_REQUESTS = 5 @@ -238,7 +239,7 @@ def config_get(section, var, fallback, is_boolean=False): global ALLOW_DATABASE_THREADS, DEFAULT_DATABASE_CACHE global BCL_LOG_FILE, LOGLEVEL, ENABLE_BITCOINLIB_LOGGING global TIMEOUT_REQUESTS, DEFAULT_LANGUAGE, DEFAULT_NETWORK, DEFAULT_WITNESS_TYPE - global SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY + global SERVICE_CACHING_ENABLED, DATABASE_ENCRYPTION_ENABLED, DB_FIELD_ENCRYPTION_KEY, DB_FIELD_ENCRYPTION_PASSWORD global SERVICE_MAX_ERRORS, BLOCK_COUNT_CACHE_TIME, MAX_TRANSACTIONS # Read settings from Configuration file provided in OS environment~/.bitcoinlib/ directory @@ -273,6 +274,7 @@ def config_get(section, var, fallback, is_boolean=False): SERVICE_CACHING_ENABLED = config_get('common', 'service_caching_enabled', fallback=True, is_boolean=True) DATABASE_ENCRYPTION_ENABLED = config_get('common', 'database_encryption_enabled', fallback=False, is_boolean=True) DB_FIELD_ENCRYPTION_KEY = os.environ.get('DB_FIELD_ENCRYPTION_KEY') + DB_FIELD_ENCRYPTION_PASSWORD = os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') # Log settings ENABLE_BITCOINLIB_LOGGING = config_get("logs", "enable_bitcoinlib_logging", fallback=True, is_boolean=True) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 33838e0d..26bef1d2 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -26,7 +26,7 @@ from sqlalchemy.orm import sessionmaker, relationship, session from urllib.parse import urlparse from bitcoinlib.main import * -from bitcoinlib.encoding import aes_encrypt, aes_decrypt +from bitcoinlib.encoding import aes_encrypt, aes_decrypt, double_sha256 _logger = logging.getLogger(__name__) Base = declarative_base() @@ -136,28 +136,43 @@ def add_column(engine, table_name, column): return result +def _get_encryption_key(default_impl): + impl = default_impl + key = None + if DATABASE_ENCRYPTION_ENABLED: + if not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): + _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " + "environment. Please supply 32 bytes key as hexadecimal string.") + if DB_FIELD_ENCRYPTION_KEY: + impl = default_impl + key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + elif DB_FIELD_ENCRYPTION_PASSWORD: + impl = default_impl + key = double_sha256(bytes(DB_FIELD_ENCRYPTION_PASSWORD, 'utf8')) + return key, impl + + class EncryptedBinary(TypeDecorator): """ FieldType for encrypted Binary storage using EAS encryption """ - impl = LargeBinary cache_ok = True - key = None - if DATABASE_ENCRYPTION_ENABLED: - if not DB_FIELD_ENCRYPTION_KEY: - _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - "environment. Please supply 32 bytes key as hexadecimal string.") - else: - key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + key, impl = _get_encryption_key(LargeBinary) + # if DATABASE_ENCRYPTION_ENABLED: + # if not DB_FIELD_ENCRYPTION_KEY: + # _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " + # "environment. Please supply 32 bytes key as hexadecimal string.") + # if DB_FIELD_ENCRYPTION_KEY: + # key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) def process_bind_param(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value return aes_encrypt(value, self.key) def process_result_value(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value return aes_decrypt(value, self.key) @@ -167,26 +182,27 @@ class EncryptedString(TypeDecorator): FieldType for encrypted String storage using EAS encryption """ - impl = String + # impl = String cache_ok = True - key = None - if DATABASE_ENCRYPTION_ENABLED: - if not DB_FIELD_ENCRYPTION_KEY: - _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - "environment. Please supply 32 bytes key as hexadecimal string.") - else: - impl = LargeBinary - key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + # key = None + # if DATABASE_ENCRYPTION_ENABLED: + # if not DB_FIELD_ENCRYPTION_KEY: + # _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " + # "environment. Please supply 32 bytes key as hexadecimal string.") + # if DB_FIELD_ENCRYPTION_KEY: + # impl = LargeBinary + # key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) + key, impl = _get_encryption_key(String) def process_bind_param(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value if not isinstance(value, bytes): value = bytes(value, 'utf8') return aes_encrypt(value, self.key) def process_result_value(self, value, dialect): - if value is None or self.key is None or not DATABASE_ENCRYPTION_ENABLED: + if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value return aes_decrypt(value, self.key).decode('utf8') diff --git a/tests/test_security.py b/tests/test_security.py index 4235971c..8eaf5c9f 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -23,7 +23,7 @@ from sqlalchemy.sql import text from bitcoinlib.db import BCL_DATABASE_DIR from bitcoinlib.wallets import Wallet -from bitcoinlib.config.config import DATABASE_ENCRYPTION_ENABLED +from bitcoinlib.keys import HDKey try: @@ -59,7 +59,7 @@ class TestSecurity(TestCase): - def test_security_wallet_field_encryption(self): + def test_security_wallet_field_encryption_key(self): pk = 'xprv9s21ZrQH143K2HrtPWvqgD8mUhMrrfE1ZME43baM8ti3hWgJwWX1wjHc25y2x11seT5G3KeHFY28MyTRxceeW22kMDAWsMDn7' \ 'rcWnEMFP3t' pk_wif_enc_hex = \ @@ -68,8 +68,8 @@ def test_security_wallet_field_encryption(self): '17f1ffea8e20844309f6fb6b281349a2b3915af3d12dc4c90c3b68f6666eb665682d' pk_enc_hex = 'f8777f10a435d5e3fdbb64cfdcb929626ce38c7103e772921ad1fc21c5e69e474423a998523bf53565ab45711a14086c' - if not DATABASE_ENCRYPTION_ENABLED: - self.skipTest("Database encryption not enabled, skip this test") + if not os.environ.get('DB_FIELD_ENCRYPTION_KEY'): + self.skipTest("Database field encryption key not found in environment, skip this test") self.assertEqual(os.environ.get('DB_FIELD_ENCRYPTION_KEY'), '11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff') @@ -87,6 +87,36 @@ def test_security_wallet_field_encryption(self): self.assertEqual(encrypted_main_key_wif.hex(), pk_wif_enc_hex) self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) + def test_security_wallet_field_encryption_password(self): + pk = ('zprvAWgYBBk7JR8GivM5h6vdbXRrYRC6CU9aFDsVp2gLZ82Tx74UGf7nnN4cToSvNsDnK19tkuyXjzBMDcYvuseYYE5Q4qQo9JaLuNGz' + 'hfcovSp') + pk_wif_enc_hex = \ + ('92410397d5a80ce75cdb5b0fe1204fd2e1411752c75f7b32a8c6a5574570d8be97155bcc4a86b6d34b0e6c22dfe32d340cc90dae1' + '5e54316a9db538ad8a274881c7a45a7be0a00d6e5deda2ea28d8fa0ffcf8783b1fb580df6f5c056e43b79a93859bd083fc1922c86' + 'c17a3f945bbad5fa699a9d1cb2fc9f240708a1eee90b') + pk_enc_hex = '1ff6958f0edc774f16d09d9fb36baa912fb9034f0e354c354a0a91c21e58fe05ad7c8089642565bf4fffd357db108117' + + if not os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD'): + self.skipTest("Database field encryption password not found in environment, skip this test") + self.assertEqual(os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD'), + 'verybadpassword') + + wallet = Wallet.create('wlt-private-key-encryption-test-pwd', keys=pk, + db_uri=DATABASEFILE_UNITTESTS_ENCRYPTED) + wallet.new_key() + self.assertEqual(wallet.main_key.wif, pk) + + if os.getenv('UNITTEST_DATABASE') == 'mysql': + db_query = text("SELECT wif, private FROM `keys` WHERE id=%d" % wallet._dbwallet.main_key_id) + else: + db_query = text("SELECT wif, private FROM keys WHERE id=%d" % wallet._dbwallet.main_key_id) + encrypted_main_key_wif = wallet.session.execute(db_query).fetchone()[0] + encrypted_main_key_private = wallet.session.execute(db_query).fetchone()[1] + self.assertIn(type(encrypted_main_key_wif), (bytes, memoryview), "Encryption of database private key failed!") + self.assertEqual(encrypted_main_key_wif.hex(), pk_wif_enc_hex) + self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) + self.assertNotEqual(encrypted_main_key_private, HDKey(pk).private_byte) + if __name__ == '__main__': main() From a005eeedabcf020b38ddf5b0b5f5f139e2c67225 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 24 Feb 2024 21:41:03 +0100 Subject: [PATCH 114/207] Fix to store encrypted fields in postgresql --- bitcoinlib/db.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 26bef1d2..8f26da69 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -159,12 +159,6 @@ class EncryptedBinary(TypeDecorator): cache_ok = True key, impl = _get_encryption_key(LargeBinary) - # if DATABASE_ENCRYPTION_ENABLED: - # if not DB_FIELD_ENCRYPTION_KEY: - # _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - # "environment. Please supply 32 bytes key as hexadecimal string.") - # if DB_FIELD_ENCRYPTION_KEY: - # key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) def process_bind_param(self, value, dialect): if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): @@ -182,16 +176,7 @@ class EncryptedString(TypeDecorator): FieldType for encrypted String storage using EAS encryption """ - # impl = String cache_ok = True - # key = None - # if DATABASE_ENCRYPTION_ENABLED: - # if not DB_FIELD_ENCRYPTION_KEY: - # _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " - # "environment. Please supply 32 bytes key as hexadecimal string.") - # if DB_FIELD_ENCRYPTION_KEY: - # impl = LargeBinary - # key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) key, impl = _get_encryption_key(String) def process_bind_param(self, value, dialect): @@ -204,6 +189,8 @@ def process_bind_param(self, value, dialect): def process_result_value(self, value, dialect): if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value + if value.startswith('\\x'): + value = bytes.fromhex(value[2:]) return aes_decrypt(value, self.key).decode('utf8') @@ -306,7 +293,7 @@ class DbKey(Base): address_index = Column(BigInteger, doc="Index of address in HD key structure address level") public = Column(LargeBinary(65), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") - wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") + wif = Column(EncryptedString(260), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") address = Column(String(100), index=True, From 5905559c3097ca28bacfbe43114b7663e234f08d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 24 Feb 2024 22:08:52 +0100 Subject: [PATCH 115/207] Correctly store encrypted fields as bytes in psql database --- bitcoinlib/db.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 8f26da69..c10d8f10 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -144,10 +144,10 @@ def _get_encryption_key(default_impl): _logger.warning("Database encryption is enabled but value DB_FIELD_ENCRYPTION_KEY not found in " "environment. Please supply 32 bytes key as hexadecimal string.") if DB_FIELD_ENCRYPTION_KEY: - impl = default_impl + impl = LargeBinary key = bytes().fromhex(DB_FIELD_ENCRYPTION_KEY) elif DB_FIELD_ENCRYPTION_PASSWORD: - impl = default_impl + impl = LargeBinary key = double_sha256(bytes(DB_FIELD_ENCRYPTION_PASSWORD, 'utf8')) return key, impl @@ -189,8 +189,8 @@ def process_bind_param(self, value, dialect): def process_result_value(self, value, dialect): if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): return value - if value.startswith('\\x'): - value = bytes.fromhex(value[2:]) + # if value.startswith('\\x'): + # value = bytes.fromhex(value[2:]) return aes_decrypt(value, self.key).decode('utf8') @@ -293,7 +293,7 @@ class DbKey(Base): address_index = Column(BigInteger, doc="Index of address in HD key structure address level") public = Column(LargeBinary(65), index=True, doc="Bytes representation of public key") private = Column(EncryptedBinary(48), doc="Bytes representation of private key") - wif = Column(EncryptedString(260), index=True, doc="Public or private WIF (Wallet Import Format) representation") + wif = Column(EncryptedString(128), index=True, doc="Public or private WIF (Wallet Import Format) representation") compressed = Column(Boolean, default=True, doc="Is key compressed or not. Default is True") key_type = Column(String(10), default='bip32', doc="Type of key: single, bip32 or multisig. Default is bip32") address = Column(String(100), index=True, From 2c6a3c56fe745ecdeaec1c953fe50b16b64b188f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 09:33:11 +0100 Subject: [PATCH 116/207] Show clearer warnings when try to open encrypted database --- bitcoinlib/db.py | 4 ++-- bitcoinlib/encoding.py | 6 +++++- tests/test_security.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index c10d8f10..5a7089de 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -188,9 +188,9 @@ def process_bind_param(self, value, dialect): def process_result_value(self, value, dialect): if value is None or self.key is None or not (DB_FIELD_ENCRYPTION_KEY or DB_FIELD_ENCRYPTION_PASSWORD): + if isinstance(value, bytes): + raise ValueError("Data is encrypted please provide key in environment") return value - # if value.startswith('\\x'): - # value = bytes.fromhex(value[2:]) return aes_decrypt(value, self.key).decode('utf8') diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index b16419af..ea7922dc 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -946,7 +946,11 @@ def aes_decrypt(encrypted_data, key): ct = encrypted_data[:-16] tag = encrypted_data[-16:] cipher2 = AES.new(key, AES.MODE_SIV) - return cipher2.decrypt_and_verify(ct, tag) + try: + res = cipher2.decrypt_and_verify(ct, tag) + except ValueError as e: + raise EncodingError("Could not decrypt value (password incorrect?): %s" % e) + return res def bip38_decrypt(encrypted_privkey, password): diff --git a/tests/test_security.py b/tests/test_security.py index 8eaf5c9f..6babfcdb 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -24,6 +24,7 @@ from bitcoinlib.db import BCL_DATABASE_DIR from bitcoinlib.wallets import Wallet from bitcoinlib.keys import HDKey +from bitcoinlib.encoding import EncodingError try: @@ -117,6 +118,17 @@ def test_security_wallet_field_encryption_password(self): self.assertEqual(encrypted_main_key_private.hex(), pk_enc_hex) self.assertNotEqual(encrypted_main_key_private, HDKey(pk).private_byte) + def test_security_encrypted_db_incorrect_password(self): + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.sqlite') + self.assertRaisesRegex(EncodingError, "Could not decrypt value \(password incorrect\?\): MAC check failed", + Wallet, 'wlt-encryption-test', db_uri=db) + + def test_security_encrypted_db_no_password(self): + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.sqlite') + if os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY'): + self.skipTest("This test only runs when no encryption keys are provided") + self.assertRaisesRegex(ValueError, "Data is encrypted please provide key in environment", + Wallet, 'wlt-encryption-test', db_uri=db) if __name__ == '__main__': main() From 2d621bf88ad135f87dd4c571b08102985013c410 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 09:58:12 +0100 Subject: [PATCH 117/207] Rename database to avoid gitignore --- tests/bitcoinlib_encrypted.db | Bin 0 -> 139264 bytes tests/test_security.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/bitcoinlib_encrypted.db diff --git a/tests/bitcoinlib_encrypted.db b/tests/bitcoinlib_encrypted.db new file mode 100644 index 0000000000000000000000000000000000000000..12d45fe411f4a47e39dc5639ecdd84e5cbc4047e GIT binary patch literal 139264 zcmeI*3w#qr;s@~EY?>ye?G~`afkwF{iCyvAK^7Ic|Z97WUA^*S9zQCE}yL&<;t&lTi@(*K40 zrDdPn`*<}qa`xm1F)({k45O9)v=@`l4$L-*2 z9b}w+R2uoQ)l_q74$kQ`yM?roWp=?*V|P02WocHkU@kNBTv~A2NNKsT78_@&GV$hW z&Lr5}oY^{hg4w}yBNa?qe#ojw9GoHDkd>NlNHt{X44I>h>7z38MrIgtj5*nHG&3Nf zj&t)QRp}$MM`mP2kvuW%D-`=4`wIJYmokVz5P$##AOHafKmY;|fB*y_009Veo4^%J zDjm`lKzbdRcqT1$qS!y6h++n4e7ykT_&?2lLXm&?fdB*`009U<00Izz00bZa0SG|g zXCTmrW>To&wFM@D;{|d2pJqR$$Upo*00Izz00bZa0SG_<0uX=z1R(Hp5a`3CXj+~B zOXL5`DR%kKA%n;R1Rwwb2tWV=5P$##AOHafK%gfJG|;`N%$O8fn~(3*v)D2X)oCt|+f~DJNonFU1x!Mti+nU& zEHxgdK&IKu&I&HgY_+;Mo;TT@R<1!B|8Jq#mY(c`v_Sv@5P$##AOHafKmY;|fB*y_ z&?SL>Or|DGpMp64uZlcH#qNl)L@T0xtC>!|;0FQ_fB*y_0D+&2z*4iSKSiggX|=l8 zu{2tvwNMGPCP$;xq)-VFzEJ_4cT_-^9>Gu#tTBC2_1TySduwJM)EO4vFy-HGzqM!G zuAJlRNnUTHOrnn*@rQFu<}&jrYWYJS$(hH^nU#9x_Rk}7iX~N;})NJZs-29 z8?A{ag`r#CYMzxh=GuO9^Qx5Bef4s@F>P4slv9(QGZ+VzY3(^@7Eal+?-Q=}y!zCm zj&@2y|xpXfJiRZiUaQ+|3n z%!uL7ZsPiyj=lE_%R@`Po|v*{`LM!MC2z;pBrf?&?1o9o!4tRD3(uPlvt^T(R1Z6P zIP*lA_R7Ai3R4$dSCV*9(<^Iz!<$==Ru9|0`|y~`1*cky3f5imhexM5XFY%R#&gqh zmk&Nh|LMT7xc}CFUUN=AHl<(DqWr@`vgfti-OHZfJ=guVc}?QPhPx^kZc?m$C3e*| z{iz_X$K|ef=NX#vjCqyh3D;FzRvlNBXEhqExgJk#R(5S|Q*LHu`?#$JxqPg1l!SFK zVU3U#-86}n@eOe4(iw_cLTamm|8UZ8XOw#yvmTl^;KdD*^Vfe|WjNuQU-@^&`P921|@vVU)VKz>nlp_@4jJ$)khajUh#B)&wKYIf0LYgVcm|W zWA`jnm9C!nbj%<}bno|HzW!5nLczDs+@pGA`Kd$tm@ntux9G9LPj5R|6I;9Kz{UHH z_j}4dUOW0($5-?JF@D*Y=;NE~e;>8Qcti5j5e2VYando!wD4Hmt(ymJeIRMn=GAYm zIYK=&e`e0x`z~xA*s`kN%_S*SR~JuhD6K2|{U87Qx-;XE;YYHfZrXYB?yItM`z!Q` zC${W;q3-T6(en~MH#~Uf%z~1)|M{=IV@}VzGH6_$V{tp`@+z}=S5t$lDbrn*US8Q~ zsjg}?aM?|sTw|`gygoauad_aVivE;lh*}d;c(oF+Z@^ZUo8>LLrOY1n@XX>>=lXuI zd;b#ptGPFiZmBx`-7keG?$K{1Y`J60n%jR_KH`P%RClMe488VQYVFKDd9Sig>N))$ zx=sjes?u&$!_?C~(-Tc_^K8%0&zH|2vQLY*t_xz-0-NsqFCKf+DAnM(}-SK|OIK#c! zTUBF+ZL6sLR(o*Zp^Zyo&g`I0R=@kmzH7%-CvMl%%*qv(JGVz}7=G-l;c1)SN?xA( zc+_`~oLzPIf{c3(2VFfjgOe{eI$U)eCsbw&^_5($Exp`fH8<268?$m69FDBakY+bL z=M=g>rOs57A6Z+#N}OTNaGBK!mFHszz3|OW&oqPT@wC;aJ{$eg(UH-O_YFH+`O>c5 z(^j8-*OtBL;YEdaB_E!rUVHRd{+VAzr*0lJ>kl`#y!K+^#HLmJ;)VY@Sn^(M)n9L% zonu{Ud*;svb`5&%cVhjk zOyNt4%?~AR-Emdqg~5vseJ+f>_FlI}mGJn9Gpea0pR}EC9->*$cf?))Y;vtwJN)jw z(}!uY_dS1o$wyDTqq7X&6L~|!easiLAHJA8ch;-tryqHK)1jkdj|W9_nsVLs&I+F6 z-Ff9z7QyY#%jTQ1Glfd0tKQL+*;FUw#SpqkyhGS zKanDlWI@wu=ZE+FKlT}l)w65a{jtx)e#x3TvB7KzKmY;|fB*y_009U<00Izzz)ve+ zRB9abrCx`o@-B6$7g$Ajm%7vohm_u>F7;9v#=F#|Ug)97A{v@Yy_ms!{vZ7Y75zrc z@z~RBW6Ydb`%k+Ou{H=m00Izz00bZa0SG_<0^d`hSrwsK8clci-9{>3I=cJDoCse! zy89Lur7s=beVYg4OGkI#7NKa45X-5iyKg3t#{b`;*nhC2Stq-ZjgEcid)7QG1_1~_ z00Izz00bZa0SG_<0ubm9fpn$DLSMoIUsS;by7my0h~NTUdoG7ExIot)GQk8F=-Sg0 z6lo;W8gHh%_HY2{{{Isc`we@R{SW&o`!Rc>J2n>92>}Q|00Izz00bZa0SG_<0uX?} z4-!x^YFZ`!iV%M(#b1p0OTjQ|jdcFMmST62`~SDHd&v_3@g~3@#Epd^009U<00Izz z00bZa0SG_<0$mj7%cRh)-p8YgW(Ly!_s2xUFoWs9TThhnOd8$hJsnIeGlUMlT|&{$ z`~Rft|BqAb8*DyX%Wh_S#~$xuDiI6<5P$##AOHafKmY;|fB*y_&~pURm`l3}AP~Nb z_W%UKckvd0K=>}+0T2k^#Tx)dGT-C+|A7?yYu3&VWZz+TvIRY7;~^ywfB*y_009U< z00Izz00bZafuBGik?G1y0KK6)eFvZzs`FO>dP8;k20-r`JADD*HB49E{}=r>75#S1 z2jtyB&Ac$3lnuK@5S zqx;_g;7vyNzW_iK@*g@j;|AV?Ebyz6`AOHafKmY;|fB*y_009U< zpxXs%6#b~F8g+t3ov)@zY6h#->J&;%RZbuN-<|UAQiP!0eW! zbx$6eySxr%zbU`RJ)rKTHF4Ou#aVU#g_R7T#&%v@tuW?qz~##wH6S98`fyC6Qg z&tWe!2^=rf(BW5@m3;AurGr)symR^EgA+CTBl=Ew@YRb89^16#+XC0IEi<0&V1?Ov zIT=~)u26jbpMu>MNNyiB_c=pv2ufQj#5djVD<-- z5dIUnxZsxXb2^#2ys1y5HZhT2l_QwT9Gu`bJ9)E3u-7IhwwMra^2#!a0*PW)1Dc97^z!Jy(q->Vc_ zO(i`zPLAhILZge*&B`w+n3P|VY)IFOOVn|0o+M5;u4p1zr8vb=W2qAC)tqdS)huwj z@g%*aMbiq!Fk(+nnVBbHrca+*m|rYS^Y#Rpy?OPgUz|}U#%Qu}_6nOIpU&HxIQjQ- zPQFfFFbf{Ob#m>z$x>78A~_FPx83RT2qvLMFgs+)uJH)rCObKyzQ$c;a+*mdTXDF} z^#Nf*V=uRJggltZp?0U0YZxj|_6;RP)ACE^=%y6T(IwlhUe$u>#WPAv@`44sr`J^(0RLcYC17V^y?ONpCOAElqs zTcu5lr)#|%$=+b9;u?9sy4@;%>*K3;MaAO_Z_>31qMKf<6NAY%f<8Y^rM)_yo)a3W zy<7~}JA8EZa#=+7a{XjhrPasNTu2lyPnpAR5o3u98;dkQ7?Jc{KP^_JO^K)NA#wTM zu_8vshKnTp@pw)Dl-5jZWp$(-s5B|EJ zD>a3|^~$Tam2(L;`2^p)YkUo~*ueQLP2Swg+8^H*^tae9x4lkmNd4dINip$%78^A; zX`@418yE?|-R|EGwja z4J0$z>bcB>z^~rJm; zl5gEu`e%?#kmiynN)AqJzsl^cOoRBL+T#%Hgw@xSg>TvYCBQG;W;Om9(rUzAr!m*N z8qQ)P>uJ*r%bH%FGbv1BTHD`O_}T}tK!#7GKO#~LJBdjusI7MmZPP?ba0dw@b32d1 zFV}v){4JH3G5KyGQ2s*p3DVRbx+6*IrP5v-PgC9+&DT2Y0%tON1aj0v_WsgtC5ssH z(MeIA2Fw%#_Da%p8qgpHj7m~>8Zbi)s9FAth)C^#0rV=Zw{7%g-1kQn(n1DM+C8)k z*B;i?lcpq4MU7Gu(^2&e)1L4a8z(=?k{X_n0|$S*;%fr}HN`(YLt3Vq972imt=cw! z^)xw6Zm!m2cas)W)}HY-Zo#{~oV}R zRNrBv_p9LcwTu1(WAC9x>t;Y+AVXS$R!4`emkShvkc9#@g&cQB=~GoIZT^7vngL%} zQ-<$DWOH98Qaf-U-Q4J{k^Thu|0+Z3q<^Y-$7sa#cg+qr&X;hV@Qb# z=!9@Z>C==dZNb3yGT~2#DZ~FMVlf@5O-P`d=Xi5LHg37SLi();$$>OYOn;r(Z5PjW z1L@0bm4`aBgPh>_YqY^ARq2rA`{N+&DGHT#Y(je}mR4)Zkp42{d|WM_lS||OXTA6T zcXE!7*${vL1Rwwb2tWV=5P$##AOHaf{QnYAGHUVuf3+qi@;ntoHBy>4HS^SG)b(mw zwJ35MQ>DsNd_XlS9%So&q>SAi|(DH$QlW&qPq$efJGGe!i zd~$I3Dx`DBH^l89zYm$$wzJgNmENW!M(SV+3`R*`Lr0!`y|YqBPViy@-bKdNbPy*nRq8-@%DZSodVxZ-JVNKsUk3vT@=Jd)PZhFX~ct!%hrXKW`CiS4fS@t2JvK+f^@*khe%sjDr*h7 zG=MXCD~-Hvby!n;G_5O&Y*R=V$&hYS|Me)Bsk6SLp$FOO$?&*%HBKjWQ=W~VrfL=p zP-$mpbfs!aMK!c$O49}{O}#Qwo0CH?le+%JisBzoGFfbP2N|z-hMhPdq+)~(BG!$- zXobJ!3@Q5FHOL0WLS~9_*0`-?C{H#n7E+V~k&n>%WAYXwxql!`_!h49=t>|S`9(e? zCI0rXC_$y2pVJY!Wtkp`KpHC%(to8*_i$zw?9aK-Ya+OAtZS8t71X^n~WEn)L4UPmEc>y;g@ z)qB-No1A*DK&!tZQkzdQ86S2v!O>3n%KCMtw&*`huPDyO9n8;TbQY-l@ zC@RACBOUJ{S_nV@0uUeq&5?s6wG%Vx<{@Er4S{nRamO99EAr17+QCl3AEupi66qkE z98XA*>!tDk-4y#SyBnJZ0uX=z1Rwwb2tWV=5P$##AOL|LC2$SXg$Dp!&2)bMKd%4p zQQH-XfdB*`009U<00Izz00bZa0SH`90VP9;*Z)%*oc~`=R>TDX2tWV=5P$##AOHaf zKmY;|=urYnMkAj8Ycw%pvf_GF+K?CsKmY;|fB*y_009U<00Izz00b_xfH?k-^Z(0? zis&E!0SG_<0uX=z1Rwwb2tWV=Jxl=O|2=GjAUP0#00bZa0SG_<0uX=z1Rwx`A3*@) z|33mMqJ#hhAOHafKmY;|fB*y_009W}FaezZ_pl9uQ8Lf3qb;c@JRU@C}a2e`dRknz)k5l<{FHInh z?-zyi{=cs%_9FY$<;@^sg8&2|009U<00Izz00bZa0SG{#I|Woq((|BDD3uzuMmqoh tT0H;%x;xW^bwdCG5P$##AOHafKmY;|fB*y_aG3=n7@Afolnkv={T~!Rh?W2V literal 0 HcmV?d00001 diff --git a/tests/test_security.py b/tests/test_security.py index 6babfcdb..4448d5db 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -119,12 +119,12 @@ def test_security_wallet_field_encryption_password(self): self.assertNotEqual(encrypted_main_key_private, HDKey(pk).private_byte) def test_security_encrypted_db_incorrect_password(self): - db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.sqlite') + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') self.assertRaisesRegex(EncodingError, "Could not decrypt value \(password incorrect\?\): MAC check failed", Wallet, 'wlt-encryption-test', db_uri=db) def test_security_encrypted_db_no_password(self): - db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.sqlite') + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') if os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY'): self.skipTest("This test only runs when no encryption keys are provided") self.assertRaisesRegex(ValueError, "Data is encrypted please provide key in environment", From 8a4d8175902013e39a240e3b6183ba4a064a6e8d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 10:12:33 +0100 Subject: [PATCH 118/207] Skip pwd test if no pwd in environment --- tests/test_security.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_security.py b/tests/test_security.py index 4448d5db..e4f49d90 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -119,14 +119,16 @@ def test_security_wallet_field_encryption_password(self): self.assertNotEqual(encrypted_main_key_private, HDKey(pk).private_byte) def test_security_encrypted_db_incorrect_password(self): + if not(os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY')): + self.skipTest("This test only runs when no encryption keys are provided") db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') self.assertRaisesRegex(EncodingError, "Could not decrypt value \(password incorrect\?\): MAC check failed", Wallet, 'wlt-encryption-test', db_uri=db) def test_security_encrypted_db_no_password(self): - db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') if os.environ.get('DB_FIELD_ENCRYPTION_PASSWORD') or os.environ.get('DB_FIELD_ENCRYPTION_KEY'): self.skipTest("This test only runs when no encryption keys are provided") + db = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bitcoinlib_encrypted.db') self.assertRaisesRegex(ValueError, "Data is encrypted please provide key in environment", Wallet, 'wlt-encryption-test', db_uri=db) From b329eb273a1b6e161dc638c758bed606a551b308 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 16:30:04 +0100 Subject: [PATCH 119/207] Add documentation for clw and encrypted private data --- bitcoinlib/tools/clw.py | 3 +- docs/_static/manuals.command-line-wallet.rst | 37 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 9245a22d..5caa89f6 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -45,8 +45,7 @@ def parse_args(): parser.add_argument('--database', '-d', help="URI of the database to use",) parser.add_argument('--wallet_name', '-w', nargs='?', default='', - help="Name of wallet to create or open. Provide wallet name or number when running wallet " - "actions") + help="Name of wallet to open. Provide wallet name or number when running wallet actions") parser.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") parser.add_argument('--witness-type', '-j', metavar='WITNESS_TYPE', default=None, diff --git a/docs/_static/manuals.command-line-wallet.rst b/docs/_static/manuals.command-line-wallet.rst index 256aabc6..228a062a 100644 --- a/docs/_static/manuals.command-line-wallet.rst +++ b/docs/_static/manuals.command-line-wallet.rst @@ -95,6 +95,43 @@ addresses and update unspent outputs. The -i / --wallet-info shows the contents of the updated wallet. +Encrypt private key fields +-------------------------- + +Bitcoinlib has build in functionality to encrypt private key fields in the database. If you provide a password in +the runtime environment the data is encrypted at low level in de database module. You can provide a 32 byte key +in the DB_FIELD_ENCRYPTION_KEY variable or a password in the DB_FIELD_ENCRYPTION_PASSWORD variable. + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD=iforgot + $ clw new -w cryptwallet + Command Line Wallet - BitcoinLib 0.6.14 + + CREATE wallet 'cryptwallet' (bitcoin network) + Passphrase: job giant vendor side oil embrace true cushion have matrix glimpse rack + Please write down on paper and backup. With this key you can restore your wallet and all keys + + Type 'yes' if you understood and wrote down your key: yes + ... wallet info ... + + $ clw -w cryptwallet -r + Command Line Wallet - BitcoinLib 0.6.14 + + Receive address: bc1q2cr0chgs6530mdpag2rfn7v9nt232nlpqcc4kc + Install qr code module to show QR codes: pip install pyqrcode + +If we now remove the password from the environment, we cannot open the wallet anymore: + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD= + $ clw -w cryptwallet -i + Command Line Wallet - BitcoinLib 0.6.14 + + ValueError: Data is encrypted please provide key in environment + + Example: Multi-signature Bitcoinlib test wallet ----------------------------------------------- From a143424696426a5f1b425811c5038ca6213bb35f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 21:25:39 +0100 Subject: [PATCH 120/207] Update encrypted field documentation --- docs/_static/manuals.sqlcipher.rst | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/_static/manuals.sqlcipher.rst b/docs/_static/manuals.sqlcipher.rst index 12ecf770..30341480 100644 --- a/docs/_static/manuals.sqlcipher.rst +++ b/docs/_static/manuals.sqlcipher.rst @@ -55,7 +55,9 @@ If you look at the contents of the SQLite database you can see it is encrypted. Encrypt private key fields with AES ----------------------------------- -Enable database encryption in Bitcoinlib configuration settings at ~/.bitcoinlib/config.ini +It is also possible to just encrypt the private keys in the database with secure AES encryption. You need to provide a key or password as environment variable. + +* You can skip this step if you want, but this provides an extra warning / check when no encryption key is found: Enable database encryption in Bitcoinlib configuration settings at ~/.bitcoinlib/config.ini .. code-block:: text @@ -63,7 +65,9 @@ Enable database encryption in Bitcoinlib configuration settings at ~/.bitcoinlib # You need to set the password in the DB_FIELD_ENCRYPTION_KEY environment variable. database_encryption_enabled=True -Now generate a secure 32 bytes encryption key. You can use Bitcoinlib to do this: +You can provide an encryption key directly or use a password to create a key: + +1. Generate a secure 32 bytes encryption key yourself with Bitcoinlib: .. code-block:: python @@ -73,18 +77,26 @@ Now generate a secure 32 bytes encryption key. You can use Bitcoinlib to do this This key needs to be stored in the environment when creating or accessing a wallet. No extra arguments have to be provided to the Wallet class, the data is encrypted an decrypted at database level. +2. You can also just provide a password, and let Bitcoinlib create a key for you. You will need to pass the DB_FIELD_ENCRYPTION_PASSWORD environment variable. + There are several ways to store the key in an Environment variable, on Linux you can do: .. code-block:: bash $ export DB_FIELD_ENCRYPTION_KEY='2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' -And in Windows: +or + +.. code-block:: bash + + $ export DB_FIELD_ENCRYPTION_PASSWORD=ineedtorememberthispassword + +Or in Windows: .. code-block:: bash $ setx DB_FIELD_ENCRYPTION_KEY '2414966ea9f2de189a61953c333f61013505dfbf8e383b5ed6cb1981d5ec2620' -Enviroment variables can also be stored in an .env key, in a virtual enviroment or in Python code itself. However anyone with access to the key can decrypt your private keys. +Environment variables can also be stored in an .env key, in a virtual environment or in Python code itself. However anyone with access to the key can decrypt your private keys. -Please make sure to remember and backup your encryption key, if you loose your key the private keys can not be recovered! \ No newline at end of file +Please make sure to remember and backup your encryption key or password, if you loose your key the private keys can not be recovered! \ No newline at end of file From bcb6b972eaf7cf57dc6acdf70b54bacc363f4510 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 22:18:55 +0100 Subject: [PATCH 121/207] Move config unittest file --- .github/workflows/unittests.yaml | 2 +- tests/config.ini.unittest | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/config.ini.unittest diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index a5363a42..7713cc4c 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -21,7 +21,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest + BCL_CONFIG_FILE: ./config.ini.unittest run: coverage run --source=bitcoinlib -m unittest -v - name: Coveralls diff --git a/tests/config.ini.unittest b/tests/config.ini.unittest new file mode 100644 index 00000000..de8448ce --- /dev/null +++ b/tests/config.ini.unittest @@ -0,0 +1,56 @@ +# +# BITCOINLIB - Configuration file for Unit Testing +# © 2020 March - 1200 Web Development +# +# Paths to data, logs and configuration files, all paths are relative to bitcoinlib source directory if no +# absolute path is provided. +# +# In this configuration file you can overwrite default settings, for a normal installation it is not necessary to +# change anything here. +# + +[locations] +# Location of BitcoinLib data, configuration and log files. Relative paths will be based in installation directory +;data_dir=~/.bitcoinlib + +# Default directory for database files. Relative paths will be based in user or bitcoinlib installation directory. Only used for sqlite files. +;database_dir=database + +# Default database file for wallets, keys and transactions. Relative paths will be based in 'database_dir' +;default_databasefile=bitcoinlib.sqlite +;default_databasefile_cache==bitcoinlib_cache.sqlite + + +[common] +# Allow database threads in SQLite databases +;allow_database_threads=True + +# Time for request to service providers in seconds +timeout_requests=2 + +# Default language for Mnemonic passphrases +;default_language=english + +# Default network when creating wallets, transaction, keys, etc. +;default_network=bitcoin + +# Default witness_type for new wallets and keys +;default_witness_type=legacy + +# Use caching for service providers +;service_caching_enabled=True + +# Encrypt private key field in database using symmetrically EAS encryption. +# You need to set the password in the DB_FIELD_ENCRYPTION_KEY environment variable. +;database_encryption_enabled=False + +[logs] +# Enable own logging for this library. If true logs will be stored in the log/bitcoinlib.log file. +# Set to False if this library is part of another library or software and you want to handle logs yourself. +;enable_bitcoinlib_logging=True + +# Log file name. Relative paths will be based in data_dir directory +;log_file=~bitcoinlib.log + +# Loglevel for this library, options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET +;loglevel=WARNING From dbf6883aeeecab28065038177009b7b2a8fe4453 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 25 Feb 2024 22:49:01 +0100 Subject: [PATCH 122/207] Move all unittest configfiles to unittest dir --- .github/workflows/unittests-mysql.yaml | 2 +- .github/workflows/unittests-noscrypt.yaml | 2 +- .github/workflows/unittests-postgresql.yaml | 2 +- .github/workflows/unittests_windows.yaml | 2 +- bitcoinlib/data/config.ini.unittest | 56 ------------------- .../config_encryption.ini.unittest | 0 6 files changed, 4 insertions(+), 60 deletions(-) delete mode 100644 bitcoinlib/data/config.ini.unittest rename {bitcoinlib/data => tests}/config_encryption.ini.unittest (100%) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index 204bdff9..c1d9c0c4 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -25,6 +25,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest + BCL_CONFIG_FILE: ./config.ini.unittest UNITTEST_DATABASE: mysql run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 0f6ecb1c..8a08520d 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -19,6 +19,6 @@ jobs: pip uninstall -y scrypt - name: Test with coverage env: - BCL_CONFIG_FILE: config_encryption.ini.unittest + BCL_CONFIG_FILE: ./config_encryption.ini.unittest DB_FIELD_ENCRYPTION_KEY: 11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index 5acc34d7..b72248dc 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -34,7 +34,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest + BCL_CONFIG_FILE: ./config.ini.unittest UNITTEST_DATABASE: postgresql DB_FIELD_ENCRYPTION_PASSWORD: verybadpassword run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index 529574ae..df3394ee 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -18,6 +18,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: config.ini.unittest + BCL_CONFIG_FILE: ./config.ini.unittest PYTHONUTF8: 1 run: coverage run --source=bitcoinlib -m unittest -v diff --git a/bitcoinlib/data/config.ini.unittest b/bitcoinlib/data/config.ini.unittest deleted file mode 100644 index de8448ce..00000000 --- a/bitcoinlib/data/config.ini.unittest +++ /dev/null @@ -1,56 +0,0 @@ -# -# BITCOINLIB - Configuration file for Unit Testing -# © 2020 March - 1200 Web Development -# -# Paths to data, logs and configuration files, all paths are relative to bitcoinlib source directory if no -# absolute path is provided. -# -# In this configuration file you can overwrite default settings, for a normal installation it is not necessary to -# change anything here. -# - -[locations] -# Location of BitcoinLib data, configuration and log files. Relative paths will be based in installation directory -;data_dir=~/.bitcoinlib - -# Default directory for database files. Relative paths will be based in user or bitcoinlib installation directory. Only used for sqlite files. -;database_dir=database - -# Default database file for wallets, keys and transactions. Relative paths will be based in 'database_dir' -;default_databasefile=bitcoinlib.sqlite -;default_databasefile_cache==bitcoinlib_cache.sqlite - - -[common] -# Allow database threads in SQLite databases -;allow_database_threads=True - -# Time for request to service providers in seconds -timeout_requests=2 - -# Default language for Mnemonic passphrases -;default_language=english - -# Default network when creating wallets, transaction, keys, etc. -;default_network=bitcoin - -# Default witness_type for new wallets and keys -;default_witness_type=legacy - -# Use caching for service providers -;service_caching_enabled=True - -# Encrypt private key field in database using symmetrically EAS encryption. -# You need to set the password in the DB_FIELD_ENCRYPTION_KEY environment variable. -;database_encryption_enabled=False - -[logs] -# Enable own logging for this library. If true logs will be stored in the log/bitcoinlib.log file. -# Set to False if this library is part of another library or software and you want to handle logs yourself. -;enable_bitcoinlib_logging=True - -# Log file name. Relative paths will be based in data_dir directory -;log_file=~bitcoinlib.log - -# Loglevel for this library, options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET -;loglevel=WARNING diff --git a/bitcoinlib/data/config_encryption.ini.unittest b/tests/config_encryption.ini.unittest similarity index 100% rename from bitcoinlib/data/config_encryption.ini.unittest rename to tests/config_encryption.ini.unittest From 0f60ee8b6341ece07232a8fdaa8bf1bc94b1912e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 26 Feb 2024 13:19:04 +0100 Subject: [PATCH 123/207] Change config.ini reference --- .github/workflows/unittests-mysql.yaml | 2 +- .github/workflows/unittests-noscrypt.yaml | 2 +- .github/workflows/unittests-postgresql.yaml | 2 +- .github/workflows/unittests.yaml | 2 +- .github/workflows/unittests_windows.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index c1d9c0c4..cbc846e2 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -25,6 +25,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ./config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest UNITTEST_DATABASE: mysql run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 8a08520d..75c3ba40 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -19,6 +19,6 @@ jobs: pip uninstall -y scrypt - name: Test with coverage env: - BCL_CONFIG_FILE: ./config_encryption.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/config_encryption.ini.unittest DB_FIELD_ENCRYPTION_KEY: 11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index b72248dc..7358f14a 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -34,7 +34,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ./config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest UNITTEST_DATABASE: postgresql DB_FIELD_ENCRYPTION_PASSWORD: verybadpassword run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 7713cc4c..fc5de204 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -21,7 +21,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ./config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest run: coverage run --source=bitcoinlib -m unittest -v - name: Coveralls diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index df3394ee..473844e8 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -18,6 +18,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ./config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest PYTHONUTF8: 1 run: coverage run --source=bitcoinlib -m unittest -v From f6aa339e185685cbec7d8fce861f9bf113b9026d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 26 Feb 2024 13:26:48 +0100 Subject: [PATCH 124/207] Fix config.ini reference --- .github/workflows/unittests-mysql.yaml | 2 +- .github/workflows/unittests-noscrypt.yaml | 2 +- .github/workflows/unittests-postgresql.yaml | 2 +- .github/workflows/unittests.yaml | 2 +- .github/workflows/unittests_windows.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unittests-mysql.yaml b/.github/workflows/unittests-mysql.yaml index cbc846e2..c4b1e807 100644 --- a/.github/workflows/unittests-mysql.yaml +++ b/.github/workflows/unittests-mysql.yaml @@ -25,6 +25,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest UNITTEST_DATABASE: mysql run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-noscrypt.yaml b/.github/workflows/unittests-noscrypt.yaml index 75c3ba40..2e3f6320 100644 --- a/.github/workflows/unittests-noscrypt.yaml +++ b/.github/workflows/unittests-noscrypt.yaml @@ -19,6 +19,6 @@ jobs: pip uninstall -y scrypt - name: Test with coverage env: - BCL_CONFIG_FILE: ${{ github.workspace }}/config_encryption.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config_encryption.ini.unittest DB_FIELD_ENCRYPTION_KEY: 11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests-postgresql.yaml b/.github/workflows/unittests-postgresql.yaml index 7358f14a..2ed01b27 100644 --- a/.github/workflows/unittests-postgresql.yaml +++ b/.github/workflows/unittests-postgresql.yaml @@ -34,7 +34,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest UNITTEST_DATABASE: postgresql DB_FIELD_ENCRYPTION_PASSWORD: verybadpassword run: coverage run --source=bitcoinlib -m unittest -v diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index fc5de204..a02df66a 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -21,7 +21,7 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest run: coverage run --source=bitcoinlib -m unittest -v - name: Coveralls diff --git a/.github/workflows/unittests_windows.yaml b/.github/workflows/unittests_windows.yaml index 473844e8..f3c5a2fa 100644 --- a/.github/workflows/unittests_windows.yaml +++ b/.github/workflows/unittests_windows.yaml @@ -18,6 +18,6 @@ jobs: python -m pip install .[dev] - name: Test with coverage env: - BCL_CONFIG_FILE: ${{ github.workspace }}/config.ini.unittest + BCL_CONFIG_FILE: ${{ github.workspace }}/tests/config.ini.unittest PYTHONUTF8: 1 run: coverage run --source=bitcoinlib -m unittest -v From 8017a40cbf631fcb9867acf629f03d45b8fa3e18 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 26 Feb 2024 14:59:49 +0100 Subject: [PATCH 125/207] Remove duplicate queries in transaction_update method --- bitcoinlib/wallets.py | 40 +++++++++++++++++++++++++--------------- tests/test_services.py | 12 +++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index c5ce5abc..a9f75a4f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3196,11 +3196,16 @@ def transactions_update_confirmations(self): network = self.network.name srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() - db_txs = self.session.query(DbTransaction). \ + # db_txs = self.session.query(DbTransaction). \ + # filter(DbTransaction.wallet_id == self.wallet_id, + # DbTransaction.network_name == network, DbTransaction.block_height > 0).all() + # for db_tx in db_txs: + # self.session.query(DbTransaction).filter_by(id=db_tx.id). \ + # update({DbTransaction.status: 'confirmed', + # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) + self.session.query(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, - DbTransaction.network_name == network, DbTransaction.block_height > 0).all() - for db_tx in db_txs: - self.session.query(DbTransaction).filter_by(id=db_tx.id). \ + DbTransaction.network_name == network, DbTransaction.block_height > 0).\ update({DbTransaction.status: 'confirmed', DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self._commit() @@ -3272,19 +3277,24 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N depth = self.key_depth # Update number of confirmations and status for already known transactions - if not key_id: - self.transactions_update_confirmations() + # if not key_id: + self.transactions_update_confirmations() srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) - blockcount = srv.blockcount() - db_txs = self.session.query(DbTransaction).\ - filter(DbTransaction.wallet_id == self.wallet_id, - DbTransaction.network_name == network, DbTransaction.block_height > 0).all() - for db_tx in db_txs: - self.session.query(DbTransaction).filter_by(id=db_tx.id).\ - update({DbTransaction.status: 'confirmed', - DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) - self._commit() + # blockcount = srv.blockcount() + # db_txs = self.session.query(DbTransaction).\ + # filter(DbTransaction.wallet_id == self.wallet_id, + # DbTransaction.network_name == network, DbTransaction.block_height > 0).all() + # for db_tx in db_txs: + # self.session.query(DbTransaction).filter_by(id=db_tx.id).\ + # update({DbTransaction.status: 'confirmed', + # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) + # self.session.query(DbTransaction).\ + # filter(DbTransaction.wallet_id == self.wallet_id, + # DbTransaction.network_name == network, DbTransaction.block_height > 0).\ + # update({DbTransaction.status: 'confirmed', + # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) + # self._commit() # Get transactions for wallet's addresses txs = [] diff --git a/tests/test_services.py b/tests/test_services.py index b6a23817..1967c172 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -303,15 +303,9 @@ def test_service_gettransactions_after_txid(self): def test_service_gettransactions_after_txid_segwit(self): res = ServiceTest(timeout=TIMEOUT_TEST, exclude_providers=['blockcypher']).\ - gettransactions('bc1q34aq5drpuwy3wgl9lhup9892qp6svr8ldzyy7c', - after_txid='f91d0a8a78462bc59398f2c5d7a84fcff491c26ba54c4833478b202796c8aafd') - tx_ids = [ - '9e914f4438cdfd2681bf5fb0b3dea8206fffcc48d1ca7e0f05f7b77c76115803', - 'a4bc261faf9ca47722760c9f9f075ab974c7351d8da7b0b5e5a316b3aa7aefa2', - '04be18177781f8060d63390a705cf89ffed2252a3506fab69be7079bc7ba9410'] - self.assertIn(res[0].txid, tx_ids) - self.assertIn(res[1].txid, tx_ids) - self.assertIn(res[2].txid, tx_ids) + gettransactions('bc1qj9hlju59t0m4389033r2x8mlxwc86qgqm9flm626sd22cdhfs9jsyrrp6q', + after_txid='bd430d52f35166a7dd6251c73a48559ad8b5f41b6c5bc4a6c4c1a3e3702f4287') + self.assertEqual(res[0].txid, 'cab75da6d7fe1531c881d4efdb4826410a2604aa9e6442ab12a08363f34fb408') def test_service_gettransactions_after_txid_litecoin(self): res = ServiceTest('litecoin').gettransactions( From ccd035107537a9b82f40d55da3a240eee7f4030e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 26 Feb 2024 15:42:16 +0100 Subject: [PATCH 126/207] Remove old code --- bitcoinlib/wallets.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index a9f75a4f..4f77571d 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3196,13 +3196,6 @@ def transactions_update_confirmations(self): network = self.network.name srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() - # db_txs = self.session.query(DbTransaction). \ - # filter(DbTransaction.wallet_id == self.wallet_id, - # DbTransaction.network_name == network, DbTransaction.block_height > 0).all() - # for db_tx in db_txs: - # self.session.query(DbTransaction).filter_by(id=db_tx.id). \ - # update({DbTransaction.status: 'confirmed', - # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) self.session.query(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, DbTransaction.network_name == network, DbTransaction.block_height > 0).\ @@ -3277,24 +3270,9 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N depth = self.key_depth # Update number of confirmations and status for already known transactions - # if not key_id: self.transactions_update_confirmations() srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) - # blockcount = srv.blockcount() - # db_txs = self.session.query(DbTransaction).\ - # filter(DbTransaction.wallet_id == self.wallet_id, - # DbTransaction.network_name == network, DbTransaction.block_height > 0).all() - # for db_tx in db_txs: - # self.session.query(DbTransaction).filter_by(id=db_tx.id).\ - # update({DbTransaction.status: 'confirmed', - # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) - # self.session.query(DbTransaction).\ - # filter(DbTransaction.wallet_id == self.wallet_id, - # DbTransaction.network_name == network, DbTransaction.block_height > 0).\ - # update({DbTransaction.status: 'confirmed', - # DbTransaction.confirmations: (blockcount - DbTransaction.block_height) + 1}) - # self._commit() # Get transactions for wallet's addresses txs = [] From 8553404a7ceb7ba04823cc37c0d5832492beeb10 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 1 Mar 2024 11:33:37 +0100 Subject: [PATCH 127/207] Add method to create intermediate passwords for EC multiplied BIP38 keys --- bitcoinlib/config/config.py | 4 +++ bitcoinlib/encoding.py | 30 +++++++++++++++++------ bitcoinlib/keys.py | 49 +++++++++++++++++++++++++++++++++++++ tests/test_keys.py | 10 +++++++- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 53d61092..38c2252c 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -102,6 +102,10 @@ # Mnemonics DEFAULT_LANGUAGE = 'english' +# BIP38 +BIP38_MAGIC_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e251 +BIP38_MAGIC_NO_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e253 + # Networks DEFAULT_NETWORK = 'bitcoin' NETWORK_DENOMINATORS = { # source: https://en.bitcoin.it/wiki/Units, https://en.wikipedia.org/wiki/Metric_prefix diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index ea7922dc..dbb2380a 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -987,10 +987,12 @@ def bip38_decrypt(encrypted_privkey, password): password = password.encode('utf-8') addresshash = d[0:4] d = d[4:-4] - try: - key = scrypt(password, addresshash, 64, 16384, 8, 8) - except Exception: - key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) + + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) + # try: + # key = scrypt(password, addresshash, 64, 16384, 8, 8) + # except Exception: + # key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) derivedhalf1 = key[0:32] derivedhalf2 = key[32:64] encryptedhalf1 = d[0:16] @@ -1028,10 +1030,7 @@ def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): if isinstance(password, str): password = password.encode('utf-8') addresshash = double_sha256(address)[0:4] - try: - key = scrypt(password, addresshash, 64, 16384, 8, 8) - except Exception: - key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) derivedhalf1 = key[0:32] derivedhalf2 = key[32:64] aes = AES.new(derivedhalf2, AES.MODE_ECB) @@ -1045,6 +1044,21 @@ def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): return base58encode(encrypted_privkey) +def scrypt_hash(password, salt, key_len=64, N=16384, r=8, p=1, buflen=64): + """ + Wrapper for Scrypt method for scrypt or Cryptodome library + + For documentation see methods in referring libraries + + """ + try: # Try scrypt from Cryptodome + key = scrypt(password, salt, key_len, N, r, p, buflen // key_len) + except TypeError: # Use scrypt module + key = scrypt.hash(password, salt, N, r, p, key_len) + return key + + + class Quantity: """ Class to convert very large or very small numbers to a readable format. diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 278bc224..f5078385 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -452,6 +452,55 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig return npath +def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt=os.urandom(8)): + """ + Intermediate passphrase generator for EC multiplied BIP38 encrypted private keys. + Source: https://github.com/meherett/python-bip38/blob/master/bip38/bip38.py + + :param passphrase: Passphrase or password text + :type passphrase: str + :param lot: Lot number between 100000 <= lot <= 999999 range, default to ``None`` + :type lot: int + :param sequence: Sequence number between 0 <= sequence <= 4095 range, default to ``None`` + :type sequence: int + :param owner_salt: Owner salt, default to ``os.urandom(8)`` + :type owner_salt: str, bytes + + :returns: str -- Intermediate passphrase + + >>> bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1, owner_salt="75ed1cdeb254cb38") + 'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ' + + """ + + owner_salt = to_bytes(owner_salt) + if len(owner_salt) not in [4, 8]: + raise ValueError(f"Invalid owner salt length (expected: 4 or 8 bytes, got: {len(owner_salt)})") + if len(owner_salt) == 4 and (not lot or not sequence): + raise ValueError(f"Invalid owner salt length for non lot/sequence (expected: 8 bytes, got:" + f" {len(owner_salt)})") + if (lot and not sequence) or (not lot and sequence): + raise ValueError(f"Both lot & sequence are required, got: (lot {lot}) (sequence {sequence})") + + if lot and sequence: + lot, sequence = int(lot), int(sequence) + if not 100000 <= lot <= 999999: + raise ValueError(f"Invalid lot, (expected: 100000 <= lot <= 999999, got: {lot})") + if not 0 <= sequence <= 4095: + raise ValueError(f"Invalid lot, (expected: 0 <= sequence <= 4095, got: {sequence})") + + pre_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt[:4], 32, 16384, 8, 8) + owner_entropy = owner_salt[:4] + int.to_bytes((lot * 4096 + sequence), 4, 'big') + pass_factor = double_sha256(pre_factor + owner_entropy) + magic = int.to_bytes(BIP38_MAGIC_LOT_AND_SEQUENCE, 8, 'big') + else: + pass_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt, 32, 16384, 8, 8) + magic = int.to_bytes(BIP38_MAGIC_NO_LOT_AND_SEQUENCE, 8, 'big') + owner_entropy = owner_salt + + return pubkeyhash_to_addr_base58(magic + owner_entropy + HDKey(pass_factor).public_byte, prefix=b'') + + class Address(object): """ Class to store, convert and analyse various address types as representation of public keys or scripts hashes diff --git a/tests/test_keys.py b/tests/test_keys.py index e5efb0a8..e2a84337 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -26,7 +26,7 @@ # Number of bulktests for generation of private, public keys and HDKeys. Set to 0 to disable # WARNING: Can be slow for a larger number of tests -BULKTESTCOUNT = 100 +BULKTESTCOUNT = 250 class TestKeyClasses(unittest.TestCase): @@ -600,6 +600,14 @@ def test_bip38_hdkey_method(self): k = HDKey(pkwif, witness_type='legacy') self.assertEqual(k.encrypt('Satoshi'), bip38_wif) + def test_bip38_intermediate_password(self): + password1 = 'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ' + intpwd1 = bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1, + owner_salt="75ed1cdeb254cb38") + self.assertEqual(password1, intpwd1) + self.assertEqual(bip38_intermediate_password(passphrase="TestingOneTwoThree")[:10], 'passphrase') + + class TestKeysBulk(unittest.TestCase): """ From 5d75a0a419a4ec671735f58782edecdc6e002715 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 1 Mar 2024 13:57:20 +0100 Subject: [PATCH 128/207] wft is scrypt doing --- bitcoinlib/encoding.py | 2 +- bitcoinlib/keys.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index dbb2380a..823a1594 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -1052,7 +1052,7 @@ def scrypt_hash(password, salt, key_len=64, N=16384, r=8, p=1, buflen=64): """ try: # Try scrypt from Cryptodome - key = scrypt(password, salt, key_len, N, r, p, buflen // key_len) + key = scrypt(password, salt, key_len, N, r, p) except TypeError: # Use scrypt module key = scrypt.hash(password, salt, N, r, p, key_len) return key diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index f5078385..a42da969 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -491,6 +491,10 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= pre_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt[:4], 32, 16384, 8, 8) owner_entropy = owner_salt[:4] + int.to_bytes((lot * 4096 + sequence), 4, 'big') + if isinstance(pre_factor, list): + for pf in pre_factor: + print(pf.hex()) + print(len(pre_factor)) pass_factor = double_sha256(pre_factor + owner_entropy) magic = int.to_bytes(BIP38_MAGIC_LOT_AND_SEQUENCE, 8, 'big') else: From 48a5b19e26c293f3816ae2fb69bcd21ad6abe3a3 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 2 Mar 2024 09:35:47 +0100 Subject: [PATCH 129/207] Working to add EC multiplied keys --- bitcoinlib/config/config.py | 8 ++- bitcoinlib/encoding.py | 2 +- bitcoinlib/keys.py | 102 +++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 38c2252c..3745d054 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -103,8 +103,12 @@ DEFAULT_LANGUAGE = 'english' # BIP38 -BIP38_MAGIC_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e251 -BIP38_MAGIC_NO_LOT_AND_SEQUENCE: int = 0x2ce9b3e1ff39e253 +BIP38_MAGIC_LOT_AND_SEQUENCE = b'\x2c\xe9\xb3\xe1\xff\x39\xe2\x51' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE = b'\x2c\xe9\xb3\xe1\xff\x39\xe2\x53' +BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG = b'\x04' +BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x24' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG = b'\x00' +BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x20' # Networks DEFAULT_NETWORK = 'bitcoin' diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index 823a1594..6ce8aa1c 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -521,7 +521,7 @@ def addr_base58_to_pubkeyhash(address, as_hex=False): >>> addr_base58_to_pubkeyhash('142Zp9WZn9Fh4MV8F3H5Dv4Rbg7Ja1sPWZ', as_hex=True) '21342f229392d7c9ed82c932916cee6517fbc9a2' - :param address: Crypto currency address in base-58 format + :param address: Cryptocurrency address in base-58 format :type address: str, bytes :param as_hex: Output as hexstring :type as_hex: bool diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index a42da969..807a02bc 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -496,15 +496,113 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= print(pf.hex()) print(len(pre_factor)) pass_factor = double_sha256(pre_factor + owner_entropy) - magic = int.to_bytes(BIP38_MAGIC_LOT_AND_SEQUENCE, 8, 'big') + magic = BIP38_MAGIC_LOT_AND_SEQUENCE else: pass_factor = scrypt_hash(unicodedata.normalize("NFC", passphrase), owner_salt, 32, 16384, 8, 8) - magic = int.to_bytes(BIP38_MAGIC_NO_LOT_AND_SEQUENCE, 8, 'big') + magic = BIP38_MAGIC_NO_LOT_AND_SEQUENCE owner_entropy = owner_salt return pubkeyhash_to_addr_base58(magic + owner_entropy + HDKey(pass_factor).public_byte, prefix=b'') +def bip38_create_new_encrypted_wif(intermediate_passphrase, public_key_type="compressed", seed=os.urandom(24), + network=DEFAULT_NETWORK): + """ + Create new encrypted WIF (Wallet Important Format) + + :param intermediate_passphrase: Intermediate passphrase text + :type intermediate_passphrase: str + :param public_key_type: Public key type, default to ``uncompressed`` + :type public_key_type: Literal["uncompressed", "compressed"] + :param seed: Seed, default to ``os.urandom(24)`` + :type seed: Optional[str, bytes] + :param network: Network type + :type network: Literal["mainnet", "testnet"], default to ``mainnet`` + + :returns: dict -- Encrypted WIF (Wallet Important Format) + + """ + + seed_b = to_bytes(seed) + intermediate_password_bytes = change_base(intermediate_passphrase,58, 256) + check = intermediate_password_bytes[-4:] + intermediate_decode = intermediate_password_bytes[:-4] + checksum = double_sha256(intermediate_decode)[0:4] + assert (check == checksum), "Invalid address, checksum incorrect" + if len(intermediate_decode) != 49: + raise ValueError(f"Invalid intermediate passphrase length (expected: 49, got: {len(intermediate_decode)})") + + magic: bytes = intermediate_decode[:8] + owner_entropy: bytes = intermediate_decode[8:16] + pass_point: bytes = intermediate_decode[16:] + + if magic == BIP38_MAGIC_LOT_AND_SEQUENCE: + if public_key_type == "uncompressed": + flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG + elif public_key_type == "compressed": + flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG + else: + raise ValueError(f"Invalid public key type (expected: 'uncompressed' or 'compressed', got: {public_key_type})") + elif magic == BIP38_MAGIC_NO_LOT_AND_SEQUENCE: + if public_key_type == "uncompressed": + flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG + elif public_key_type == "compressed": + flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG + else: + raise ValueError(f"Invalid public key type (expected: 'uncompressed' or 'compressed', got: {public_key_type})") + else: + raise ValueError("Invalid magic bytes, check BIP38 constants") + + factor_b: bytes = double_sha256(seed_b) + if not 0 < int.from_bytes(factor_b, 'big') < secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + + public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type) + address: str = public_key_to_addresses(public_key=public_key, network=network) + address_hash: bytes = get_checksum(get_bytes(address, unhexlify=False)) + salt: bytes = address_hash + owner_entropy + scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64) + derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:] + + aes: AESModeOfOperationECB = AESModeOfOperationECB(key) + encrypted_half_1: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1) + )) + encrypted_half_2: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2) + )) + encrypted_wif: str = ensure_string(check_encode(( + integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2 + ))) + + point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed")) + point_b_prefix: bytes = integer_to_bytes( + (bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1]) + ) + point_b_half_1: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1) + )) + point_b_half_2: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2) + )) + encrypted_point_b: bytes = ( + point_b_prefix + point_b_half_1 + point_b_half_2 + ) + confirmation_code: str = ensure_string(check_encode(( + integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b + ))) + + return dict( + encrypted_wif=encrypted_wif, + confirmation_code=confirmation_code, + public_key=bytes_to_string(public_key), + seed=bytes_to_string(seed_b), + public_key_type=public_key_type, + address=address + ) + + + class Address(object): """ Class to store, convert and analyse various address types as representation of public keys or scripts hashes From 5c6b2cce3cded347fdd900f0c94f4defc506428e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 2 Mar 2024 21:52:06 +0100 Subject: [PATCH 130/207] Inverse of uncompressed public key does not exist --- bitcoinlib/keys.py | 9 ++++++--- tests/test_keys.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 5bb920e5..352fdb72 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1680,12 +1680,15 @@ def inverse(self): :return Key: """ if self.is_private: - return HDKey(secp256k1_n - self.secret, network=self.network, compressed=self.compressed, + return HDKey(secp256k1_n - self.secret, network=self.network.name, compressed=self.compressed, witness_type=self.witness_type, multisig=self.multisig, encoding=self.encoding) else: # Inverse y in init: self._y = secp256k1_p - self._y - return HDKey(('02' if self._y % 2 else '03') + self.x_hex, network=self.network, compressed=self.compressed, - witness_type=self.witness_type, multisig=self.multisig, encoding=self.encoding) + if not self.compressed: + return self + return HDKey(('02' if self._y % 2 else '03') + self.x_hex, network=self.network.name, + compressed=self.compressed, witness_type=self.witness_type, multisig=self.multisig, + encoding=self.encoding) def info(self): """ diff --git a/tests/test_keys.py b/tests/test_keys.py index fac43ea1..b21219ca 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -94,6 +94,18 @@ def test_keys_inverse2(self): self.assertEqual((-k).address(), pub_k.inverse().address()) self.assertEqual((-k).address(), k.inverse().address()) + pkwif = 'Mtpv7L6Q8tPadPv8iUDKAXk1wyCmdJ6q2y2d3AixyoGVMH3WeoCDwkLbpUBXXB5HHbueeqTikkeBGTBV7tCcgJtEfm1wCt4ZcQixz7TtV5CAXfd' + k = HDKey(pkwif, network='litecoin', compressed=False, witness_type='p2sh-segwit') + pub_k = k.public() + self.assertEqual(pub_k, pub_k.inverse()) + + k = HDKey(pkwif, network='litecoin', witness_type='p2sh-segwit') + pub_k = k.public() + kpi = pub_k.inverse() + self.assertEqual(kpi.address(), "MQVYsZ5o5uhN2X6QMbu9RVu5YADiq859MY") + self.assertEqual(kpi.witness_type, 'p2sh-segwit') + self.assertEqual(kpi.network.name, 'litecoin') + def test_dict_and_json_outputs(self): k = HDKey() k.address(script_type='p2wsh', encoding='bech32') From b6054ef39f25efcce2a2776bf7057f0acee4d9b0 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 3 Mar 2024 17:41:51 +0100 Subject: [PATCH 131/207] Add EC point multiplication --- bitcoinlib/keys.py | 124 ++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 352fdb72..f61612ff 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -37,7 +37,6 @@ from fastecdsa import point as fastecdsa_point else: import ecdsa - secp256k1_curve = ecdsa.ellipticcurve.CurveFp(secp256k1_p, secp256k1_a, secp256k1_b) secp256k1_generator = ecdsa.ellipticcurve.Point(secp256k1_curve, secp256k1_Gx, secp256k1_Gy, secp256k1_n) @@ -508,15 +507,15 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= return pubkeyhash_to_addr_base58(magic + owner_entropy + HDKey(pass_factor).public_byte, prefix=b'') -def bip38_create_new_encrypted_wif(intermediate_passphrase, public_key_type="compressed", seed=os.urandom(24), +def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, seed=os.urandom(24), network=DEFAULT_NETWORK): """ Create new encrypted WIF (Wallet Important Format) :param intermediate_passphrase: Intermediate passphrase text :type intermediate_passphrase: str - :param public_key_type: Public key type, default to ``uncompressed`` - :type public_key_type: Literal["uncompressed", "compressed"] + :param compressed: Compressed key + :type compressed: boolean :param seed: Seed, default to ``os.urandom(24)`` :type seed: Optional[str, bytes] :param network: Network type @@ -540,19 +539,15 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, public_key_type="com pass_point: bytes = intermediate_decode[16:] if magic == BIP38_MAGIC_LOT_AND_SEQUENCE: - if public_key_type == "uncompressed": - flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG - elif public_key_type == "compressed": + if compressed: flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG else: - raise ValueError(f"Invalid public key type (expected: 'uncompressed' or 'compressed', got: {public_key_type})") + flag: bytes = BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG elif magic == BIP38_MAGIC_NO_LOT_AND_SEQUENCE: - if public_key_type == "uncompressed": - flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG - elif public_key_type == "compressed": + if compressed: flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG else: - raise ValueError(f"Invalid public key type (expected: 'uncompressed' or 'compressed', got: {public_key_type})") + flag: bytes = BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG else: raise ValueError("Invalid magic bytes, check BIP38 constants") @@ -560,49 +555,52 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, public_key_type="com if not 0 < int.from_bytes(factor_b, 'big') < secp256k1_n: raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") - # public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type) + pk_point = ec_point_multiplication(HDKey(pass_point).public_point(), int.from_bytes(factor_b, 'big')) + k = HDKey((pk_point.x, pk_point.y),compressed=compressed) + public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type) # address: str = public_key_to_addresses(public_key=public_key, network=network) # address_hash: bytes = get_checksum(get_bytes(address, unhexlify=False)) - # salt: bytes = address_hash + owner_entropy - # scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64) - # derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:] - # - # aes: AESModeOfOperationECB = AESModeOfOperationECB(key) - # encrypted_half_1: bytes = aes.encrypt(integer_to_bytes( - # bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1) - # )) - # encrypted_half_2: bytes = aes.encrypt(integer_to_bytes( - # bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2) - # )) - # encrypted_wif: str = ensure_string(check_encode(( - # integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2 - # ))) - # - # point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed")) - # point_b_prefix: bytes = integer_to_bytes( - # (bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1]) - # ) - # point_b_half_1: bytes = aes.encrypt(integer_to_bytes( - # bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1) - # )) - # point_b_half_2: bytes = aes.encrypt(integer_to_bytes( - # bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2) - # )) - # encrypted_point_b: bytes = ( - # point_b_prefix + point_b_half_1 + point_b_half_2 - # ) - # confirmation_code: str = ensure_string(check_encode(( - # integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b - # ))) - # - # return dict( - # encrypted_wif=encrypted_wif, - # confirmation_code=confirmation_code, - # public_key=bytes_to_string(public_key), - # seed=bytes_to_string(seed_b), - # public_key_type=public_key_type, - # address=address - # ) + + salt: bytes = address_hash + owner_entropy + scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64) + derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:] + + aes: AESModeOfOperationECB = AESModeOfOperationECB(key) + encrypted_half_1: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1) + )) + encrypted_half_2: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2) + )) + encrypted_wif: str = ensure_string(check_encode(( + integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2 + ))) + + point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed")) + point_b_prefix: bytes = integer_to_bytes( + (bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1]) + ) + point_b_half_1: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1) + )) + point_b_half_2: bytes = aes.encrypt(integer_to_bytes( + bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2) + )) + encrypted_point_b: bytes = ( + point_b_prefix + point_b_half_1 + point_b_half_2 + ) + confirmation_code: str = ensure_string(check_encode(( + integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b + ))) + + return dict( + encrypted_wif=encrypted_wif, + confirmation_code=confirmation_code, + public_key=bytes_to_string(public_key), + seed=bytes_to_string(seed_b), + public_key_type=public_key_type, + address=address + ) @@ -2572,6 +2570,26 @@ def ec_point(m): return point +def ec_point_multiplication(p, m): + """ + Method for elliptic curve multiplication on the secp256k1 curve. Multiply Generator point G by m + + :param m: A scalar multiplier + :type m: int + + :return Point: Generator point G multiplied by m + """ + m = int(m) + if USE_FASTECDSA: + point = fastecdsa_point.Point(p[0], p[1], fastecdsa_secp256k1) + return point * m + else: + raise NotImplementedError + # point = secp256k1_generator + # point *= m + # return point + + def mod_sqrt(a): """ Compute the square root of 'a' using the secp256k1 'bitcoin' curve From 7b1c01607f5231213ec0c26415ea91efeecdd6e4 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 6 Mar 2024 14:25:55 +0100 Subject: [PATCH 132/207] Add BIP38 ec multiplied keys --- bitcoinlib/config/config.py | 3 ++ bitcoinlib/keys.py | 68 +++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 3745d054..9ca751a7 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -109,6 +109,9 @@ BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x24' BIP38_MAGIC_NO_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG = b'\x00' BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG = b'\x20' +BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX = b'\x01\x42' +BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX = b'\x01\x43' +BIP38_CONFIRMATION_CODE_PREFIX = b'\x64\x3b\xf6\xa8\x9a' # Networks DEFAULT_NETWORK = 'bitcoin' diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index f61612ff..8b7bedf2 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -295,6 +295,7 @@ def deserialize_address(address, encoding=None, network=None): 'script_type': script_type, 'witness_type': witness_type, 'networks': networks, + 'checksum': checksum, 'witver': None, } if encoding == 'bech32' or encoding is None: @@ -319,6 +320,7 @@ def deserialize_address(address, encoding=None, network=None): 'script_type': script_type, 'witness_type': witness_type, 'networks': networks, + 'checksum': addr_bech32_checksum(address), 'witver': witver, } except EncodingError as err: @@ -556,49 +558,41 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, see raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") pk_point = ec_point_multiplication(HDKey(pass_point).public_point(), int.from_bytes(factor_b, 'big')) - k = HDKey((pk_point.x, pk_point.y),compressed=compressed) - public_key: bytes = multiply_public_key(pass_point, factor_b, public_key_type) - # address: str = public_key_to_addresses(public_key=public_key, network=network) - # address_hash: bytes = get_checksum(get_bytes(address, unhexlify=False)) + k = HDKey((pk_point.x, pk_point.y), compressed=compressed, witness_type='legacy') + public_key = k.public_hex + address = k.address() + address_hash = double_sha256(bytes(address, 'utf8'))[:4] salt: bytes = address_hash + owner_entropy - scrypt_hash: bytes = scrypt.hash(pass_point, salt, 1024, 1, 1, 64) - derived_half_1, derived_half_2, key = scrypt_hash[:16], scrypt_hash[16:32], scrypt_hash[32:] - - aes: AESModeOfOperationECB = AESModeOfOperationECB(key) - encrypted_half_1: bytes = aes.encrypt(integer_to_bytes( - bytes_to_integer(seed_b[:16]) ^ bytes_to_integer(derived_half_1) - )) - encrypted_half_2: bytes = aes.encrypt(integer_to_bytes( - bytes_to_integer(encrypted_half_1[8:] + seed_b[16:]) ^ bytes_to_integer(derived_half_2) - )) - encrypted_wif: str = ensure_string(check_encode(( - integer_to_bytes(BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + flag + address_hash + owner_entropy + encrypted_half_1[:8] + encrypted_half_2 - ))) - - point_b: bytes = get_bytes(private_key_to_public_key(factor_b, public_key_type="compressed")) - point_b_prefix: bytes = integer_to_bytes( - (bytes_to_integer(scrypt_hash[63:]) & 1) ^ bytes_to_integer(point_b[:1]) - ) - point_b_half_1: bytes = aes.encrypt(integer_to_bytes( - bytes_to_integer(point_b[1:17]) ^ bytes_to_integer(derived_half_1) - )) - point_b_half_2: bytes = aes.encrypt(integer_to_bytes( - bytes_to_integer(point_b[17:]) ^ bytes_to_integer(derived_half_2) - )) - encrypted_point_b: bytes = ( - point_b_prefix + point_b_half_1 + point_b_half_2 - ) - confirmation_code: str = ensure_string(check_encode(( - integer_to_bytes(CONFIRMATION_CODE_PREFIX) + flag + address_hash + owner_entropy + encrypted_point_b - ))) + scrypt_hash_bytes: bytes = scrypt_hash(pass_point, salt, 64, 1024, 1, 1) + derived_half_1, derived_half_2, key = scrypt_hash_bytes[:16], scrypt_hash_bytes[16:32], scrypt_hash_bytes[32:] + + aes = AES.new(key, AES.MODE_ECB) + encrypted_half_1 = \ + aes.encrypt((int.from_bytes(seed_b[:16], 'big') ^ int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) + encrypted_half_2 = \ + aes.encrypt((int.from_bytes((encrypted_half_1[8:] + seed_b[16:]), 'big') ^ + int.from_bytes(derived_half_2,'big')).to_bytes(16, 'big')) + encrypted_wif = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_half_1[:8] + + encrypted_half_2, prefix=BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + + point_b = HDKey(factor_b).public_byte + point_b_prefix = (int.from_bytes(scrypt_hash_bytes[63:], 'big') & 1 ^ + int.from_bytes(point_b[:1], 'big')).to_bytes(1, 'big') + point_b_half_1 = aes.encrypt((int.from_bytes(point_b[1:17], 'big') ^ + int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) + point_b_half_2 = aes.encrypt((int.from_bytes(point_b[17:], 'big') ^ + int.from_bytes(derived_half_2, 'big')).to_bytes(16, 'big')) + encrypted_point_b = point_b_prefix + point_b_half_1 + point_b_half_2 + confirmation_code = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_point_b, + prefix=BIP38_CONFIRMATION_CODE_PREFIX) return dict( encrypted_wif=encrypted_wif, confirmation_code=confirmation_code, - public_key=bytes_to_string(public_key), - seed=bytes_to_string(seed_b), - public_key_type=public_key_type, + public_key=public_key, + seed=seed_b, + compressed=compressed, address=address ) From b652ee2f416dd01f594021651e3c4f7e678ce82d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 7 Mar 2024 22:56:59 +0100 Subject: [PATCH 133/207] Add method to decrypt ec multiplied bip38 keys --- bitcoinlib/encoding.py | 91 - bitcoinlib/keys.py | 193 ++ index.html | 5574 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 5767 insertions(+), 91 deletions(-) create mode 100644 index.html diff --git a/bitcoinlib/encoding.py b/bitcoinlib/encoding.py index 5281c838..d6b84b61 100644 --- a/bitcoinlib/encoding.py +++ b/bitcoinlib/encoding.py @@ -970,97 +970,6 @@ def aes_decrypt(encrypted_data, key): return res -def bip38_decrypt(encrypted_privkey, password): - """ - BIP0038 non-ec-multiply decryption. Returns WIF private key. - Based on code from https://github.com/nomorecoin/python-bip38-testing - This method is called by Key class init function when importing BIP0038 key. - - :param encrypted_privkey: Encrypted private key using WIF protected key format - :type encrypted_privkey: str - :param password: Required password for decryption - :type password: str - - :return tupple (bytes, bytes): (Private Key bytes, 4 byte address hash for verification) - """ - d = change_base(encrypted_privkey, 58, 256) - identifier = d[0:2] - flagbyte = d[2:3] - d = d[3:] - # ec_multiply = False - if identifier == b'\x01\x43': - # ec_multiply = True - raise EncodingError("EC multiply BIP38 keys are not supported at the moment") - elif identifier != b'\x01\x42': - raise EncodingError("Unknown BIP38 identifier, value must be 0x0142 (non-EC-multiplied) or " - "0x0143 (EC-multiplied)") - if flagbyte == b'\xc0': - compressed = False - elif flagbyte == b'\xe0' or flagbyte == b'\x20': - compressed = True - else: - raise EncodingError("Unrecognised password protected key format. Flagbyte incorrect.") - if isinstance(password, str): - password = password.encode('utf-8') - addresshash = d[0:4] - d = d[4:-4] - - key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) - # try: - # key = scrypt(password, addresshash, 64, 16384, 8, 8) - # except Exception: - # key = scrypt.hash(password, addresshash, 16384, 8, 8, 64) - derivedhalf1 = key[0:32] - derivedhalf2 = key[32:64] - encryptedhalf1 = d[0:16] - encryptedhalf2 = d[16:32] - # aes = pyaes.AESModeOfOperationECB(derivedhalf2) - aes = AES.new(derivedhalf2, AES.MODE_ECB) - decryptedhalf2 = aes.decrypt(encryptedhalf2) - decryptedhalf1 = aes.decrypt(encryptedhalf1) - priv = decryptedhalf1 + decryptedhalf2 - priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') - # if compressed: - # # FIXME: This works but does probably not follow the BIP38 standards (was before: priv = b'\0' + priv) - # priv += b'\1' - return priv, addresshash, compressed - - -def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): - """ - BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted private key - Based on code from https://github.com/nomorecoin/python-bip38-testing - - :param private_hex: Private key in hex format - :type private_hex: str - :param address: Address string - :type address: str - :param password: Required password for encryption - :type password: str - :param flagbyte: Flagbyte prefix for WIF - :type flagbyte: bytes - - :return str: BIP38 password encrypted private key - """ - if isinstance(address, str): - address = address.encode('utf-8') - if isinstance(password, str): - password = password.encode('utf-8') - addresshash = double_sha256(address)[0:4] - key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) - derivedhalf1 = key[0:32] - derivedhalf2 = key[32:64] - aes = AES.new(derivedhalf2, AES.MODE_ECB) - # aes = pyaes.AESModeOfOperationECB(derivedhalf2) - encryptedhalf1 = \ - aes.encrypt((int(private_hex[0:32], 16) ^ int.from_bytes(derivedhalf1[0:16], 'big')).to_bytes(16, 'big')) - encryptedhalf2 = \ - aes.encrypt((int(private_hex[32:64], 16) ^ int.from_bytes(derivedhalf1[16:32], 'big')).to_bytes(16, 'big')) - encrypted_privkey = b'\x01\x42' + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2 - encrypted_privkey += double_sha256(encrypted_privkey)[:4] - return base58encode(encrypted_privkey) - - def scrypt_hash(password, salt, key_len=64, N=16384, r=8, p=1, buflen=64): """ Wrapper for Scrypt method for scrypt or Cryptodome library diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 8b7bedf2..f5a5bbbb 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -456,6 +456,199 @@ def path_expand(path, path_template=None, level_offset=None, account_id=0, cosig return npath +def bip38_decrypt(encrypted_privkey, password): + """ + BIP0038 non-ec-multiply decryption. Returns WIF private key. + Based on code from https://github.com/nomorecoin/python-bip38-testing + This method is called by Key class init function when importing BIP0038 key. + + :param encrypted_privkey: Encrypted private key using WIF protected key format + :type encrypted_privkey: str + :param password: Required password for decryption + :type password: str + + :return tupple (bytes, bytes): (Private Key bytes, 4 byte address hash for verification) + """ + d = change_base(encrypted_privkey, 58, 256) + identifier = d[0:2] + flagbyte = d[2:3] + address_hash: bytes = d[3:7] + if identifier == BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: + owner_entropy: bytes = d[7:15] + encrypted_half_1_half_1: bytes = d[15:23] + encrypted_half_2: bytes = d[23:-4] + + lot_and_sequence = None + if flagbyte in [BIP38_MAGIC_LOT_AND_SEQUENCE_UNCOMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, + b'\x0c', b'\x14', b'\x1c', b'\x2c', b'\x34', b'\x3c']: + owner_salt: bytes = owner_entropy[:4] + lot_and_sequence = owner_entropy[4:] + else: + owner_salt: bytes = owner_entropy + + # pass_factor: bytes = scrypt.hash(unicodedata.normalize("NFC", passphrase), owner_salt, 16384, 8, 8, 32) + pass_factor = scrypt_hash(password, owner_salt, 32, 16384, 8, 8) + if lot_and_sequence: + pass_factor: bytes = double_sha256(pass_factor + owner_entropy) + if int.from_bytes(pass_factor, 'big') == 0 or int.from_bytes(pass_factor, 'big') >= secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + + # pre_public_key: str = private_key_to_public_key( + # private_key=pass_factor, public_key_type="compressed" + # ) + pre_public_key = HDKey(pass_factor).public_byte + salt = address_hash + owner_entropy + encrypted_seed_b: bytes = scrypt_hash(pre_public_key, salt, 64, 1024, 1, 1) + key: bytes = encrypted_seed_b[32:] + + # aes = AES.new(key, AES.MODE_ECB) + # encrypted_half_1 = \ + # aes.encrypt( + # (int.from_bytes(seed_b[:16], 'big') ^ int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) + # encrypted_half_2 = \ + # aes.encrypt((int.from_bytes((encrypted_half_1[8:] + seed_b[16:]), 'big') ^ + # int.from_bytes(derived_half_2, 'big')).to_bytes(16, 'big')) + # encrypted_wif = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_half_1[:8] + + # encrypted_half_2, prefix=BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) + + # aes: AESModeOfOperationECB = AESModeOfOperationECB(key) + aes = AES.new(key, AES.MODE_ECB) + + # encrypted_half_1_half_2_seed_b_last_3 = integer_to_bytes( + # bytes_to_integer(aes.decrypt(encrypted_half_2)) ^ bytes_to_integer(encrypted_seed_b[16:32]) + # ) + encrypted_half_1_half_2_seed_b_last_3 = ( + int.from_bytes(aes.decrypt(encrypted_half_2), 'big') ^ + int.from_bytes(encrypted_seed_b[16:32], 'big')).to_bytes(16, 'big') + encrypted_half_1_half_2: bytes = encrypted_half_1_half_2_seed_b_last_3[:8] + encrypted_half_1: bytes = ( + encrypted_half_1_half_1 + encrypted_half_1_half_2 + ) + + seed_b: bytes = (( + int.from_bytes(aes.decrypt(encrypted_half_1), 'big') ^ + int.from_bytes(encrypted_seed_b[:16], 'big')).to_bytes(16, 'big') + + encrypted_half_1_half_2_seed_b_last_3[8:]) + + factor_b: bytes = double_sha256(seed_b) + if int.from_bytes(factor_b, 'big') == 0 or int.from_bytes(factor_b, 'big') >= secp256k1_n: + raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + + # private_key: bytes = multiply_private_key(pass_factor, factor_b) + private_key = HDKey(pass_factor) * HDKey(factor_b) + # public_key: str = private_key_to_public_key( + # private_key=private_key, public_key_type="uncompressed" + # ) + # wif_type: Literal["wif", "wif-compressed"] = "wif" + # public_key_type: Literal["uncompressed", "compressed"] = "uncompressed" + compressed = False + public_key = private_key.public_uncompressed_hex + # if bytes_to_integer(flag) in FLAGS["compression"]: + if flagbyte in [BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, + b'\x28', b'\x2c', b'\x30', b'\x34', b'\x38', b'\x3c', b'\xe0', b'\xe8', b'\xf0', b'\xf8']: + public_key: str = private_key.public_compressed_hex + public_key_type = "compressed" + wif_type = "wif-compressed" + compressed = True + + # address: str = public_key_to_addresses(public_key=public_key, network=network) + address = private_key.address(compressed=compressed) + address_hash_check = double_sha256(bytes(address, 'utf8'))[:4] + # if get_checksum(get_bytes(address, unhexlify=False)) == address_hash: + if address_hash_check == address_hash: + # wif: str = private_key_to_wif( + # private_key=private_key, wif_type=wif_type, network=network + # ) + wif = private_key.wif() + lot = None + sequence = None + # if detail: + if lot_and_sequence: + # sequence: int = bytes_to_integer(lot_and_sequence) % 4096 + # lot: int = (bytes_to_integer(lot_and_sequence) - sequence) // 4096 + sequence = int.from_bytes(lot_and_sequence, 'big') % 4096 + lot = int.from_bytes(lot_and_sequence, 'big') // 4096 + # return dict( + # wif=wif, + # private_key=bytes_to_string(private_key), + # wif_type=wif_type, + # public_key=public_key, + # public_key_type=public_key_type, + # seed=bytes_to_string(seed_b), + # address=address, + # lot=lot, + # sequence=sequence + # ) + return wif + elif identifier == BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: + d = d[3:] + if flagbyte == b'\xc0': + compressed = False + elif flagbyte == b'\xe0' or flagbyte == b'\x20': + compressed = True + else: + raise EncodingError("Unrecognised password protected key format. Flagbyte incorrect.") + if isinstance(password, str): + password = password.encode('utf-8') + addresshash = d[0:4] + d = d[4:-4] + + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) + derivedhalf1 = key[0:32] + derivedhalf2 = key[32:64] + encryptedhalf1 = d[0:16] + encryptedhalf2 = d[16:32] + + # aes = pyaes.AESModeOfOperationECB(derivedhalf2) + aes = AES.new(derivedhalf2, AES.MODE_ECB) + decryptedhalf2 = aes.decrypt(encryptedhalf2) + decryptedhalf1 = aes.decrypt(encryptedhalf1) + priv = decryptedhalf1 + decryptedhalf2 + priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') + # if compressed: + # # FIXME: This works but does probably not follow the BIP38 standards (was before: priv = b'\0' + priv) + # priv += b'\1' + return priv, addresshash, compressed + else: + raise EncodingError("Unknown BIP38 identifier, value must be 0x0142 (non-EC-multiplied) or " + "0x0143 (EC-multiplied)") + + +def bip38_encrypt(private_hex, address, password, flagbyte=b'\xe0'): + """ + BIP0038 non-ec-multiply encryption. Returns BIP0038 encrypted private key + Based on code from https://github.com/nomorecoin/python-bip38-testing + + :param private_hex: Private key in hex format + :type private_hex: str + :param address: Address string + :type address: str + :param password: Required password for encryption + :type password: str + :param flagbyte: Flagbyte prefix for WIF + :type flagbyte: bytes + + :return str: BIP38 password encrypted private key + """ + if isinstance(address, str): + address = address.encode('utf-8') + if isinstance(password, str): + password = password.encode('utf-8') + addresshash = double_sha256(address)[0:4] + key = scrypt_hash(password, addresshash, 64, 16384, 8, 8) + derivedhalf1 = key[0:32] + derivedhalf2 = key[32:64] + aes = AES.new(derivedhalf2, AES.MODE_ECB) + # aes = pyaes.AESModeOfOperationECB(derivedhalf2) + encryptedhalf1 = \ + aes.encrypt((int(private_hex[0:32], 16) ^ int.from_bytes(derivedhalf1[0:16], 'big')).to_bytes(16, 'big')) + encryptedhalf2 = \ + aes.encrypt((int(private_hex[32:64], 16) ^ int.from_bytes(derivedhalf1[16:32], 'big')).to_bytes(16, 'big')) + encrypted_privkey = b'\x01\x42' + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2 + encrypted_privkey += double_sha256(encrypted_privkey)[:4] + return base58encode(encrypted_privkey) + + def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt=os.urandom(8)): """ Intermediate passphrase generator for EC multiplied BIP38 encrypted private keys. diff --git a/index.html b/index.html new file mode 100644 index 00000000..c6c7706a --- /dev/null +++ b/index.html @@ -0,0 +1,5574 @@ + + + + Het Financieele Dagblad + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + +
+
+
+ + +
+ +
+
+
Close sub menu
+
+
+ + + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+
+ +
+ + +
+
+
+
+
+
+ +
+ + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+
+ + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ +
+
+
+
+

Dagoverzicht

+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+ + + +
+

Politiek

+ +

Financiële Markten

+ +
+ +
+
+
+ +
+
+
+
+ + + + +

Schakel browser notificaties in

+ +
+

Schakel browser notificaties in om meldingen te ontvangen op dit apparaat.

+ +
+ + +
+
+
+ + +

Browser notificaties inschakelen

+ +
+

U heeft browser notificaties voor fd.nl eerder geweigerd. Ga naar browser instellingen en sta browser notificaties toe voor fd.nl. De precieze handelingen zijn afhankelijk van uw browser.

+
+
+ + +

U volgt een onderwerp

+ +
+

Wilt u ook meldingen ontvangen voor dit onderwerp?

+ +
+ + +
+
+
+ + +

Meldingen

+ +
+

Wilt u ook meldingen ontvangen voor dit onderwerp?

+ +
+ + +
+ +
+ + + +
+
+
+ + + From 04897b601aa5f4d8d412a42ceb8cd333d1ae714c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 8 Mar 2024 13:59:52 +0100 Subject: [PATCH 134/207] Add EC multiply unittests --- bitcoinlib/keys.py | 78 +++++++--------------------- bitcoinlib/tools/clw.py | 2 +- tests/bip38_protected_key_tests.json | 24 +++++++++ 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index f5a5bbbb..1be80c48 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -486,37 +486,18 @@ def bip38_decrypt(encrypted_privkey, password): else: owner_salt: bytes = owner_entropy - # pass_factor: bytes = scrypt.hash(unicodedata.normalize("NFC", passphrase), owner_salt, 16384, 8, 8, 32) pass_factor = scrypt_hash(password, owner_salt, 32, 16384, 8, 8) if lot_and_sequence: pass_factor: bytes = double_sha256(pass_factor + owner_entropy) if int.from_bytes(pass_factor, 'big') == 0 or int.from_bytes(pass_factor, 'big') >= secp256k1_n: raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") - # pre_public_key: str = private_key_to_public_key( - # private_key=pass_factor, public_key_type="compressed" - # ) pre_public_key = HDKey(pass_factor).public_byte salt = address_hash + owner_entropy encrypted_seed_b: bytes = scrypt_hash(pre_public_key, salt, 64, 1024, 1, 1) key: bytes = encrypted_seed_b[32:] - # aes = AES.new(key, AES.MODE_ECB) - # encrypted_half_1 = \ - # aes.encrypt( - # (int.from_bytes(seed_b[:16], 'big') ^ int.from_bytes(derived_half_1, 'big')).to_bytes(16, 'big')) - # encrypted_half_2 = \ - # aes.encrypt((int.from_bytes((encrypted_half_1[8:] + seed_b[16:]), 'big') ^ - # int.from_bytes(derived_half_2, 'big')).to_bytes(16, 'big')) - # encrypted_wif = pubkeyhash_to_addr_base58(flag + address_hash + owner_entropy + encrypted_half_1[:8] + - # encrypted_half_2, prefix=BIP38_EC_MULTIPLIED_PRIVATE_KEY_PREFIX) - - # aes: AESModeOfOperationECB = AESModeOfOperationECB(key) aes = AES.new(key, AES.MODE_ECB) - - # encrypted_half_1_half_2_seed_b_last_3 = integer_to_bytes( - # bytes_to_integer(aes.decrypt(encrypted_half_2)) ^ bytes_to_integer(encrypted_seed_b[16:32]) - # ) encrypted_half_1_half_2_seed_b_last_3 = ( int.from_bytes(aes.decrypt(encrypted_half_2), 'big') ^ int.from_bytes(encrypted_seed_b[16:32], 'big')).to_bytes(16, 'big') @@ -534,52 +515,36 @@ def bip38_decrypt(encrypted_privkey, password): if int.from_bytes(factor_b, 'big') == 0 or int.from_bytes(factor_b, 'big') >= secp256k1_n: raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") - # private_key: bytes = multiply_private_key(pass_factor, factor_b) private_key = HDKey(pass_factor) * HDKey(factor_b) - # public_key: str = private_key_to_public_key( - # private_key=private_key, public_key_type="uncompressed" - # ) - # wif_type: Literal["wif", "wif-compressed"] = "wif" - # public_key_type: Literal["uncompressed", "compressed"] = "uncompressed" compressed = False public_key = private_key.public_uncompressed_hex - # if bytes_to_integer(flag) in FLAGS["compression"]: if flagbyte in [BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, b'\x28', b'\x2c', b'\x30', b'\x34', b'\x38', b'\x3c', b'\xe0', b'\xe8', b'\xf0', b'\xf8']: public_key: str = private_key.public_compressed_hex - public_key_type = "compressed" - wif_type = "wif-compressed" compressed = True - # address: str = public_key_to_addresses(public_key=public_key, network=network) address = private_key.address(compressed=compressed) address_hash_check = double_sha256(bytes(address, 'utf8'))[:4] - # if get_checksum(get_bytes(address, unhexlify=False)) == address_hash: - if address_hash_check == address_hash: - # wif: str = private_key_to_wif( - # private_key=private_key, wif_type=wif_type, network=network - # ) - wif = private_key.wif() - lot = None - sequence = None - # if detail: - if lot_and_sequence: - # sequence: int = bytes_to_integer(lot_and_sequence) % 4096 - # lot: int = (bytes_to_integer(lot_and_sequence) - sequence) // 4096 - sequence = int.from_bytes(lot_and_sequence, 'big') % 4096 - lot = int.from_bytes(lot_and_sequence, 'big') // 4096 - # return dict( - # wif=wif, - # private_key=bytes_to_string(private_key), - # wif_type=wif_type, - # public_key=public_key, - # public_key_type=public_key_type, - # seed=bytes_to_string(seed_b), - # address=address, - # lot=lot, - # sequence=sequence - # ) - return wif + if address_hash_check != address_hash: + raise ValueError("Address hash has invalid checksum") + wif = private_key.wif() + lot = None + sequence = None + if lot_and_sequence: + sequence = int.from_bytes(lot_and_sequence, 'big') % 4096 + lot = int.from_bytes(lot_and_sequence, 'big') // 4096 + + # TODO: Check if more data needs to be returned + retdict = dict( + wif=wif, + private_key=private_key.private_hex, + public_key=public_key, + seed=seed_b.hex(), + address=address, + lot=lot, + sequence=sequence + ) + return private_key.private_byte, address_hash, compressed elif identifier == BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: d = d[3:] if flagbyte == b'\xc0': @@ -605,9 +570,6 @@ def bip38_decrypt(encrypted_privkey, password): decryptedhalf1 = aes.decrypt(encryptedhalf1) priv = decryptedhalf1 + decryptedhalf2 priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') - # if compressed: - # # FIXME: This works but does probably not follow the BIP38 standards (was before: priv = b'\0' + priv) - # priv += b'\1' return priv, addresshash, compressed else: raise EncodingError("Unknown BIP38 identifier, value must be 0x0142 (non-EC-multiplied) or " diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 5caa89f6..e3df4e75 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -61,7 +61,7 @@ def parse_args(): help="Name of wallet to create or open. Provide wallet name or number when running wallet " "actions") parser_new.add_argument('--password', - help='Password for BIP38 encrypted key. Use to create a wallet with a protected key') + help='Password for BIP38 encrypted key. Use to create a wallet from a protected key') parser_new.add_argument('--network', '-n', help="Specify 'bitcoin', 'litecoin', 'testnet' or other supported network") parser_new.add_argument('--passphrase', default=None, metavar="PASSPHRASE", diff --git a/tests/bip38_protected_key_tests.json b/tests/bip38_protected_key_tests.json index 5032a7df..0642f9bc 100644 --- a/tests/bip38_protected_key_tests.json +++ b/tests/bip38_protected_key_tests.json @@ -26,6 +26,30 @@ "wif": "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7", "address": "1HmPbwsvG5qJ3KJfxzsZRZWhbm1xBMuS8B", "description": "no EC multiply / compression #2" + }, + { + "passphrase": "TestingOneTwoThree", + "passphrase_code": "passphrasepxFy57B9v8HtUsszJYKReoNDV6VHjUSGt8EVJmux9n1J3Ltf1gRxyDGXqnf9qm", + "bip38": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", + "wif": "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", + "address": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", + "description": "EC multiply / no compression / no lot sequence numbers #1" + }, + { + "passphrase": "Satoshi", + "passphrase_code": "passphraseoRDGAXTWzbp72eVbtUDdn1rwpgPUGjNZEc6CGBo8i5EC1FPW8wcnLdq4ThKzAS", + "bip38": "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", + "wif": "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", + "address": "1CqzrtZC6mXSAhoxtFwVjz8LtwLJjDYU3V", + "description": "EC multiply / no compression / no lot sequence numbers #2" + }, + { + "passphrase": "MOLON LABE", + "passphrase_code": "passphraseaB8feaLQDENqCgr4gKZpmf4VoaT6qdjJNJiv7fsKvjqavcJxvuR1hy25aTu5sX", + "bip38": "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", + "wif": "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", + "address": "1Jscj8ALrYu2y9TD8NrpvDBugPedmbj4Yh", + "description": "EC multiply / no compression / lot sequence numbers #1" } ], "invalid": { From 2f4de7b537f7d28dd364386e7a054b8b3d9fa5e9 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 9 Mar 2024 20:23:11 +0100 Subject: [PATCH 135/207] Fix issue with uncompressed keys create from public point --- bitcoinlib/keys.py | 22 ++++++++-------------- tests/test_keys.py | 7 +++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 1be80c48..54ad6bb4 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1077,7 +1077,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', if not self.is_private: self.secret = None if self.key_format == 'point': - self.compressed = True + self.compressed = compressed self._x = import_key[0] self._y = import_key[1] self.x_bytes = self._x.to_bytes(32, 'big') @@ -1085,9 +1085,9 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', self.x_hex = self.x_bytes.hex() self.y_hex = self.y_bytes.hex() prefix = '03' if self._y % 2 else '02' - self.public_hex = prefix + self.x_hex + self._public_uncompressed_hex = '04' + self.x_hex + self.y_hex self.public_compressed_hex = prefix + self.x_hex - self.public_byte = (b'\3' if self._y % 2 else b'\2') + self.x_bytes + self.public_hex = self.public_compressed_hex if compressed else self._public_uncompressed_hex else: pub_key = to_hexstring(import_key) if len(pub_key) == 130: @@ -1096,10 +1096,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', self.y_hex = pub_key[66:130] self._y = int(self.y_hex, 16) self.compressed = False - if self._y % 2: - prefix = '03' - else: - prefix = '02' + prefix = '03' if self._y % 2 else '02' self.public_hex = pub_key self.public_compressed_hex = prefix + self.x_hex else: @@ -1108,13 +1105,10 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', self.compressed = True self._x = int(self.x_hex, 16) self.public_compressed_hex = pub_key - self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) - if self._public_uncompressed_hex: - self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) - if self.compressed: - self.public_byte = self.public_compressed_byte - else: - self.public_byte = self.public_uncompressed_byte + self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) + if self._public_uncompressed_hex: + self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) + self.public_byte = self.public_compressed_byte if self.compressed else self.public_uncompressed_byte elif self.is_private and self.key_format == 'decimal': self.secret = int(import_key) diff --git a/tests/test_keys.py b/tests/test_keys.py index b21219ca..83e252ce 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -139,6 +139,13 @@ def test_keys_create_public_point(self): self.assertEqual(k.public(), k2) self.assertEqual(k.address(), k2.address()) + k = HDKey(compressed=False, witness_type='legacy') + p = (k.x, k.y) + k2 = HDKey(p, compressed=False, witness_type='legacy') + self.assertEqual(k, k2) + self.assertEqual(k.public(), k2) + self.assertEqual(k.address(), k2.address()) + class TestGetKeyFormat(unittest.TestCase): From a93920b14cbc81d6522f87dfb574364fb430f54b Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 9 Mar 2024 20:36:03 +0100 Subject: [PATCH 136/207] Skip encrypt unittest for EC multiplied keys --- tests/bip38_protected_key_tests.json | 9 ++++++--- tests/test_keys.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/bip38_protected_key_tests.json b/tests/bip38_protected_key_tests.json index 0642f9bc..6852c035 100644 --- a/tests/bip38_protected_key_tests.json +++ b/tests/bip38_protected_key_tests.json @@ -33,7 +33,8 @@ "bip38": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", "wif": "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2", "address": "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", - "description": "EC multiply / no compression / no lot sequence numbers #1" + "description": "EC multiply / no compression / no lot sequence numbers #1", + "test_encrypt": false }, { "passphrase": "Satoshi", @@ -41,7 +42,8 @@ "bip38": "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", "wif": "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH", "address": "1CqzrtZC6mXSAhoxtFwVjz8LtwLJjDYU3V", - "description": "EC multiply / no compression / no lot sequence numbers #2" + "description": "EC multiply / no compression / no lot sequence numbers #2", + "test_encrypt": false }, { "passphrase": "MOLON LABE", @@ -49,7 +51,8 @@ "bip38": "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", "wif": "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8", "address": "1Jscj8ALrYu2y9TD8NrpvDBugPedmbj4Yh", - "description": "EC multiply / no compression / lot sequence numbers #1" + "description": "EC multiply / no compression / lot sequence numbers #1", + "test_encrypt": false } ], "invalid": { diff --git a/tests/test_keys.py b/tests/test_keys.py index 83e252ce..0bd81851 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -637,6 +637,8 @@ def test_encrypt_private_key(self): if not USING_MODULE_SCRYPT: return for v in self.vectors["valid"]: + if v.get('test_encrypt') is False: + continue k = Key(v['wif']) # print("Check %s + %s = %s " % (v['wif'], v['passphrase'], v['bip38'])) self.assertEqual(str(v['bip38']), k.encrypt(str(v['passphrase']))) From 871813cfdf4e4d097b32ee2b9110b5e9276d8c48 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 10 Mar 2024 00:03:59 +0100 Subject: [PATCH 137/207] Add more unittests for EC multiplied BIP38 keys --- bitcoinlib/keys.py | 2 +- tests/bip38_protected_key_tests.json | 2 +- tests/test_keys.py | 60 +++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 54ad6bb4..c40f1f6a 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -518,7 +518,7 @@ def bip38_decrypt(encrypted_privkey, password): private_key = HDKey(pass_factor) * HDKey(factor_b) compressed = False public_key = private_key.public_uncompressed_hex - if flagbyte in [BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, + if flagbyte in [BIP38_MAGIC_NO_LOT_AND_SEQUENCE_COMPRESSED_FLAG, BIP38_MAGIC_LOT_AND_SEQUENCE_COMPRESSED_FLAG, b'\x28', b'\x2c', b'\x30', b'\x34', b'\x38', b'\x3c', b'\xe0', b'\xe8', b'\xf0', b'\xf8']: public_key: str = private_key.public_compressed_hex compressed = True diff --git a/tests/bip38_protected_key_tests.json b/tests/bip38_protected_key_tests.json index 6852c035..7de05202 100644 --- a/tests/bip38_protected_key_tests.json +++ b/tests/bip38_protected_key_tests.json @@ -54,7 +54,7 @@ "description": "EC multiply / no compression / lot sequence numbers #1", "test_encrypt": false } - ], +], "invalid": { "decrypt": [], "encrypt": [], diff --git a/tests/test_keys.py b/tests/test_keys.py index 0bd81851..0c355b68 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Key, Encoding and Mnemonic Class -# © 2017-2018 July - 1200 Web Development +# © 2017-2024 March - 1200 Web Development # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -681,6 +681,64 @@ def test_bip38_intermediate_password(self): self.assertEqual(password1, intpwd1) self.assertEqual(bip38_intermediate_password(passphrase="TestingOneTwoThree")[:10], 'passphrase') + intermediate_codes = [ + {"passphrase": "MOLON LABE", "lot": None, "sequence": None, "owner_salt": "d7ebe42cf42a79f4", + "intermediate_passphrase": "passphraserDFxboKK9cTkBQMb73vdzgsXB5L6cCMFCzTVoMTpMWYD8SJXv3jcKyHbRWBcza"}, + {"passphrase": "MOLON LABE", "lot": 100000, "sequence": 1, "owner_salt": "d7ebe42cf42a79f4", + "intermediate_passphrase": "passphrasedYVZ6EdRqSmcHHsYWmJ7wzWcWYuDQmf9EGH9Pnrv67eHy4qswaAGGc8Et3eeGp"}, + {"passphrase": "MOLON LABE", "lot": 100000, "sequence": 1, "owner_salt": "d7ebe42c", + "intermediate_passphrase": "passphrasedYVZ6EdRqSmcHHsYWmJ7wzWcWYuDQmf9EGH9Pnrv67eHy4qswaAGGc8Et3eeGp"} + ] + for ic in intermediate_codes: + intermediate_password = bip38_intermediate_password(ic['passphrase'], ic['lot'], ic['sequence'], + ic['owner_salt']) + self.assertEqual(intermediate_password, ic['intermediate_passphrase']) + + def test_bip38_create_new_encrypted_wif(self): + create_new_encrypted_wif = [ + {"intermediate_passphrase": "passphraserDFxboKK9cTkBQMb73vdzgsXB5L6cCMFCzTVoMTpMWYD8SJXv3jcKyHbRWBcza", + "seed": "9b6cad86daddae99ac3b76c1e47e61bc7f4665d02e10c290", + "encrypted_wif": "6PnQDk5XngQugiy1Fi2kzzgKAQrxZrtQDGNFiQTMamjiJcjBT4LhXdnhNf", + "confirmation_code": "cfrm38VUF9PjRxQojZERDySt9Q7Z9FSdhQkMP5RFsouS4y3Emf2YD2CXXMCypQvv94dJujaPTfq", + "public_key": "0348ca8b4e7c0c75ecfd4b437535d186a12f3027be0c29d2125e9c0dec48677caa", + "compressed": True, "address": "16uZsrjjENCVsXwJqw2kMWGwWbDKQ12a1h"}, + {"intermediate_passphrase": "passphrasedYVZ6EdRqSmcHHsYWmJ7wzWcWYuDQmf9EGH9Pnrv67eHy4qswaAGGc8Et3eeGp", + "seed": "9b6cad86daddae99ac3b76c1e47e61bc7f4665d02e10c290", + "encrypted_wif": "6PgRAPfrPWjPXfC6x9XB139RHzUP8GFcVen5Ju3qJDhRP69Q4Vd8Wbct6B", + "confirmation_code": "cfrm38V8kEzECGczWJmEoGuYfkoamcmVij3tHUhD6DEEquSRXp61HzhnT8jwQwBBZiKs9Jg4LXZ", + "public_key": "04597967956e7f4c0e13ed7cd98baa9d7697a7f685d4347168e4a011c5fe6ba628e06ef89587c17afb5504" + "4336e44648dfa944ca85a4af0a7b28c29d4eefd0da92", + "compressed": False, "address": "1KRg2YJxuHiNcqfp9gVpkgRFhcvALy1zgk"} + ] + for ew in create_new_encrypted_wif: + res = bip38_create_new_encrypted_wif(ew["intermediate_passphrase"], ew["compressed"], ew["seed"]) + self.assertEqual(res['encrypted_wif'], ew['encrypted_wif']) + self.assertEqual(res['confirmation_code'], ew['confirmation_code']) + self.assertEqual(res['address'], ew['address']) + + def test_bip38_decrypt_wif(self): + bip38_decrypt_test_vectors = [ + {"encrypted_wif": "6PRL8jj6dLQjBBJjHMdUKLSNLEpjTyAfmt8GnCnfT87NeQ2BU5eAW1tcsS", + "passphrase": "TestingOneTwoThree", + "network": "testnet", "wif": "938jwjergAxARSWx2YSt9nSBWBz24h8gLhv7EUfgEP1wpMLg6iX", + "private_key": "cbf4b9f70470856bb4f40f80b87edb90865997ffee6df315ab166d713af433a5", "wif_type": "wif", + "public_key": "04d2ce831dd06e5c1f5b1121ef34c2af4bcb01b126e309234adbc3561b60c9360ea7f23327b49ba7f10d17fad15f068b8807dbbc9e4ace5d4a0b40264eefaf31a4", + "compressed": False, "seed": None, "address": "myM3eoxWDWxFe7GYHZw8K21rw7QDNZeDYM", "lot": None, + "sequence": None}, + {"encrypted_wif": "6PYVB5nHnumbUua1UmsAMPHWHa76Ci48MY79aKYnpKmwxeGqHU2XpXtKvo", + "passphrase": "TestingOneTwoThree", + "network": "testnet", "wif": "cURAYbG6FtvUasdBsooEmmY9MqUfhJ8tdybQWV7iA4BAwunCT2Fu", + "private_key": "cbf4b9f70470856bb4f40f80b87edb90865997ffee6df315ab166d713af433a5", + "wif_type": "wif-compressed", + "public_key": "02d2ce831dd06e5c1f5b1121ef34c2af4bcb01b126e309234adbc3561b60c9360e", + "compressed": True, "seed": None, "address": "mkaJhmE5vvaXG17uZdCm6wKpckEfnG4yt9", "lot": None, + "sequence": None}, + ] + + for tv in bip38_decrypt_test_vectors: + res = bip38_decrypt(tv['encrypted_wif'], tv['passphrase']) + self.assertEqual(res[0].hex(), tv['private_key']) + self.assertEqual(res[2], tv['compressed']) class TestKeysBulk(unittest.TestCase): From f43aa3986db1fa7b89204c5342419114768b52e8 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 11 Mar 2024 15:09:52 +0100 Subject: [PATCH 138/207] Get EC BIP38 key multiplication working with ecdsa module --- bitcoinlib/keys.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index c40f1f6a..aaa18b7e 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -674,9 +674,9 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, see :param compressed: Compressed key :type compressed: boolean :param seed: Seed, default to ``os.urandom(24)`` - :type seed: Optional[str, bytes] - :param network: Network type - :type network: Literal["mainnet", "testnet"], default to ``mainnet`` + :type seed: str, bytes + :param network: Network name + :type network: str :returns: dict -- Encrypted WIF (Wallet Important Format) @@ -713,7 +713,7 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, see raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") pk_point = ec_point_multiplication(HDKey(pass_point).public_point(), int.from_bytes(factor_b, 'big')) - k = HDKey((pk_point.x, pk_point.y), compressed=compressed, witness_type='legacy') + k = HDKey((pk_point[0], pk_point[1]), compressed=compressed, witness_type='legacy', network=network) public_key = k.public_hex address = k.address() address_hash = double_sha256(bytes(address, 'utf8'))[:4] @@ -2709,28 +2709,29 @@ def ec_point(m): return fastecdsa_keys.get_public_key(m, fastecdsa_secp256k1) else: point = secp256k1_generator - point *= m - return point + return point * m def ec_point_multiplication(p, m): """ Method for elliptic curve multiplication on the secp256k1 curve. Multiply Generator point G by m + :param p: Point on SECP256k1 curve + :type p: tuple :param m: A scalar multiplier :type m: int - :return Point: Generator point G multiplied by m + :return tuple: Generator point G multiplied by m as tuple in (x, y) format """ m = int(m) if USE_FASTECDSA: point = fastecdsa_point.Point(p[0], p[1], fastecdsa_secp256k1) - return point * m + point_m = point * m + return (point_m.x, point_m.y) else: - raise NotImplementedError - # point = secp256k1_generator - # point *= m - # return point + point = ecdsa.ellipticcurve.Point(ecdsa.SECP256k1.curve, p[0], p[1]) + point_m = point * m + return (point_m.x(), point_m.y()) def mod_sqrt(a): From e38d4e56569c65c333e231135a370c1930012523 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 11 Mar 2024 19:31:58 +0100 Subject: [PATCH 139/207] Add BIP38 key examples --- bitcoinlib/keys.py | 28 +++---- examples/bip38_encrypted_wif_private_key.py | 82 +++++++++++++++++++++ tests/test_keys.py | 14 +++- 3 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 examples/bip38_encrypted_wif_private_key.py diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index aaa18b7e..26b00e6c 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -467,7 +467,7 @@ def bip38_decrypt(encrypted_privkey, password): :param password: Required password for decryption :type password: str - :return tupple (bytes, bytes): (Private Key bytes, 4 byte address hash for verification) + :return tuple (bytes, bytes, boolean, dict): (Private Key bytes, 4 byte address hash for verification, compressed?, dictionary with additional info) """ d = change_base(encrypted_privkey, 58, 256) identifier = d[0:2] @@ -490,7 +490,7 @@ def bip38_decrypt(encrypted_privkey, password): if lot_and_sequence: pass_factor: bytes = double_sha256(pass_factor + owner_entropy) if int.from_bytes(pass_factor, 'big') == 0 or int.from_bytes(pass_factor, 'big') >= secp256k1_n: - raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") pre_public_key = HDKey(pass_factor).public_byte salt = address_hash + owner_entropy @@ -513,7 +513,7 @@ def bip38_decrypt(encrypted_privkey, password): factor_b: bytes = double_sha256(seed_b) if int.from_bytes(factor_b, 'big') == 0 or int.from_bytes(factor_b, 'big') >= secp256k1_n: - raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") private_key = HDKey(pass_factor) * HDKey(factor_b) compressed = False @@ -534,7 +534,6 @@ def bip38_decrypt(encrypted_privkey, password): sequence = int.from_bytes(lot_and_sequence, 'big') % 4096 lot = int.from_bytes(lot_and_sequence, 'big') // 4096 - # TODO: Check if more data needs to be returned retdict = dict( wif=wif, private_key=private_key.private_hex, @@ -544,7 +543,7 @@ def bip38_decrypt(encrypted_privkey, password): lot=lot, sequence=sequence ) - return private_key.private_byte, address_hash, compressed + return private_key.private_byte, address_hash, compressed, retdict elif identifier == BIP38_NO_EC_MULTIPLIED_PRIVATE_KEY_PREFIX: d = d[3:] if flagbyte == b'\xc0': @@ -570,7 +569,7 @@ def bip38_decrypt(encrypted_privkey, password): decryptedhalf1 = aes.decrypt(encryptedhalf1) priv = decryptedhalf1 + decryptedhalf2 priv = (int.from_bytes(priv, 'big') ^ int.from_bytes(derivedhalf1, 'big')).to_bytes(32, 'big') - return priv, addresshash, compressed + return priv, addresshash, compressed, {} else: raise EncodingError("Unknown BIP38 identifier, value must be 0x0142 (non-EC-multiplied) or " "0x0143 (EC-multiplied)") @@ -616,6 +615,8 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= Intermediate passphrase generator for EC multiplied BIP38 encrypted private keys. Source: https://github.com/meherett/python-bip38/blob/master/bip38/bip38.py + Use intermediate password to create a encrypted WIF key with the :func:`bip38_create_new_encrypted_wif` method. + :param passphrase: Passphrase or password text :type passphrase: str :param lot: Lot number between 100000 <= lot <= 999999 range, default to ``None`` @@ -625,7 +626,7 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= :param owner_salt: Owner salt, default to ``os.urandom(8)`` :type owner_salt: str, bytes - :returns: str -- Intermediate passphrase + :returns str: Intermediate passphrase >>> bip38_intermediate_password(passphrase="TestingOneTwoThree", lot=199999, sequence=1, owner_salt="75ed1cdeb254cb38") 'passphraseb7ruSN4At4Rb8hPTNcAVezfsjonvUs4Qo3xSp1fBFsFPvVGSbpP2WTJMhw3mVZ' @@ -667,18 +668,19 @@ def bip38_intermediate_password(passphrase, lot=None, sequence=None, owner_salt= def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, seed=os.urandom(24), network=DEFAULT_NETWORK): """ - Create new encrypted WIF (Wallet Important Format) + Create new encrypted WIF BIP38 EC multiplied key. Use :func:`bip38_intermediate_password` to create a + intermediate passphrase first. :param intermediate_passphrase: Intermediate passphrase text :type intermediate_passphrase: str - :param compressed: Compressed key + :param compressed: Compressed or uncompressed key :type compressed: boolean :param seed: Seed, default to ``os.urandom(24)`` :type seed: str, bytes :param network: Network name :type network: str - :returns: dict -- Encrypted WIF (Wallet Important Format) + :returns dict: Dictionary with encrypted WIF key and confirmation code """ @@ -710,7 +712,7 @@ def bip38_create_new_encrypted_wif(intermediate_passphrase, compressed=True, see factor_b: bytes = double_sha256(seed_b) if not 0 < int.from_bytes(factor_b, 'big') < secp256k1_n: - raise ValueError("Invalid EC encrypted WIF (Wallet Important Format)") + raise ValueError("Invalid EC encrypted WIF (Wallet Import Format)") pk_point = ec_point_multiplication(HDKey(pass_point).public_point(), int.from_bytes(factor_b, 'big')) k = HDKey((pk_point[0], pk_point[1]), compressed=compressed, witness_type='legacy', network=network) @@ -1381,7 +1383,7 @@ def _bip38_decrypt(encrypted_privkey, password, network=DEFAULT_NETWORK): :return str: Private Key WIF """ - priv, addresshash, compressed = bip38_decrypt(encrypted_privkey, password) + priv, addresshash, compressed, _ = bip38_decrypt(encrypted_privkey, password) # Verify addresshash k = Key(priv, compressed=compressed, network=network) @@ -1927,7 +1929,7 @@ def _bip38_decrypt(encrypted_privkey, password, network=DEFAULT_NETWORK, witness :return str: Private Key WIF """ - priv, addresshash, compressed = bip38_decrypt(encrypted_privkey, password) + priv, addresshash, compressed, _ = bip38_decrypt(encrypted_privkey, password) # compressed = True if priv[-1:] == b'\1' else False # Verify addresshash diff --git a/examples/bip38_encrypted_wif_private_key.py b/examples/bip38_encrypted_wif_private_key.py new file mode 100644 index 00000000..37fe706e --- /dev/null +++ b/examples/bip38_encrypted_wif_private_key.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Bip38 encrypted private keys +# +# © 2024 March - 1200 Web Development +# + +from bitcoinlib.keys import * + +# +# Example #1 - BIP38 key - no EC multiplication +# +private_wif = "L3QtG7mpcV1AiGCuRCi34HgwTtWDPWNe6m8Wi58S1LzavAsu3v1x" +password = "bitcoinlib" +expected_encrypted_wif = "6PYRg5u7XPXoL9v8nXbBJkzjcMtCqSDM1p9MJttpXb42W1DNt33iX8tosj" +k = HDKey(private_wif, witness_type='legacy') +encrypted_wif = k.encrypt(password=password) +assert(encrypted_wif == expected_encrypted_wif) +print("Encrypted WIF: %s" % encrypted_wif) + +k2 = HDKey(encrypted_wif, password=password, witness_type='legacy') +assert(k2.wif_key() == private_wif) +print("Decrypted WIF: %s" % k2.wif_key()) + + +# +# Example #2 - EC multiplied BIP38 key encryption - not lot and sequence +# from https://bip38.readthedocs.io/en/v0.3.0/index.html +# +passphrase = "meherett" +owner_salt = "75ed1cdeb254cb38" +seed = "99241d58245c883896f80843d2846672d7312e6195ca1a6c" +compressed = False +expected_intermediate_password = "passphraseondJwvQGEWFNrNJRPi4G5XAL5SU777GwTNtqmDXqA3CGP7HXfH6AdBxxc5WUKC" +expected_encrypted_wif = "6PfP7T3iQ5jLJLsH5DneySLLF5bhd879DHW87Pxzwtwvn2ggcncxsNKN5c" +expected_confirmation_code = "cfrm38V5NZfTZKRaRDTvFAMkNKqKAxTxdDjDdb5RpFfXrVRw7Nov5m2iP3K1Eg5QQRxs52kgapy" +expected_private_key = "5Jh21edvnWUXFjRz8mDVN3CSPp1CyTuUSFBKZeWYU726R6MW3ux" + +intermediate_password = bip38_intermediate_password(passphrase, owner_salt=owner_salt) +assert(intermediate_password == expected_intermediate_password) +print("\nIntermediate Password: %s" % intermediate_password) + +res = bip38_create_new_encrypted_wif(intermediate_password, compressed=compressed, seed=seed) +assert(res['encrypted_wif'] == expected_encrypted_wif) +print("Encrypted WIF: %s" % res['encrypted_wif']) +assert(res['confirmation_code'] == expected_confirmation_code) +print("Confirmation Code: %s" % res['confirmation_code']) + +k = HDKey(res['encrypted_wif'], password=passphrase, compressed=compressed, witness_type='legacy') +assert(k.wif_key() == expected_private_key) +print("Private WIF: %s" % k.wif_key()) + +# +# Example #2 - EC multiplied BIP38 key encryption - with lot and sequence +# from https://bip38.readthedocs.io/en/v0.3.0/index.html +# +passphrase = "meherett" +owner_salt = "75ed1cdeb254cb38" +seed = "99241d58245c883896f80843d2846672d7312e6195ca1a6c" +compressed = True +lot = 369861 +sequence = 1 +expected_intermediate_password = "passphraseb7ruSNDGP7cmnFHQpmos7TeAy26AFN4GyRTBqq6hiaFbQzQBvirD9oHsafQvzd" +expected_encrypted_wif = "6PoEPBnJjm8UAiSGWQEKKNq9V2GMHqKkTcUqUFzsaX7wgjpQWR2qWPdnpt" +expected_confirmation_code = "cfrm38VWx5xH1JFm5EVE3mzQvDPFkz7SqNiaFxhyUfp3Fjc2wdYmK7dGEWoW6irDPSrwoaxB5zS" +expected_private_key = "KzFbTBirbEEtEPgWL3xhohUcrg6yUmJupAGrid7vBP9F2Vh8GTUB" + +intermediate_password = bip38_intermediate_password(passphrase, lot=lot, sequence=sequence, owner_salt=owner_salt) +assert(intermediate_password == expected_intermediate_password) +print("\nIntermediate Password: %s" % intermediate_password) + +res = bip38_create_new_encrypted_wif(intermediate_password, compressed=compressed, seed=seed) +assert(res['encrypted_wif'] == expected_encrypted_wif) +print("Encrypted WIF: %s" % res['encrypted_wif']) +assert(res['confirmation_code'] == expected_confirmation_code) +print("Confirmation Code: %s" % res['confirmation_code']) + +k = HDKey(res['encrypted_wif'], password=passphrase, compressed=compressed, witness_type='legacy') +assert(k.wif_key() == expected_private_key) +print("Private WIF: %s" % k.wif_key()) diff --git a/tests/test_keys.py b/tests/test_keys.py index 0c355b68..f29b78f3 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -701,17 +701,25 @@ def test_bip38_create_new_encrypted_wif(self): "encrypted_wif": "6PnQDk5XngQugiy1Fi2kzzgKAQrxZrtQDGNFiQTMamjiJcjBT4LhXdnhNf", "confirmation_code": "cfrm38VUF9PjRxQojZERDySt9Q7Z9FSdhQkMP5RFsouS4y3Emf2YD2CXXMCypQvv94dJujaPTfq", "public_key": "0348ca8b4e7c0c75ecfd4b437535d186a12f3027be0c29d2125e9c0dec48677caa", - "compressed": True, "address": "16uZsrjjENCVsXwJqw2kMWGwWbDKQ12a1h"}, + "compressed": True, "address": "16uZsrjjENCVsXwJqw2kMWGwWbDKQ12a1h", "network": "bitcoin"}, {"intermediate_passphrase": "passphrasedYVZ6EdRqSmcHHsYWmJ7wzWcWYuDQmf9EGH9Pnrv67eHy4qswaAGGc8Et3eeGp", "seed": "9b6cad86daddae99ac3b76c1e47e61bc7f4665d02e10c290", "encrypted_wif": "6PgRAPfrPWjPXfC6x9XB139RHzUP8GFcVen5Ju3qJDhRP69Q4Vd8Wbct6B", "confirmation_code": "cfrm38V8kEzECGczWJmEoGuYfkoamcmVij3tHUhD6DEEquSRXp61HzhnT8jwQwBBZiKs9Jg4LXZ", "public_key": "04597967956e7f4c0e13ed7cd98baa9d7697a7f685d4347168e4a011c5fe6ba628e06ef89587c17afb5504" "4336e44648dfa944ca85a4af0a7b28c29d4eefd0da92", - "compressed": False, "address": "1KRg2YJxuHiNcqfp9gVpkgRFhcvALy1zgk"} + "compressed": False, "address": "1KRg2YJxuHiNcqfp9gVpkgRFhcvALy1zgk", "network": "bitcoin"}, + {"intermediate_passphrase": "passphrasedYVZ6EdRqSmcHHsYWmJ7wzWcWYuDQmf9EGH9Pnrv67eHy4qswaAGGc8Et3eeGp", + "seed": "9b6cad86daddae99ac3b76c1e47e61bc7f4665d02e10c290", + "encrypted_wif": "6PgEuJVC5CJV4m9f5NgmW1MQCV56XyQ3ZASqckdz4s3PAznxKRi9H6JW5c", + "confirmation_code": "cfrm38V8AorwLxaG1GThhCDdS2Av74pRkojnePAS69nscD93DDVchgcjg1o8mxpXSJRaZbXFUYv", + "public_key": "04597967956e7f4c0e13ed7cd98baa9d7697a7f685d4347168e4a011c5fe6ba628e06ef89587c17afb5504" + "4336e44648dfa944ca85a4af0a7b28c29d4eefd0da92", + "compressed": False, "address": "LdedHkcnywxRseMyKpV82hV1uqHSU2m1ez", "network": "litecoin"} ] for ew in create_new_encrypted_wif: - res = bip38_create_new_encrypted_wif(ew["intermediate_passphrase"], ew["compressed"], ew["seed"]) + res = bip38_create_new_encrypted_wif(ew["intermediate_passphrase"], ew["compressed"], ew["seed"], + network=ew["network"]) self.assertEqual(res['encrypted_wif'], ew['encrypted_wif']) self.assertEqual(res['confirmation_code'], ew['confirmation_code']) self.assertEqual(res['address'], ew['address']) From 75afa630ae0eea101d2b0d16c51e327a64eb23ee Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 11 Mar 2024 20:32:38 +0100 Subject: [PATCH 140/207] Add more inverse key unittests --- tests/test_keys.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_keys.py b/tests/test_keys.py index f29b78f3..057ca558 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -101,10 +101,13 @@ def test_keys_inverse2(self): k = HDKey(pkwif, network='litecoin', witness_type='p2sh-segwit') pub_k = k.public() - kpi = pub_k.inverse() - self.assertEqual(kpi.address(), "MQVYsZ5o5uhN2X6QMbu9RVu5YADiq859MY") - self.assertEqual(kpi.witness_type, 'p2sh-segwit') - self.assertEqual(kpi.network.name, 'litecoin') + pub_k_inv = pub_k.inverse() + self.assertEqual(pub_k_inv.address(), "MQVYsZ5o5uhN2X6QMbu9RVu5YADiq859MY") + self.assertEqual(pub_k_inv.witness_type, 'p2sh-segwit') + self.assertEqual(pub_k_inv.network.name, 'litecoin') + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k_inv.address()) + self.assertEqual((-k).address(), k.inverse().address()) def test_dict_and_json_outputs(self): k = HDKey() From 9b7ff64573ea7c737dbc373f93e322b64a798177 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 11 Mar 2024 21:57:15 +0100 Subject: [PATCH 141/207] Add multi witnesstype unittest with passphrases --- tests/test_wallets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1259fbe5..1caafef6 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2709,3 +2709,16 @@ def test_wallet_mixed_witness_type_create(self): self.assertListEqual(sorted(w.witness_types()), ['legacy', 'p2sh-segwit', 'segwit']) self.assertListEqual(sorted(w.witness_types(account_id=101)), ['p2sh-segwit', 'segwit']) self.assertListEqual(w.witness_types(network='litecoin'), ['segwit']) + + def test_wallet_mixed_witness_types_passphrase(self): + p1 = 'advance upset milk quit sword tide pumpkin unit weekend denial tobacco alien' + p2 = 'danger aspect north choose run bean short race prepare receive armed burst' + w = Wallet.create('multiwitnessmultisigtest', keys=[p1, p2], cosigner_id=0, db_uri=self.database_uri) + w.new_key() + w.new_key(network='litecoin', witness_type='p2sh-segwit') + w.new_key(witness_type='legacy') + w.new_key(witness_type='p2sh-segwit') + expected_addresslist = \ + ['39h96ozh8F8W2sVrc2EhEbFwwdRoLHJAfB', '3LdJC6MSmFqKrn2WrxRfhd8DYkYYr8FNDr', + 'MTSW4eC7xJiyp4YjwGZqpGmubsdm28Cdvc', 'bc1qgw8rg0057q9fmupx7ru6vtkxzy03gexc9ljycagj8z3hpzdfg7usvu56dp'] + self.assertListEqual(sorted(w.addresslist()), expected_addresslist) \ No newline at end of file From 9732b2be2d9594d4c69c2d631479d16d674c813d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 12 Mar 2024 16:10:26 +0100 Subject: [PATCH 142/207] Fix issue with parsing nonstandard input scripts --- bitcoinlib/config/config.py | 3 +++ bitcoinlib/transactions.py | 12 +++++++++--- tests/test_transactions.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 9ca751a7..6b3ad66e 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -82,6 +82,9 @@ 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), 'locktime_csv': ('unlocking', ['locktime_csv', op.op_checksequenceverify, op.op_drop], []), + # + # List of nonstandard scripts, use for blockchain parsing. Must begin with 'nonstandard' + 'nonstandard_0001': ('unlocking', [op.op_0], []), } SIGHASH_ALL = 1 diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 02fdf274..e3372d3c 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -381,9 +381,12 @@ def parse(cls, raw, witness_type='segwit', index_n=0, strict=True, network=DEFAU output_n = raw.read(4)[::-1] unlocking_script_size = read_varbyteint(raw) unlocking_script = raw.read(unlocking_script_size) + script_type = None + # TODO - handle non-standard input script b'\1\0', # see tx 38cf5779d1c5ca32b79cd5052b54e824102e878f041607d3b962038f5a8cf1ed - # if unlocking_script_size == 1 and unlocking_script == b'\0': + if unlocking_script_size == 1 and unlocking_script == b'\0': + script_type = 'nonstandard_0001' inp_type = 'legacy' if witness_type == 'segwit' and not unlocking_script_size: @@ -391,7 +394,8 @@ def parse(cls, raw, witness_type='segwit', index_n=0, strict=True, network=DEFAU sequence_number = raw.read(4) return Input(prev_txid=prev_hash, output_n=output_n, unlocking_script=unlocking_script, - witness_type=inp_type, sequence=sequence_number, index_n=index_n, strict=strict, network=network) + witness_type=inp_type, sequence=sequence_number, index_n=index_n, strict=strict, network=network, + script_type=script_type) def update_scripts(self, hash_type=SIGHASH_ALL): """ @@ -481,7 +485,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): elif self.script_type == 'p2tr': # segwit_v1 self.redeemscript = self.witnesses[0] # FIXME: Address cannot be known without looking at previous transaction - elif self.script_type not in ['coinbase', 'unknown'] and self.strict: + elif self.script_type[:11] not in ['coinbase', 'unknown', 'nonstandard'] and self.strict: raise TransactionError("Unknown unlocking script type %s for input %d" % (self.script_type, self.index_n)) if addr_data and not self.address: self.address = Address(hashed_data=addr_data, encoding=self.encoding, network=self.network, @@ -1571,6 +1575,8 @@ def raw(self, sign_id=None, hash_type=SIGHASH_ALL, witness_type=None): else: r_witness += b'\0' if sign_id is None: + if i.script_type == 'nonstandard_0001': + r += b'\1' r += varstr(i.unlocking_script) elif sign_id == i.index_n: r += varstr(i.unlocking_script_unsigned) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 9aabe40c..a724b018 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1279,6 +1279,15 @@ def test_transaction_p2tr_input_litecoin(self): self.assertEqual(t.inputs[0].witnesses[1].hex(), witness_1) self.assertEqual(t.inputs[0].witnesses[2].hex(), witness_2) + def test_transaction_non_standard_input_script_0001(self): + txid = '38cf5779d1c5ca32b79cd5052b54e824102e878f041607d3b962038f5a8cf1ed' + traw = \ + '0100000001bf9fe5c8a75a849345150a323ce50466827e4df1f2626eac7e30122dd6d1a812000000000100ffffffff0180380100000000001976a9148f0da0329aa2638c17fda841347f2ed737b6e40088ac00000000' + t = Transaction.parse_hex(traw) + self.assertEqual(t.inputs[0].script_type, 'nonstandard_0001') + self.assertEqual(t.txid, txid) + self.assertEqual(traw, t.raw_hex()) + class TestTransactionsMultisigSoroush(unittest.TestCase): # Source: Example from @@ -1867,3 +1876,4 @@ def test_transaction_segwit_vsize(self): t = Transaction.parse_hex(rawtx) self.assertEqual(t.vsize, 612) self.assertEqual(t.weight_units, 2445) + self.assertEqual(t.raw_hex(), rawtx) From b1e5d9883de73abe6cdb59ddc4ed50e975e0a6fa Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 12 Mar 2024 22:24:51 +0100 Subject: [PATCH 143/207] Use anti-fee-snipping and set locktime to current block+1 by default in wallets --- bitcoinlib/db.py | 1 + bitcoinlib/transactions.py | 4 ++-- bitcoinlib/wallets.py | 34 ++++++++++++++++++++++++---------- tests/bitcoinlib_encrypted.db | Bin 139264 -> 139264 bytes 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 5a7089de..ba8486bb 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -248,6 +248,7 @@ class DbWallet(Base): "* If accounts are used, the account level must be 3. I.e.: m/purpose/coin_type/account/ " "* All keys must be hardened, except for change, address_index or cosigner_id " " Max length of path is 8 levels") + anti_fee_snipping = Column(Boolean, default=True, doc="Set default locktime in transactions to avoid fee-snipping") default_account_id = Column(Integer, doc="ID of default account for this wallet if multiple accounts are used") __table_args__ = ( diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index e3372d3c..95741430 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1089,9 +1089,9 @@ def __init__(self, inputs=None, outputs=None, locktime=0, version=None, :type version: bytes, int :param network: Network, leave empty for default network :type network: str, Network - :param fee: Fee in smallest denominator (ie Satoshi) for complete transaction + :param fee: Fee in the smallest denominator (ie Satoshi) for complete transaction :type fee: int - :param fee_per_kb: Fee in smallest denominator per kilobyte. Specify when exact transaction size is not known. + :param fee_per_kb: Fee in the smallest denominator per kilobyte. Specify when exact transaction size is not known. :type fee_per_kb: int :param size: Transaction size in bytes :type size: int diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 4f77571d..bb65ac93 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -874,7 +874,8 @@ def store(self): wallet_id=self.hdwallet.wallet_id, txid=bytes.fromhex(self.txid), block_height=self.block_height, size=self.size, confirmations=self.confirmations, date=self.date, fee=self.fee, status=self.status, input_total=self.input_total, output_total=self.output_total, network_name=self.network.name, - raw=self.rawtx, verified=self.verified, account_id=self.account_id) + raw=self.rawtx, verified=self.verified, account_id=self.account_id, locktime=self.locktime, + version=self.version_int, coinbase=self.coinbase, index=self.index) sess.add(new_tx) self.hdwallet._commit() txidn = new_tx.id @@ -890,6 +891,7 @@ def store(self): db_tx.network_name = self.network.name if self.network.name else db_tx.name db_tx.raw = self.rawtx if self.rawtx else db_tx.raw db_tx.verified = self.verified + db_tx.locktime = self.locktime self.hdwallet._commit() assert txidn @@ -1041,8 +1043,8 @@ class Wallet(object): @classmethod def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_id, sort_keys, - witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, db_uri, db_cache_uri, - db_password): + witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, + anti_fee_snipping, db_uri, db_cache_uri, db_password): db = Db(db_uri, db_password) session = db.session @@ -1082,7 +1084,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, - key_path=key_path) + key_path=key_path, anti_fee_snipping=anti_fee_snipping) session.add(new_wallet) session.commit() new_wallet_id = new_wallet.id @@ -1121,7 +1123,8 @@ def _commit(self): @classmethod def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, - cosigner_id=None, key_path=None, db_uri=None, db_cache_uri=None, db_password=None): + cosigner_id=None, key_path=None, anti_fee_snipping=True, db_uri=None, db_cache_uri=None, + db_password=None): """ Create Wallet and insert in database. Generate masterkey or import key when specified. @@ -1192,6 +1195,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 * All keys must be hardened, except for change, address_index or cosigner_id * Max length of path is 8 levels :type key_path: list, str + :param anti_fee_snipping: Set default locktime in transactions as current block height + 1 to avoid fee-snipping. Default is True + :type anti_fee_snipping: boolean :param db_uri: URI of the database for wallets, wallet transactions and keys :type db_uri: str :param db_cache_uri: URI of the cache database. If not specified the default cache database is used when using sqlite, for other databasetypes the cache database is merged with the wallet database (db_uri) @@ -1310,7 +1315,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, - key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + anti_fee_snipping=anti_fee_snipping, key_path=main_key_path, db_uri=db_uri, + db_cache_uri=db_cache_uri, db_password=db_password) if multisig: wlt_cos_id = 0 @@ -1328,7 +1334,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 purpose=hdpm.purpose, scheme=scheme, parent_id=hdpm.wallet_id, sort_keys=sort_keys, witness_type=hdpm.witness_type, encoding=encoding, multisig=True, sigs_required=None, cosigner_id=wlt_cos_id, key_path=c_key_path, - db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + anti_fee_snipping=anti_fee_snipping, db_uri=db_uri, db_cache_uri=db_cache_uri, + db_password=db_password) hdpm.cosigner.append(w) wlt_cos_id += 1 # hdpm._dbwallet = hdpm.session.query(DbWallet).filter(DbWallet.id == hdpm.wallet_id) @@ -1414,6 +1421,7 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.depth_public_master = self.key_path.index(hardened_keys[-1]) self.key_depth = len(self.key_path) - 1 self.last_updated = None + self.anti_fee_snipping = db_wlt.anti_fee_snipping else: raise WalletError("Wallet '%s' not found, please specify correct wallet ID or name." % wallet) @@ -3706,6 +3714,12 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco transaction.add_output(value, addr) srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + + if not locktime and self.anti_fee_snipping: + blockcount = srv.blockcount() + if blockcount: + transaction.locktime = blockcount + 1 + transaction.fee_per_kb = None if isinstance(fee, int): fee_estimate = fee @@ -3725,10 +3739,10 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco # Add inputs sequence = 0xffffffff - if 0 < transaction.locktime < 0xffffffff: - sequence = SEQUENCE_ENABLE_LOCKTIME - elif replace_by_fee: + if replace_by_fee: sequence = SEQUENCE_REPLACE_BY_FEE + elif 0 < transaction.locktime < 0xffffffff: + sequence = SEQUENCE_ENABLE_LOCKTIME amount_total_input = 0 if input_arr is None: selected_utxos = self.select_inputs(amount_total_output + fee_estimate, transaction.network.dust_amount, diff --git a/tests/bitcoinlib_encrypted.db b/tests/bitcoinlib_encrypted.db index 12d45fe411f4a47e39dc5639ecdd84e5cbc4047e..9f965e9db4e43b33f68dedc979be75e6c2f27453 100644 GIT binary patch delta 171 zcmZoTz|nAkV}i7x3IhX!5)i|H=tLc3Ruu-lvd+eottpIi?X`h2LJTDg{6YMe`MUWP z8A|wKI34(m`M7u=^6ubx!Lf)Vn1h}D2zx&Br-_XoTy52??BdeWjJ>|we|s@5Wt2?J zE6I#cOHGY0&dV$)$jnPuNXpO8Nlna~{=uD5Z~GY^#tz2q?GB7tTx_~*Y~qg6(=Yln QN^CFkV{Btw7QlD`04C}$+W-In delta 152 zcmZoTz|nAkV}i7xG6MsH5)i|H$V44uR%HggaIeOcttpIi?REKoFbFZUGw^%ypXO`k zm*o1$7s7g#>jc+)t}-rP)>>9`mKQ8@SRz?i7}}XnPi$0RVPRpeo!Dr){gX3e78h4r u0xP??v@~O{`1U#n#-)tYwYbB@v From e125827f090fd3304275b5a3815518a728c40b1a Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Wed, 13 Mar 2024 18:31:46 +0100 Subject: [PATCH 144/207] Add unittests for anti fee snipping for wallets --- bitcoinlib/wallets.py | 7 ++++--- tests/test_wallets.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index bb65ac93..25c935df 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -109,7 +109,7 @@ def wallet_exists(wallet, db_uri=None, db_password=None): def wallet_create_or_open( name, keys='', owner='', network=None, account_id=0, purpose=None, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, cosigner_id=None, - key_path=None, db_uri=None, db_cache_uri=None, db_password=None): + key_path=None, anti_fee_snipping=True, db_uri=None, db_cache_uri=None, db_password=None): """ Create a wallet with specified options if it doesn't exist, otherwise just open @@ -124,7 +124,8 @@ def wallet_create_or_open( else: return Wallet.create(name, keys, owner, network, account_id, purpose, scheme, sort_keys, password, witness_type, encoding, multisig, sigs_required, cosigner_id, - key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) + key_path, anti_fee_snipping, db_uri=db_uri, db_cache_uri=db_cache_uri, + db_password=db_password) def wallet_delete(wallet, db_uri=None, force=False, db_password=None): @@ -1195,7 +1196,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 * All keys must be hardened, except for change, address_index or cosigner_id * Max length of path is 8 levels :type key_path: list, str - :param anti_fee_snipping: Set default locktime in transactions as current block height + 1 to avoid fee-snipping. Default is True + :param anti_fee_snipping: Set default locktime in transactions as current block height + 1 to avoid fee-snipping. Default is True, which will make the network more secure. You could disable it to avoid transaction fingerprinting. :type anti_fee_snipping: boolean :param db_uri: URI of the database for wallets, wallet transactions and keys :type db_uri: str diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1caafef6..715b1c3b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2266,6 +2266,24 @@ def test_wallet_transaction_replace_by_fee(self): self.assertTrue(t2.replace_by_fee) self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) + def test_wallet_anti_fee_snipping(self): + w = wallet_create_or_open('antifeesnippingtestwallet', network='testnet', anti_fee_snipping=True, + db_uri=self.database_uri) + w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + block_height = Service(network='testnet').blockcount() + self.assertEqual(t.locktime, block_height+1) + + w2 = wallet_create_or_open('antifeesnippingtestwallet2', network='testnet', anti_fee_snipping=True) + w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) + self.assertEqual(t.locktime, 1901070183) + + w3 = wallet_create_or_open('antifeesnippingtestwallet3', network='testnet', anti_fee_snipping=False) + w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + self.assertEqual(t.locktime, 0) + @classmethod def tearDownClass(cls): del cls.wallet From a05bf88543a87ebedb4d5e9798d70e3dcea59be7 Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Wed, 13 Mar 2024 20:36:34 +0100 Subject: [PATCH 145/207] Add anti-fee-sniping option to commandline wallet --- bitcoinlib/db.py | 2 +- bitcoinlib/tools/clw.py | 7 +++++-- bitcoinlib/wallets.py | 22 +++++++++++----------- tests/test_tools.py | 14 +++++++++----- tests/test_wallets.py | 9 ++++----- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index ba8486bb..01a1c9cf 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -248,7 +248,7 @@ class DbWallet(Base): "* If accounts are used, the account level must be 3. I.e.: m/purpose/coin_type/account/ " "* All keys must be hardened, except for change, address_index or cosigner_id " " Max length of path is 8 levels") - anti_fee_snipping = Column(Boolean, default=True, doc="Set default locktime in transactions to avoid fee-snipping") + anti_fee_sniping = Column(Boolean, default=True, doc="Set default locktime in transactions to avoid fee-sniping") default_account_id = Column(Integer, doc="ID of default account for this wallet if multiple accounts are used") __table_args__ = ( diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index e3df4e75..27bf3a15 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -89,7 +89,9 @@ def parse_args(): parser_new.add_argument('--yes', '-y', action='store_true', default=False, help='Non-interactive mode, does not prompt for confirmation') parser_new.add_argument('--quiet', '-q', action='store_true', - help='Quit mode, no output writen to console.') + help='Quiet mode, no output writen to console.') + parser_new.add_argument('--disable-anti-fee-sniping', action='store_true', default=False, + help='Disable anti-fee-sniping, and set locktime in all transaction to zero.') group_wallet = parser.add_argument_group("Wallet Actions") group_wallet.add_argument('--wallet-remove', action='store_true', @@ -188,7 +190,8 @@ def create_wallet(wallet_name, args, db_uri, output_to): passphrase = get_passphrase(args.passphrase_strength, args.yes, args.quiet) key_list.append(HDKey.from_passphrase(passphrase, network=args.network)) return Wallet.create(wallet_name, key_list, sigs_required=sigs_required, network=args.network, - cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type) + cosigner_id=args.cosigner_id, db_uri=db_uri, witness_type=args.witness_type, + anti_fee_sniping=not(args.disable_anti_fee_sniping)) elif args.create_from_key: from bitcoinlib.keys import get_key_format import_key = args.create_from_key diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 25c935df..f7a8ed5a 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -109,7 +109,7 @@ def wallet_exists(wallet, db_uri=None, db_password=None): def wallet_create_or_open( name, keys='', owner='', network=None, account_id=0, purpose=None, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, cosigner_id=None, - key_path=None, anti_fee_snipping=True, db_uri=None, db_cache_uri=None, db_password=None): + key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, db_password=None): """ Create a wallet with specified options if it doesn't exist, otherwise just open @@ -124,7 +124,7 @@ def wallet_create_or_open( else: return Wallet.create(name, keys, owner, network, account_id, purpose, scheme, sort_keys, password, witness_type, encoding, multisig, sigs_required, cosigner_id, - key_path, anti_fee_snipping, db_uri=db_uri, db_cache_uri=db_cache_uri, + key_path, anti_fee_sniping, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) @@ -1045,7 +1045,7 @@ class Wallet(object): @classmethod def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_id, sort_keys, witness_type, encoding, multisig, sigs_required, cosigner_id, key_path, - anti_fee_snipping, db_uri, db_cache_uri, db_password): + anti_fee_sniping, db_uri, db_cache_uri, db_password): db = Db(db_uri, db_password) session = db.session @@ -1085,7 +1085,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ new_wallet = DbWallet(name=name, owner=owner, network_name=network, purpose=purpose, scheme=scheme, sort_keys=sort_keys, witness_type=witness_type, parent_id=parent_id, encoding=encoding, multisig=multisig, multisig_n_required=sigs_required, cosigner_id=cosigner_id, - key_path=key_path, anti_fee_snipping=anti_fee_snipping) + key_path=key_path, anti_fee_sniping=anti_fee_sniping) session.add(new_wallet) session.commit() new_wallet_id = new_wallet.id @@ -1124,7 +1124,7 @@ def _commit(self): @classmethod def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0, scheme='bip32', sort_keys=True, password='', witness_type=None, encoding=None, multisig=None, sigs_required=None, - cosigner_id=None, key_path=None, anti_fee_snipping=True, db_uri=None, db_cache_uri=None, + cosigner_id=None, key_path=None, anti_fee_sniping=True, db_uri=None, db_cache_uri=None, db_password=None): """ Create Wallet and insert in database. Generate masterkey or import key when specified. @@ -1196,8 +1196,8 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 * All keys must be hardened, except for change, address_index or cosigner_id * Max length of path is 8 levels :type key_path: list, str - :param anti_fee_snipping: Set default locktime in transactions as current block height + 1 to avoid fee-snipping. Default is True, which will make the network more secure. You could disable it to avoid transaction fingerprinting. - :type anti_fee_snipping: boolean + :param anti_fee_sniping: Set default locktime in transactions as current block height + 1 to avoid fee-sniping. Default is True, which will make the network more secure. You could disable it to avoid transaction fingerprinting. + :type anti_fee_sniping: boolean :param db_uri: URI of the database for wallets, wallet transactions and keys :type db_uri: str :param db_cache_uri: URI of the cache database. If not specified the default cache database is used when using sqlite, for other databasetypes the cache database is merged with the wallet database (db_uri) @@ -1316,7 +1316,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 hdpm = cls._create(name, key, owner=owner, network=network, account_id=account_id, purpose=purpose, scheme=scheme, parent_id=None, sort_keys=sort_keys, witness_type=witness_type, encoding=encoding, multisig=multisig, sigs_required=sigs_required, cosigner_id=cosigner_id, - anti_fee_snipping=anti_fee_snipping, key_path=main_key_path, db_uri=db_uri, + anti_fee_sniping=anti_fee_sniping, key_path=main_key_path, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) if multisig: @@ -1335,7 +1335,7 @@ def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0 purpose=hdpm.purpose, scheme=scheme, parent_id=hdpm.wallet_id, sort_keys=sort_keys, witness_type=hdpm.witness_type, encoding=encoding, multisig=True, sigs_required=None, cosigner_id=wlt_cos_id, key_path=c_key_path, - anti_fee_snipping=anti_fee_snipping, db_uri=db_uri, db_cache_uri=db_cache_uri, + anti_fee_sniping=anti_fee_sniping, db_uri=db_uri, db_cache_uri=db_cache_uri, db_password=db_password) hdpm.cosigner.append(w) wlt_cos_id += 1 @@ -1422,7 +1422,7 @@ def __init__(self, wallet, db_uri=None, db_cache_uri=None, session=None, main_ke self.depth_public_master = self.key_path.index(hardened_keys[-1]) self.key_depth = len(self.key_path) - 1 self.last_updated = None - self.anti_fee_snipping = db_wlt.anti_fee_snipping + self.anti_fee_sniping = db_wlt.anti_fee_sniping else: raise WalletError("Wallet '%s' not found, please specify correct wallet ID or name." % wallet) @@ -3716,7 +3716,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) - if not locktime and self.anti_fee_snipping: + if not locktime and self.anti_fee_sniping: blockcount = srv.blockcount() if blockcount: transaction.locktime = blockcount + 1 diff --git a/tests/test_tools.py b/tests/test_tools.py index 03be68b9..b37998a0 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -87,6 +87,7 @@ def test_tools_clw_create_multisig_wallet(self): ] cmd_wlt_create = "%s %s new -w testms -m 2 2 %s -r -n testnet -d %s -o 0" % \ (self.python_executable, self.clw_executable, ' '.join(key_list), self.database_uri) + print(cmd_wlt_create) cmd_wlt_delete = "%s %s -w testms --wallet-remove -d %s" % \ (self.python_executable, self.clw_executable, self.database_uri) output_wlt_create = "2NBrLTapyFqU4Wo29xG4QeEt8kn38KVWRR" @@ -276,11 +277,14 @@ def test_tools_wallet_multisig_cosigners(self): 'ZYXRnhWiS3jjHqgeZ') pub_key3 = ('BC11mYrL5yBtMgaYxHEUg3anvLX3gcLi8hbtwbjymReCgGiP6hYifVMi96M3ejtvZpZbDvetBfbzgRxmu22ZkqP2i7yhFge' 'mSkHp7BRhoDubrQvs') - cmd_wlt_create1 = "%s %s new -w wlt_multisig_2_3_A -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ - (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.database_uri) + cmd_wlt_create1 = ("%s %s new -w wlt_multisig_2_3_A -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q " + "--disable-anti-fee-sniping") % \ + (self.python_executable, self.clw_executable, pk1, pub_key2, pub_key3, self.database_uri) Popen(cmd_wlt_create1, stdin=PIPE, stdout=PIPE, shell=True).communicate() - cmd_wlt_create2 = "%s %s new -w wlt_multisig_2_3_B -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q" % \ - (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.database_uri) + cmd_wlt_create2 = ("%s %s new -w wlt_multisig_2_3_B -m 2 3 %s %s %s -d %s -n bitcoinlib_test -q " + "--disable-anti-fee-sniping") % \ + (self.python_executable, self.clw_executable, pub_key1, pub_key2, pk3, self.database_uri) + print(cmd_wlt_create2) Popen(cmd_wlt_create2, stdin=PIPE, stdout=PIPE, shell=True).communicate() cmd_wlt_receive1 = "%s %s -w wlt_multisig_2_3_A -d %s -r -o 1 -q" % \ @@ -313,7 +317,7 @@ def test_tools_wallet_multisig_cosigners(self): self.assertIn("'verified': True,", response) filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'import_test.tx') - sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file %s" % \ + sign_import_tx_file = "%s %s -w wlt_multisig_2_3_B -d %s -o 1 --import-tx-file %s" % \ (self.python_executable, self.clw_executable, self.database_uri, filename) output = Popen(sign_import_tx_file, stdin=PIPE, stdout=PIPE, shell=True).communicate() response2 = normalize_string(output[0]) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 715b1c3b..40b8622c 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2266,20 +2266,19 @@ def test_wallet_transaction_replace_by_fee(self): self.assertTrue(t2.replace_by_fee) self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) - def test_wallet_anti_fee_snipping(self): - w = wallet_create_or_open('antifeesnippingtestwallet', network='testnet', anti_fee_snipping=True, - db_uri=self.database_uri) + def test_wallet_anti_fee_sniping(self): + w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) block_height = Service(network='testnet').blockcount() self.assertEqual(t.locktime, block_height+1) - w2 = wallet_create_or_open('antifeesnippingtestwallet2', network='testnet', anti_fee_snipping=True) + w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) self.assertEqual(t.locktime, 1901070183) - w3 = wallet_create_or_open('antifeesnippingtestwallet3', network='testnet', anti_fee_snipping=False) + w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) self.assertEqual(t.locktime, 0) From bdba7a53e7becac4c891f94ca434600a72e14aef Mon Sep 17 00:00:00 2001 From: Lennart Jongeneel Date: Wed, 13 Mar 2024 20:48:52 +0100 Subject: [PATCH 146/207] Fix typo in database: sniping --- tests/bitcoinlib_encrypted.db | Bin 139264 -> 147456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/bitcoinlib_encrypted.db b/tests/bitcoinlib_encrypted.db index 9f965e9db4e43b33f68dedc979be75e6c2f27453..c28e2a77298aa7cf62d0f566934bc2b5d8f0dec5 100644 GIT binary patch delta 1137 zcmZ`(L1@!Z7=EvvYf^2q>R}xfe471q|$ z^)L{nz3$-ImnIF##25aC{Q3Ux|NieM-Fd_9?C^Vu zSPB4;WD#N!U=eSPO&v>t3o_gtJS;_2yS=o%)%d$KE`vgvs!@9VChN9PHl?y$qK7X97P#J@8c6Ivx#wZ8aA=^;P(Y?-ORghgS1lV0^f}Qh;NPAB?J4(s=^`Gm7rTn#g5fN%4 zQdQM1(1K{XQjzpJD&zWCH%X)lMh$;8x|J#B<}$_9<%zUgStZKtDz=MgjbQyhHPaJm zhZ&nSs+YxX2M63EzT_d+@p=R6n8?^b#I7-H2Cb@V)WE(*&SW;gkT)@t?>labX-7v* zF$p#d(X4AO)CJjM=MZB3X-Od)UalT$vPV^s6e2R~M33h_LG8O4c)HQ`%%HkyVGR#s zc41*YpD8%gNo;7-ONEd_=9m^FEOskGsAB{vzMO?>NG>D?v{>9N6r7D()tyQMPFm>jf* Date: Thu, 14 Mar 2024 21:12:29 +0100 Subject: [PATCH 147/207] Fix unittest with incorrect blockcounts --- tests/test_services.py | 26 ++++++++++---------------- tests/test_wallets.py | 3 ++- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index 1967c172..a6dc372a 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -603,22 +603,16 @@ def test_service_network_litecoin_legacy(self): self.assertIn(txid, [utxo['txid'] for utxo in utxos]) def test_service_blockcount(self): - srv = ServiceTest(min_providers=3) - n_blocks = None - for provider in srv.results: - if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - n_blocks = srv.results[provider] - - # Test Litecoin network - srv = ServiceTest(min_providers=3, network='litecoin') - n_blocks = None - for provider in srv.results: - if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000, - msg="Provider %s value %d != %d" % (provider, srv.results[provider], n_blocks)) - n_blocks = srv.results[provider] + for nw in ['bitcoin', 'litecoin', 'testnet']: + srv = ServiceTest(min_providers=3, cache_uri='', network=nw) + srv.blockcount() + n_blocks = None + for provider in srv.results: + if n_blocks is not None: + self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000 if nw == 'testnet' else 3, + msg="Network %s, provider %s value %d != %d" % + (nw, provider, srv.results[provider], n_blocks)) + n_blocks = srv.results[provider] def test_service_max_providers(self): srv = ServiceTest(max_providers=1, cache_uri='') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 40b8622c..9aa09b57 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2271,7 +2271,8 @@ def test_wallet_anti_fee_sniping(self): w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) block_height = Service(network='testnet').blockcount() - self.assertEqual(t.locktime, block_height+1) + # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta to 5000 + self.assertAlmostEqual(t.locktime, block_height+1, delta=5000) w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) From 54a2ae16551e2b08cdb0b2291fb6ff3d3445f62b Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 14 Mar 2024 21:34:03 +0100 Subject: [PATCH 148/207] Increase delta in blockcount unittests --- tests/test_services.py | 2 +- tests/test_wallets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index a6dc372a..19d56dbb 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -609,7 +609,7 @@ def test_service_blockcount(self): n_blocks = None for provider in srv.results: if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=5000 if nw == 'testnet' else 3, + self.assertAlmostEqual(srv.results[provider], n_blocks, delta=10000 if nw == 'testnet' else 3, msg="Network %s, provider %s value %d != %d" % (nw, provider, srv.results[provider], n_blocks)) n_blocks = srv.results[provider] diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 9aa09b57..547960c4 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2272,7 +2272,7 @@ def test_wallet_anti_fee_sniping(self): t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) block_height = Service(network='testnet').blockcount() # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta to 5000 - self.assertAlmostEqual(t.locktime, block_height+1, delta=5000) + self.assertAlmostEqual(t.locktime, block_height+1, delta=10000) w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) From 63d8bcf06dc37b133614211124ded250f44ccd0f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 18 Mar 2024 13:54:40 +0100 Subject: [PATCH 149/207] Fix script issues, test with CLTV scripts --- bitcoinlib/config/config.py | 2 ++ bitcoinlib/scripts.py | 68 +++++++++++++++++++------------------ bitcoinlib/transactions.py | 52 ++++++++++++++++------------ bitcoinlib/wallets.py | 5 +-- tests/test_script.py | 4 +-- tests/test_transactions.py | 17 ++++++---- 6 files changed, 83 insertions(+), 65 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 6b3ad66e..ab4ba75e 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -67,6 +67,8 @@ 'p2tr': ('locking', ['op_n', 'data'], [32]), 'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), 'p2pk': ('locking', ['key', op.op_checksig], []), + 'locktime_cltv_script': ('locking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop, op.op_dup, + op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]), 'nulldata': ('locking', [op.op_return, 'data'], [0]), 'nulldata_1': ('locking', [op.op_return, op.op_0], []), 'nulldata_2': ('locking', [op.op_return], []), diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 0170f7bd..124f9f6c 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -136,7 +136,7 @@ def get_data_type(data): class Script(object): def __init__(self, commands=None, message=None, script_types='', is_locking=True, keys=None, signatures=None, - blueprint=None, tx_data=None, public_hash=b'', sigs_required=None, redeemscript=b'', + blueprint=None, env_data=None, public_hash=b'', sigs_required=None, redeemscript=b'', hash_type=SIGHASH_ALL): """ Create a Script object with specified parameters. Use parse() method to create a Script from raw hex @@ -173,8 +173,8 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True :type signatures: list of Signature :param blueprint: Simplified version of script, normally generated by Script object :type blueprint: list of str - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param public_hash: Public hash of key or redeemscript used to create scripts :type public_hash: bytes :param sigs_required: Nubmer of signatures required to create multisig script @@ -193,13 +193,13 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self.keys = keys if keys else [] self.signatures = signatures if signatures else [] self._blueprint = blueprint if blueprint else [] - self.tx_data = {} if not tx_data else tx_data + self.env_data = {} if not env_data else env_data self.sigs_required = sigs_required if sigs_required else len(self.keys) if len(self.keys) else 1 self.redeemscript = redeemscript self.public_hash = public_hash self.hash_type = hash_type - if not self.commands and self.script_types and (self.keys or self.signatures or self.public_hash): + if not self.commands and self.script_types: # and (self.keys or self.signatures or self.public_hash): for st in self.script_types: st_values = SCRIPT_TYPES[st] script_template = st_values[1] @@ -217,6 +217,8 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True command = [sig_n_and_m.pop() + 80] elif tc == 'redeemscript': command = [self.redeemscript] + elif tc in self.env_data: + command = [env_data[tc]] if not command or command == [b'']: raise ScriptError("Cannot create script, please supply %s" % (tc if tc != 'data' else 'public key hash')) @@ -238,7 +240,7 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self._blueprint.append('data-%d' % len(c)) @classmethod - def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse(cls, script, message=None, env_data=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -251,8 +253,8 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: BytesIO, bytes, str :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -267,10 +269,10 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0): elif isinstance(script, str): data_length = len(script) script = BytesIO(bytes.fromhex(script)) - return cls.parse_bytesio(script, message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(script, message, env_data, data_length, strict, _level) @classmethod - def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict=True, _level=0): + def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -278,8 +280,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict :type script: BytesIO :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param data_length: Length of script data if known. Supply if you can to increase efficiency and lower change of incorrect parsing :type data_length: int :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True @@ -297,8 +299,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict sigs_required = None # hash_type = SIGHASH_ALL # todo: check hash_type = None - if not tx_data: - tx_data = {} + if not env_data: + env_data = {} chb = script.read(1) ch = int.from_bytes(chb, 'big') @@ -385,7 +387,7 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict chb = script.read(1) ch = int.from_bytes(chb, 'big') - s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, tx_data=tx_data, + s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, env_data=env_data, hash_type=hash_type) script.seek(0) s._raw = script.read() @@ -412,15 +414,15 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict elif st == 'p2pkh' and len(s.commands) > 2: s.public_hash = s.commands[2] s.redeemscript = redeemscript if redeemscript else s.redeemscript - if s.redeemscript and 'redeemscript' not in s.tx_data: - s.tx_data['redeemscript'] = s.redeemscript + if s.redeemscript and 'redeemscript' not in s.env_data: + s.env_data['redeemscript'] = s.redeemscript s.sigs_required = sigs_required if sigs_required else s.sigs_required return s @classmethod - def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -433,8 +435,8 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: str :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -443,10 +445,10 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) // 2 - return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, strict, _level) @classmethod - def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): + def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -456,8 +458,8 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): :type script: bytes :param message: Signed message to verify, normally a transaction hash :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts - :type tx_data: dict + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -466,7 +468,7 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) - return cls.parse_bytesio(BytesIO(script), message, tx_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level) def __repr__(self): s_items = [] @@ -496,8 +498,8 @@ def __add__(self, other): self.signatures += other.signatures self._blueprint += other._blueprint self.script_types = _get_script_types(self._blueprint) - if other.tx_data and not self.tx_data: - self.tx_data = other.tx_data + if other.env_data and not self.env_data: + self.env_data = other.env_data if other.redeemscript and not self.redeemscript: self.redeemscript = other.redeemscript return self @@ -556,7 +558,7 @@ def serialize_list(self): clist.append(bytes(cmd)) return clist - def evaluate(self, message=None, tx_data=None): + def evaluate(self, message=None, env_data=None): """ Evaluate script, run all commands and check if it is valid @@ -577,12 +579,12 @@ def evaluate(self, message=None, tx_data=None): :param message: Signed message to verify, normally a transaction hash. Leave empty to use Script.message. If supplied Script.message will be ignored. :type message: bytes - :param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.tx_data. If supplied Script.tx_data will be ignored + :param data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.data. If supplied Script.data will be ignored :return bool: Valid or not valid """ self.message = self.message if message is None else message - self.tx_data = self.tx_data if tx_data is None else tx_data + self.env_data = self.env_data if env_data is None else env_data self.stack = Stack() commands = self.commands[:] @@ -609,12 +611,12 @@ def evaluate(self, message=None, tx_data=None): if method_name == 'op_checksig' or method_name == 'op_checksigverify': res = method(self.message) elif method_name == 'op_checkmultisig' or method_name == 'op_checkmultisigverify': - res = method(self.message, self.tx_data) + res = method(self.message, self.env_data) elif method_name == 'op_checklocktimeverify': res = self.stack.op_checklocktimeverify( - self.tx_data['sequence'], self.tx_data.get('locktime')) + self.env_data['sequence'], self.env_data.get('locktime')) elif method_name == 'op_checksequenceverify': - res = self.stack.op_checksequenceverify(self.tx_data['sequence'], self.tx_data['version']) + res = self.stack.op_checksequenceverify(self.env_data['sequence'], self.env_data['version']) else: res = method() if res is False: diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 95741430..55f204aa 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -185,8 +185,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :type value: int, Value, str :param double_spend: Is this input also spend in another transaction :type double_spend: bool - :param locktime_cltv: Check Lock Time Verify value. Script level absolute time lock for this input - :type locktime_cltv: int :param locktime_csv: Check Sequence Verify value :type locktime_csv: int :param key_path: Key path of input key as BIP32 string or list @@ -253,9 +251,9 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.locktime_csv = locktime_csv self.witness_type = witness_type if encoding is None: - self.encoding = 'base58' - if self.witness_type == 'segwit': - self.encoding = 'bech32' + self.encoding = 'bech32' + if self.witness_type == 'legacy' or self.witness_type == 'p2sh-segwit': + self.encoding = 'base58' else: self.encoding = encoding self.valid = None @@ -311,6 +309,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.witness_type = 'segwit' elif self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: self.witness_type = 'p2sh-segwit' + self.encoding = 'base58' else: self.witness_type = 'legacy' elif self.witness_type == 'segwit' and self.script_type == 'sig_pubkey' and encoding is None: @@ -494,8 +493,11 @@ def update_scripts(self, hash_type=SIGHASH_ALL): if self.locktime_cltv: self.unlocking_script_unsigned = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script_unsigned) - self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) - elif self.locktime_csv: + # if self.unlocking_script: + # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) + # if self.witness_type == 'segwit': + # self.witnesses.insert(0, script_add_locktime_cltv(self.locktime_cltv, b'')) + if self.locktime_csv: self.unlocking_script_unsigned = script_add_locktime_csv(self.locktime_csv, self.unlocking_script_unsigned) self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) return True @@ -573,7 +575,7 @@ def as_dict(self): 'sequence': self.sequence, 'signatures': [s.hex() for s in self.signatures], 'sigs_required': self.sigs_required, - 'locktime_cltv': self.locktime_cltv, + # 'locktime_cltv': self.locktime_cltv, 'locktime_csv': self.locktime_csv, 'public_hash': self.public_hash.hex(), 'script_code': self.script_code.hex(), @@ -1309,12 +1311,12 @@ def info(self): print(" Relative timelock for %d seconds" % (512 * (ti.sequence - SEQUENCE_LOCKTIME_TYPE_FLAG))) else: print(" Relative timelock for %d blocks" % ti.sequence) - if ti.locktime_cltv: - if ti.locktime_cltv & SEQUENCE_LOCKTIME_TYPE_FLAG: - print(" Check Locktime Verify (CLTV) for %d seconds" % - (512 * (ti.locktime_cltv - SEQUENCE_LOCKTIME_TYPE_FLAG))) - else: - print(" Check Locktime Verify (CLTV) for %d blocks" % ti.locktime_cltv) + # if ti.locktime_cltv: + # if ti.locktime_cltv & SEQUENCE_LOCKTIME_TYPE_FLAG: + # print(" Check Locktime Verify (CLTV) for %d seconds" % + # (512 * (ti.locktime_cltv - SEQUENCE_LOCKTIME_TYPE_FLAG))) + # else: + # print(" Check Locktime Verify (CLTV) for %d blocks" % ti.locktime_cltv) if ti.locktime_csv: if ti.locktime_csv & SEQUENCE_LOCKTIME_TYPE_FLAG: print(" Check Sequence Verify Timelock (CSV) for %d seconds" % @@ -1342,7 +1344,7 @@ def info(self): print("Confirmations: %s" % self.confirmations) print("Block: %s" % self.block_height) - def set_locktime_relative_blocks(self, blocks, input_index_n=0): + def set_locktime_relative_blocks(self, blocks, input_index_n=0, locktime=0): """ Set nSequence relative locktime for this transaction. The transaction will only be valid if the specified number of blocks has been mined since the previous UTXO is confirmed. @@ -1354,6 +1356,8 @@ def set_locktime_relative_blocks(self, blocks, input_index_n=0): :type blocks: int :param input_index_n: Index number of input for nSequence locktime :type input_index_n: int + :param locktime: Overwrite default locktime, must be lower than current network blockcount. If anti-fee-sniping is used in a Wallet this value is already filled in. + :type locktime: int :return: """ @@ -1364,10 +1368,11 @@ def set_locktime_relative_blocks(self, blocks, input_index_n=0): if blocks > SEQUENCE_LOCKTIME_MASK: raise TransactionError("Number of nSequence timelock blocks exceeds %d" % SEQUENCE_LOCKTIME_MASK) self.inputs[input_index_n].sequence = blocks - self.version_int = 2 + self.version_int = 2 if self.version_int < 2 else self.version_int + self.locktime = locktime if locktime else self.locktime self.sign_and_update(index_n=input_index_n) - def set_locktime_relative_time(self, seconds, input_index_n=0): + def set_locktime_relative_time(self, seconds, input_index_n=0, locktime=0): """ Set nSequence relative locktime for this transaction. The transaction will only be valid if the specified amount of seconds have been passed since the previous UTXO is confirmed. @@ -1381,6 +1386,8 @@ def set_locktime_relative_time(self, seconds, input_index_n=0): :type seconds: int :param input_index_n: Index number of input for nSequence locktime :type input_index_n: int + :param locktime: Overwrite default locktime, must be lower than current network blockcount. If anti-fee-sniping is used in a Wallet this value is already filled in. + :type locktime: int :return: """ @@ -1393,7 +1400,8 @@ def set_locktime_relative_time(self, seconds, input_index_n=0): elif (seconds // 512) > SEQUENCE_LOCKTIME_MASK: raise TransactionError("Number of relative nSeqence timelock seconds exceeds %d" % SEQUENCE_LOCKTIME_MASK) self.inputs[input_index_n].sequence = seconds // 512 + SEQUENCE_LOCKTIME_TYPE_FLAG - self.version_int = 2 + self.version_int = 2 if self.version_int < 2 else self.version_int + self.locktime = locktime if locktime else self.locktime self.sign_and_update(index_n=input_index_n) def set_locktime_blocks(self, blocks): @@ -1402,6 +1410,8 @@ def set_locktime_blocks(self, blocks): So for example if you set this value to 600000 the transaction will only be valid after block 600000. + You can also pass the locktime value directly to a Transaction object, or when sending from a wallet. + :param blocks: Transaction is valid after supplied block number. Value must be between 0 and 500000000. Zero means no locktime. :type blocks: int @@ -1425,6 +1435,8 @@ def set_locktime_time(self, timestamp): """ Set nLocktime, a transaction level absolute lock time in timestamp using the transaction sequence field. + You can also pass the locktime value directly to a Transaction object, or when sending from a wallet. + :param timestamp: Transaction is valid after the given timestamp. Value must be between 500000000 and 0xfffffffe :return: """ @@ -1770,7 +1782,7 @@ def sign_and_update(self, index_n=None): def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', unlocking_script_unsigned=None, script_type=None, address='', sequence=0xffffffff, compressed=True, sigs_required=None, sort=False, index_n=None, - value=None, double_spend=False, locktime_cltv=None, locktime_csv=None, + value=None, double_spend=False,locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True): """ Add input to this transaction @@ -1809,8 +1821,6 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash :type value: int :param double_spend: True if double spend is detected, depends on which service provider is selected :type double_spend: bool - :param locktime_cltv: Check Lock Time Verify value. Script level absolute time lock for this input - :type locktime_cltv: int :param locktime_csv: Check Sequency Verify value. :type locktime_csv: int :param key_path: Key path of input key as BIP32 string or list diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index f7a8ed5a..9f361d9e 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3585,7 +3585,7 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non else: utxo_query = utxo_query.filter(DbKey.id.in_(input_key_id)) if skip_dust_amounts: - utxo_query = utxo_query.filter(DbTransactionOutput.value > dust_amount) + utxo_query = utxo_query.filter(DbTransactionOutput.value >= dust_amount) utxos = utxo_query.order_by(DbTransaction.confirmations.desc()).all() if not utxos: raise WalletError("Create transaction: No unspent transaction outputs found or no key available for UTXO's") @@ -3717,9 +3717,10 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) if not locktime and self.anti_fee_sniping: + srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() if blockcount: - transaction.locktime = blockcount + 1 + transaction.locktime = blockcount transaction.fee_per_kb = None if isinstance(fee, int): diff --git a/tests/test_script.py b/tests/test_script.py index 326c58fd..670972e9 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -724,7 +724,7 @@ def test_script_verify_transaction_input_p2wsh(self): self.assertEqual(str(s), "signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_SHA256 data-32 OP_EQUAL") transaction_hash = bytes.fromhex('43f0f6dfb58acc8ed05f5afc224c2f6c50523230bfcba5e5fd91d345e8a159ab') data = {'redeemscript': redeemscript} - self.assertTrue(s.evaluate(message=transaction_hash, tx_data=data)) + self.assertTrue(s.evaluate(message=transaction_hash, data=data)) def test_script_verify_transaction_input_p2pk(self): pass @@ -783,7 +783,7 @@ def test_script_serialize(self): Script([op.op_sha256, script_hash, op.op_equal]) self.assertEqual(str(script), 'OP_0 signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_SHA256 ' 'data-32 OP_EQUAL') - self.assertTrue(script.evaluate(message=transaction_hash, tx_data={'redeemscript': redeemscript.serialize()})) + self.assertTrue(script.evaluate(message=transaction_hash, env_data={'redeemscript': redeemscript.serialize()})) self.assertEqual(script.stack, []) def test_script_deserialize_sig_pk2(self): diff --git a/tests/test_transactions.py b/tests/test_transactions.py index a724b018..50a52b30 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -2,7 +2,7 @@ # # BitcoinLib - Python Cryptocurrency Library # Unit Tests for Transaction Class -# © 2017 - 2022 November - 1200 Web Development +# © 2017 - 2024 March - 1200 Web Development # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -45,7 +45,7 @@ def test_transaction_input_add_scriptsig(self): b"\x15,\x03\x02 \x16\x170?c\x8e\x08\x94\x7f\x18i~\xdc\xb3\xa7\xa5:\xe6m\xf9O&)\xdb\x98\xdc\x0c\xc5\x07k4" \ b"\xb7\x01!\x020\x9a\x19i\x19\xcf\xf1\xd1\x87T'\x1b\xe7\xeeT\xd1\xb3\x7fAL\xbb)+U\xd7\xed\x1f\r\xc8 \x9d" \ b"\x13" - ti = Input(prev_txid, output_index, unlocking_script=unlock_scr) + ti = Input(prev_txid, output_index, unlocking_script=unlock_scr, witness_type='legacy') expected_dict = { 'output_n': 0, 'script': '47304402206ca28f7bafdd65bdfc0fbd88f5a5b003699127caf0fff6e65535d7f131152c0302201617' @@ -73,7 +73,8 @@ def test_transaction_input_add_public_key(self): def test_transaction_input_locking_script(self): ph = "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48" - ti = Input(ph, 0, unlocking_script_unsigned='76a91423e102597c4a99516f851406f935a6e634dbccec88ac') + ti = Input(ph, 0, unlocking_script_unsigned='76a91423e102597c4a99516f851406f935a6e634dbccec88ac', + witness_type='legacy') self.assertEqual(ti.address, '14GiCdJHj3bznWpcocjcu9ByCmDPEhEoP8') def test_transaction_compressed_mixup_error(self): @@ -101,7 +102,7 @@ def test_transaction_input_with_pkh(self): output_n = 0 ki_public_hash = ki.hash160 ti = Input(prev_txid=prev_tx, output_n=output_n, public_hash=ki_public_hash, network='bitcoin', - compressed = False) + compressed = False, witness_type='legacy') self.assertEqual(ti.address, '1BbSBYZChXewL1KTTcZksPmpgvDZH93wtt') # TODO: Move and rewrite @@ -257,7 +258,7 @@ def test_transactions_serialize_raw(self): def test_transactions_sign_1(self): pk = Key('cR6pgV8bCweLX1JVN3Q1iqxXvaw4ow9rrp8RenvJcckCMEbZKNtz', network='testnet') # Private key for import inp = Input(prev_txid='d3c7fbd3a4ca1cca789560348a86facb3bb21dcd75ed38e85235fb6a32802955', output_n=1, - keys=pk.public(), network='testnet') + keys=pk.public(), network='testnet', witness_type='legacy') # key for address mkzpsGwaUU7rYzrDZZVXFne7dXEeo6Zpw2 pubkey = Key('0391634874ffca219ff5633f814f7f013f7385c66c65c8c7d81e7076a5926f1a75', network='testnet') out = Output(880000, public_hash=pubkey.hash160, network='testnet') @@ -1115,7 +1116,8 @@ def test_transaction_sign_p2pk(self): output_n = 0 value = 9000 - inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') + inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature', + witness_type='legacy') outp = Output(7000, k, network='testnet', script_type='p2pk') t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) @@ -1132,7 +1134,8 @@ def test_transaction_sign_p2pk_value(self): value = Value.from_satoshi(9000, network='testnet') fee = 0.00002000 - inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature') + inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature', + witness_type='legacy') outp = Output(value - fee, k, network='testnet', script_type='p2pk') t = Transaction([inp], [outp], network='testnet', witness_type='legacy') t.sign(k.private_byte) From 62200d6d14970cc141cf3a3da5fcf854ce1a4017 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 18 Mar 2024 14:09:25 +0100 Subject: [PATCH 150/207] Fix incorrect data attribute --- tests/test_script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_script.py b/tests/test_script.py index 670972e9..cf5e37b6 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -724,7 +724,7 @@ def test_script_verify_transaction_input_p2wsh(self): self.assertEqual(str(s), "signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_SHA256 data-32 OP_EQUAL") transaction_hash = bytes.fromhex('43f0f6dfb58acc8ed05f5afc224c2f6c50523230bfcba5e5fd91d345e8a159ab') data = {'redeemscript': redeemscript} - self.assertTrue(s.evaluate(message=transaction_hash, data=data)) + self.assertTrue(s.evaluate(message=transaction_hash, env_data=data)) def test_script_verify_transaction_input_p2pk(self): pass From f9767f0df316755459644c34b7b3a4fe63f10d59 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 18 Mar 2024 17:17:46 +0100 Subject: [PATCH 151/207] Enable timelock unittests --- tests/test_script.py | 33 +++++++++---------- tests/test_transactions.py | 67 +++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/tests/test_script.py b/tests/test_script.py index cf5e37b6..9dd9c9f7 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -464,23 +464,22 @@ def test_op_nops(self): for n in [None, 1] + list(range(4, 11)): self.assertTrue(getattr(Stack(), 'op_nop%s' % (str(n) if n else ''))()) - # TODO: Add - # def test_op_checklocktimeverify(self): - # cur_timestamp = int(datetime.now().timestamp()) - # st = Stack([encode_num(500)]) - # self.assertTrue(st.op_checklocktimeverify(tx_locktime=1000, sequence=1)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=1000, sequence=0xffffffff)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=499, sequence=1)) - # self.assertTrue(st.op_checklocktimeverify(tx_locktime=500, sequence=1)) - # self.assertFalse(st.op_checklocktimeverify(tx_locktime=cur_timestamp, sequence=1)) - # - # st = Stack([encode_num(cur_timestamp-100)]) - # self.assertTrue(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) - # self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=660600)) - # - # cur_timestamp = int(datetime.now().timestamp()) - # st = Stack([encode_num(cur_timestamp+100)]) - # self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) + def test_op_checklocktimeverify(self): + cur_timestamp = int(datetime.now().timestamp()) + st = Stack([encode_num(500)]) + self.assertTrue(st.op_checklocktimeverify(tx_locktime=1000, sequence=1)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=1000, sequence=0xffffffff)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=499, sequence=1)) + self.assertTrue(st.op_checklocktimeverify(tx_locktime=500, sequence=1)) + self.assertFalse(st.op_checklocktimeverify(tx_locktime=cur_timestamp, sequence=1)) + + st = Stack([encode_num(cur_timestamp-100)]) + self.assertTrue(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) + self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=660600)) + + cur_timestamp = int(datetime.now().timestamp()) + st = Stack([encode_num(cur_timestamp+100)]) + self.assertFalse(st.op_checklocktimeverify(sequence=0xfffffffe, tx_locktime=cur_timestamp)) # TODO: Add # def test_op_checksequenceverify(self): diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 50a52b30..fcec95c7 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -105,29 +105,6 @@ def test_transaction_input_with_pkh(self): compressed = False, witness_type='legacy') self.assertEqual(ti.address, '1BbSBYZChXewL1KTTcZksPmpgvDZH93wtt') - # TODO: Move and rewrite - # def test_transaction_input_locktime(self): - # rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044' \ - # '022003ea734e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b' \ - # '161ffb654be7e89c84de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d097733' \ - # '7376868b033029ba49cc64765dfdffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a32' \ - # '7696a70a000000006a47304402207758c05e849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c' \ - # '9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef98a21a3c567b4c6defe8ffca06012103ab51db28d30d3a' \ - # 'c99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdffffff029b040000000000001976a91406d66a' \ - # 'dea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a9140614a615ee10d84a1e6d85ec1ff7ff' \ - # 'f527757d5987ffffffff' - # t = Transaction.parse_hex(rawtx) - # t.inputs[0].set_locktime_relative_time(1000) - # self.assertEqual(t.inputs[0].sequence, 4194305) - # t.inputs[0].set_locktime_relative_time(0) - # self.assertEqual(t.inputs[0].sequence, 0xffffffff) - # t.inputs[0].set_locktime_relative_time(100) - # self.assertEqual(t.inputs[0].sequence, 4194305) - # t.inputs[0].set_locktime_relative_blocks(120) - # self.assertEqual(t.inputs[0].sequence, 120) - # t.inputs[0].set_locktime_relative_blocks(0) - # self.assertEqual(t.inputs[0].sequence, 0xffffffff) - class TestTransactionOutputs(unittest.TestCase): @@ -219,17 +196,17 @@ def test_transactions_deserialize_errors(self): 'Input transaction hash not found. Probably malformed raw transaction', Transaction.parse_hex, rawtx) # FIXME: tx.parse_hex() should check remaining size - # rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ - # 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ - # 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ - # 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ - # '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ - # '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ - # '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5' \ - # 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae000000' - # self.assertRaisesRegex(TransactionError, - # 'Error when deserializing raw transaction, bytes left for locktime must be 4 not 3', - # Transaction.parse, rawtx) + rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ + 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ + 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ + 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ + '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ + '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ + '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5' \ + 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae000000' + self.assertRaisesRegex(TransactionError, + 'Error when deserializing raw transaction, bytes left for locktime must be 4 not 3', + Transaction.parse, rawtx) # rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ # 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ # 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ @@ -1624,6 +1601,28 @@ def test_transaction_save_load_sign(self): t2.sign(pk2) self.assertTrue(t2.verify()) + def test_transaction_set_locktimes(self): + rawtx = '0200000002f42e4ee59d33dffc39978bd6f7a1fdef42214b7de7d6d2716b2a5ae0a92fbb09000000006a473044' \ + '022003ea734e54ddc00d4d681e2cac9ecbedb45d24af307aefbc55ecb005c5d2dc13022054d5a0fdb7a0c3ae7b' \ + '161ffb654be7e89c84de06013d416f708f85afe11845a601210213692eb7eb74a0f86284890885629f2d097733' \ + '7376868b033029ba49cc64765dfdffffff27a321a0e098276e3dce7aedf33a633db31bf34262bde3fe30106a32' \ + '7696a70a000000006a47304402207758c05e849310af174ad4d484cdd551d66244d4cf0b5bba84e94d59eb8d3c' \ + '9b02203e005ef10ede62db1900ed0bc2c72c7edd83ef98a21a3c567b4c6defe8ffca06012103ab51db28d30d3a' \ + 'c99965a5405c3d473e25dff6447db1368e9191229d6ec0b635fdffffff029b040000000000001976a91406d66a' \ + 'dea8ca6fcbb4a7a5f18458195c869f4b5488ac307500000000000017a9140614a615ee10d84a1e6d85ec1ff7ff' \ + 'f527757d5987ffffffff' + t = Transaction.parse_hex(rawtx) + t.set_locktime_relative_time(1000) + self.assertEqual(t.inputs[0].sequence, 4194305) + t.set_locktime_relative_time(0) + self.assertEqual(t.inputs[0].sequence, 0xffffffff) + t.set_locktime_relative_time(100) + self.assertEqual(t.inputs[0].sequence, 4194305) + t.set_locktime_relative_blocks(120) + self.assertEqual(t.inputs[0].sequence, 120) + t.set_locktime_relative_blocks(0) + self.assertEqual(t.inputs[0].sequence, 0xffffffff) + def test_transaction_locktime_cltv(self): # timelock = 533600 # inputs = [ From 6c75717bf1bebc32a946daae428d5707db71299d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 18 Mar 2024 17:33:36 +0100 Subject: [PATCH 152/207] Check for invalid transaction locktime --- bitcoinlib/transactions.py | 6 +++++- tests/test_transactions.py | 14 +------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 55f204aa..251922f4 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -994,7 +994,11 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None): inputs[n].update_scripts() - locktime = int.from_bytes(rawtx.read(4)[::-1], 'big') + locktime_bytes = rawtx.read(4)[::-1] + if len(locktime_bytes) != 4: + raise TransactionError("Invalid transaction size, locktime bytes incomplete") + + locktime = int.from_bytes(locktime_bytes, 'big') raw_len = len(raw_bytes) if not raw_bytes: pos_end = rawtx.tell() diff --git a/tests/test_transactions.py b/tests/test_transactions.py index fcec95c7..37fb1ed4 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -195,7 +195,6 @@ def test_transactions_deserialize_errors(self): self.assertRaisesRegex(TransactionError, 'Input transaction hash not found. Probably malformed raw transaction', Transaction.parse_hex, rawtx) - # FIXME: tx.parse_hex() should check remaining size rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ @@ -205,19 +204,8 @@ def test_transactions_deserialize_errors(self): '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5' \ 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae000000' self.assertRaisesRegex(TransactionError, - 'Error when deserializing raw transaction, bytes left for locktime must be 4 not 3', + 'Invalid transaction size, locktime bytes incomplete', Transaction.parse, rawtx) - # rawtx = '01000000000101c114c54564ea09b33c73bfd0237a4d283fe9e73285ad6d34fd3fa42c99f194640300000000ffffffff0200' \ - # 'e1f5050000000017a914e10a445f3084bd131394c66bf0023653dcc247ab877cdb3b0300000000220020701a8d401c84fb13' \ - # 'e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004830450221009c5bd2fa1acb5884fca1612217bd65992c96' \ - # 'c839accea226a3c59d7cc28779c502202cff98a71d195ab61c08fc126577466bb05ae0bfce5554b59455bd758309d4950148' \ - # '3045022100f81ce75339657d31698793e78f475c04fe56bafdb3cfc6e1035846aeeeb98f7902203ad5b1bcb96494457197cb' \ - # '3c12b67ddd3cf8127fe054dec971c858252c004bf8016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea36' \ - # '8e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70f01874496feff2103c96d495bfdd5' \ - # 'ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000' - # self.assertRaisesRegex(TransactionError, - # "Error when deserializing raw transaction, bytes left for locktime must be 4 not 3", - # Transaction.parse, rawtx) def test_transactions_verify_signature(self): for r in self.rawtxs: From 85e316e7a742234c31df8af63ac3deafaf3951d7 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 18 Mar 2024 20:34:26 +0100 Subject: [PATCH 153/207] Avoid type conversions when parsing transactions --- bitcoinlib/transactions.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 251922f4..97a4c01f 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -888,24 +888,31 @@ def parse(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ + raw_bytes = b'' if isinstance(rawtx, bytes): + raw_bytes = rawtx rawtx = BytesIO(rawtx) elif isinstance(rawtx, str): + raw_bytes = bytes.fromhex(rawtx) rawtx = BytesIO(bytes.fromhex(rawtx)) - return cls.parse_bytesio(rawtx, strict, network) + return cls.parse_bytesio(rawtx, strict, network, raw_bytes=raw_bytes) @classmethod - def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None): + def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None, raw_bytes=b''): """ Parse a raw transaction and create a Transaction object - :param rawtx: Raw transaction string + :param rawtx: Raw transaction bytes stream :type rawtx: BytesIO :param strict: Raise exception when transaction is malformed, incomplete or not understood :type strict: bool :param network: Network, leave empty for default network :type network: str, Network + :param index: index position in block + :type index: int + :param raw_bytes: Raw transaction as bytes if available + :param raw_bytes: bytes :return Transaction: """ @@ -915,7 +922,6 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None): network = network if not isinstance(network, Network): cls.network = Network(network) - raw_bytes = b'' try: pos_start = rawtx.tell() @@ -995,7 +1001,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None): inputs[n].update_scripts() locktime_bytes = rawtx.read(4)[::-1] - if len(locktime_bytes) != 4: + if len(locktime_bytes) != 4 and strict: raise TransactionError("Invalid transaction size, locktime bytes incomplete") locktime = int.from_bytes(locktime_bytes, 'big') @@ -1025,7 +1031,8 @@ def parse_hex(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ - return cls.parse_bytesio(BytesIO(bytes.fromhex(rawtx)), strict, network) + raw_bytes = bytes.fromhex(rawtx) + return cls.parse_bytesio(BytesIO(raw_bytes), strict, network, raw_bytes=raw_bytes) @classmethod def parse_bytes(cls, rawtx, strict=True, network=DEFAULT_NETWORK): @@ -1043,7 +1050,7 @@ def parse_bytes(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ - return cls.parse(BytesIO(rawtx), strict, network) + return cls.parse(BytesIO(rawtx), strict, network, raw_bytes=raw_bytes) @staticmethod def load(txid=None, filename=None): From d9b6c02dae42d53b37855d560670d61eb153165c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 5 Apr 2024 22:37:05 +0200 Subject: [PATCH 154/207] Fix calling wrong method when parsing transaction --- bitcoinlib/transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 97a4c01f..f2d1e5b2 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1050,7 +1050,7 @@ def parse_bytes(cls, rawtx, strict=True, network=DEFAULT_NETWORK): :return Transaction: """ - return cls.parse(BytesIO(rawtx), strict, network, raw_bytes=raw_bytes) + return cls.parse_bytesio(BytesIO(rawtx), strict, network, raw_bytes=rawtx) @staticmethod def load(txid=None, filename=None): From 70d35dbb75b08fd5ad8c0978adb7a9df358d1d36 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 6 Apr 2024 20:33:09 +0200 Subject: [PATCH 155/207] Allow to derive hardened change and address_index keys, ie used in bitcoin core --- bitcoinlib/wallets.py | 51 ++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 9f361d9e..8ad780d3 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -412,8 +412,9 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 session.commit() return WalletKey(wk.id, session, k) + address_index = k.child_index % 0x80000000 nk = DbKey(id=new_key_id, name=name[:80], wallet_id=wallet_id, public=k.public_byte, private=k.private_byte, purpose=purpose, - account_id=account_id, depth=k.depth, change=change, address_index=k.child_index, + account_id=account_id, depth=k.depth, change=change, address_index=address_index, wif=k.wif(witness_type=witness_type, multisig=multisig, is_private=True), address=address, parent_id=parent_id, compressed=k.compressed, is_private=k.is_private, path=path, key_type=key_type, network_name=network, encoding=encoding, cosigner_id=cosigner_id, @@ -1100,7 +1101,7 @@ def _create(cls, name, key, owner, network, account_id, purpose, scheme, parent_ session.commit() w = cls(new_wallet_id, db_uri=db_uri, db_cache_uri=db_cache_uri, main_key_object=mk.key()) - w.key_for_path([0, 0], account_id=account_id, cosigner_id=cosigner_id) + w.key_for_path([], account_id=account_id, cosigner_id=cosigner_id, change=0, address_index=0) else: # scheme == 'single': if not key: key = HDKey(network=network, depth=key_depth) @@ -1791,7 +1792,6 @@ def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness if self.scheme == 'single': return [self.main_key] - network, account_id, _ = self._get_account_defaults(network, account_id) if network != self.network.name and "coin_type'" not in self.key_path: raise WalletError("Multiple networks not supported by wallet key structure") @@ -1808,21 +1808,19 @@ def new_keys(self, name='', account_id=None, change=0, cosigner_id=None, witness _, purpose, encoding = get_key_structure_data(witness_type, self.multisig) address_index = 0 - if (self.multisig and cosigner_id is not None and + if not((self.multisig and cosigner_id is not None and (len(self.cosigner) > cosigner_id and self.cosigner[cosigner_id].key_path == 'm' or - self.cosigner[cosigner_id].key_path == ['m'])): - req_path = [] - else: + self.cosigner[cosigner_id].key_path == ['m']))): prevkey = self.session.query(DbKey).\ filter_by(wallet_id=self.wallet_id, purpose=purpose, network_name=network, account_id=account_id, witness_type=witness_type, change=change, cosigner_id=cosigner_id, depth=self.key_depth).\ order_by(DbKey.address_index.desc()).first() if prevkey: address_index = prevkey.address_index + 1 - req_path = [change, address_index] - return self.keys_for_path(req_path, name=name, account_id=account_id, witness_type=witness_type, network=network, - cosigner_id=cosigner_id, address_index=address_index, number_of_keys=number_of_keys) + return self.keys_for_path([], name=name, account_id=account_id, witness_type=witness_type, network=network, + cosigner_id=cosigner_id, address_index=address_index, number_of_keys=number_of_keys, + change=change) def new_key_change(self, name='', account_id=None, witness_type=None, network=None): """ @@ -2126,8 +2124,10 @@ def new_account(self, name='', account_id=None, witness_type=None, network=None) acckey = self.key_for_path([], level_offset=self.depth_public_master-self.key_depth, account_id=account_id, name=name, witness_type=witness_type, network=network) - self.key_for_path([0, 0], witness_type=witness_type, network=network, account_id=account_id) - self.key_for_path([1, 0], witness_type=witness_type, network=network, account_id=account_id) + self.key_for_path([], witness_type=witness_type, network=network, account_id=account_id, change=0, + address_index=0) + self.key_for_path([], witness_type=witness_type, network=network, account_id=account_id, change=1, + address_index=0) return acckey def path_expand(self, path, level_offset=None, account_id=None, cosigner_id=0, address_index=None, change=0, @@ -2276,7 +2276,8 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos else: wk = wlt.keys_for_path(path, level_offset=level_offset, account_id=account_id, name=name, cosigner_id=cosigner_id, network=network, recreate=recreate, - witness_type=witness_type, number_of_keys=number_of_keys) + witness_type=witness_type, number_of_keys=number_of_keys, change=change, + address_index=address_index) public_keys.append(wk) keys_to_add = [public_keys] if type(public_keys[0]) is list: @@ -2331,8 +2332,14 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos account_id = 0 if ("account'" not in self.key_path or self.key_path.index("account'") >= len(fullpath)) \ else int(fullpath[self.key_path.index("account'")][:-1]) - change = None if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ - else int(fullpath[self.key_path.index("change")]) + change_pos = [self.key_path.index(chg) for chg in ["change", "change'"] if chg in self.key_path] + change = None if not change_pos or change_pos[0] >= len(fullpath) else ( + int(fullpath[change_pos[0]].strip("'"))) + # if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ + # else int(fullpath[self.key_path.index("change")]) + # if change is None: + # change = None if "change'" not in self.key_path or self.key_path.index("change'") >= len(fullpath) \ + # else int(fullpath[self.key_path.index("change'")].strip("'")) if name and len(fullpath) == len(newpath.split('/')): key_name = name else: @@ -2347,16 +2354,24 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos if nkey: new_keys.append(nkey) if len(new_keys) < number_of_keys: + parent_id = new_keys[0].parent_id + if parent_id not in self._key_objects: + self.key(parent_id) topkey = self._key_objects[new_keys[0].parent_id] parent_key = topkey.key() new_key_id = self.session.query(DbKey.id).order_by(DbKey.id.desc()).first()[0] + 1 - keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1]) + len(new_keys), int(fullpath[-1]) + - number_of_keys)] + hardened_child = False + if fullpath[-1].endswith("'"): + hardened_child = True + keys_to_add = [str(k_id) for k_id in range(int(fullpath[-1].strip("'")) + len(new_keys), + int(fullpath[-1].strip("'")) + number_of_keys)] for key_idx in keys_to_add: new_key_id += 1 + if hardened_child: + key_idx = "%s'" % key_idx ck = parent_key.subkey_for_path(key_idx, network=network) - key_name = 'address index %s' % key_idx + key_name = 'address index %s' % key_idx.strip("'") newpath = '/'.join(newpath.split('/')[:-1] + [key_idx]) new_keys.append(WalletKey.from_key( key=ck, name=key_name, wallet_id=self.wallet_id, account_id=account_id, From 8595801e0a92f26930bbed4ccb08b329c9cc8b2a Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 7 Apr 2024 11:11:43 +0200 Subject: [PATCH 156/207] Define standard key paths in configuration --- bitcoinlib/config/config.py | 17 +++++++++++------ bitcoinlib/wallets.py | 5 ----- examples/wallet_bitcoind.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 examples/wallet_bitcoind.py diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index ab4ba75e..7a637c8e 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -157,6 +157,11 @@ ENCODING_BECH32_PREFIXES = ['bc', 'tb', 'ltc', 'tltc', 'blt'] DEFAULT_WITNESS_TYPE = 'segwit' BECH32M_CONST = 0x2bc830a3 +KEY_PATH_LEGACY = ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] +KEY_PATH_P2SH = ["m", "purpose'", "cosigner_index", "change", "address_index"] +KEY_PATH_P2WSH = ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] +KEY_PATH_P2WPKH = ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] +KEY_PATH_BITCOINCORE = ['m', "account'", "change'", "address_index'"] # Wallets WALLET_KEY_STRUCTURES = [ @@ -176,7 +181,7 @@ 'multisig': False, 'encoding': 'base58', 'description': 'Legacy wallet using pay-to-public-key-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_LEGACY }, { 'purpose': 45, @@ -185,7 +190,7 @@ 'multisig': True, 'encoding': 'base58', 'description': 'Legacy multisig wallet using pay-to-script-hash scripts', - 'key_path': ["m", "purpose'", "cosigner_index", "change", "address_index"] + 'key_path': KEY_PATH_P2SH }, { 'purpose': 48, @@ -194,7 +199,7 @@ 'multisig': True, 'encoding': 'base58', 'description': 'Segwit multisig wallet using pay-to-wallet-script-hash scripts nested in p2sh scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] + 'key_path': KEY_PATH_P2WSH }, { 'purpose': 48, @@ -203,7 +208,7 @@ 'multisig': True, 'encoding': 'bech32', 'description': 'Segwit multisig wallet using native segwit pay-to-wallet-script-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "script_type'", "change", "address_index"] + 'key_path': KEY_PATH_P2WSH }, { 'purpose': 49, @@ -212,7 +217,7 @@ 'multisig': False, 'encoding': 'base58', 'description': 'Segwit wallet using pay-to-wallet-public-key-hash scripts nested in p2sh scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_P2WPKH }, { 'purpose': 84, @@ -221,7 +226,7 @@ 'multisig': False, 'encoding': 'bech32', 'description': 'Segwit multisig wallet using native segwit pay-to-wallet-public-key-hash scripts', - 'key_path': ["m", "purpose'", "coin_type'", "account'", "change", "address_index"] + 'key_path': KEY_PATH_P2WPKH }, # { # 'purpose': 86, diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 8ad780d3..b9943639 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2335,11 +2335,6 @@ def keys_for_path(self, path, level_offset=None, name=None, account_id=None, cos change_pos = [self.key_path.index(chg) for chg in ["change", "change'"] if chg in self.key_path] change = None if not change_pos or change_pos[0] >= len(fullpath) else ( int(fullpath[change_pos[0]].strip("'"))) - # if "change" not in self.key_path or self.key_path.index("change") >= len(fullpath) \ - # else int(fullpath[self.key_path.index("change")]) - # if change is None: - # change = None if "change'" not in self.key_path or self.key_path.index("change'") >= len(fullpath) \ - # else int(fullpath[self.key_path.index("change'")].strip("'")) if name and len(fullpath) == len(newpath.split('/')): key_name = name else: diff --git a/examples/wallet_bitcoind.py b/examples/wallet_bitcoind.py new file mode 100644 index 00000000..9003aaf3 --- /dev/null +++ b/examples/wallet_bitcoind.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib +# +# © 2024 April - 1200 Web Development +# + +import os +from bitcoinlib.wallets import wallet_create_or_open +from bitcoinlib.services.bitcoind import BitcoindClient + +# +# Settings and Initialization +# + +# Generate your own private key with: HDKey(network='testnet').wif_private() +pkwif = 'vprv9DMUxX4ShgxMKxHYfZ7Z35RxRLC9Av59MyJaMmCFRqfvUdUZdBB1awTDvTfDzZbtsPzZVCcpCunaELcuPsnLeLMg634hsxSSvwpTdfgCYMX' +# Put connection string with format http://bitcoinlib:password@localhost:18332) +# to Bitcoin Core node in the following file: +bitcoind_url = open(os.path.join(os.path.expanduser('~'), ".bitcoinlib/.bitcoind_connection_string")).read() +bcc = BitcoindClient(base_url=bitcoind_url) +lastblock = bcc.proxy.getblockcount() +print("Connected to bitcoind, last block: " + str(lastblock)) + +# +# Create Wallets +# + + + + +# +# Using wallets +# + From 482e9bcd2557c91c5ab117822477d40ceaf9c084 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 7 Apr 2024 20:39:31 +0200 Subject: [PATCH 157/207] Add example connect to bitcoin core node --- bitcoinlib/services/baseclient.py | 1 + bitcoinlib/services/bitcoind.py | 2 +- examples/wallet_bitcoind.py | 37 ----------- examples/wallet_bitcoind_connected_wallets.py | 66 +++++++++++++++++++ .../wallet_bitcoind_connected_wallets2.py | 27 ++++++++ 5 files changed, 95 insertions(+), 38 deletions(-) delete mode 100644 examples/wallet_bitcoind.py create mode 100644 examples/wallet_bitcoind_connected_wallets.py create mode 100644 examples/wallet_bitcoind_connected_wallets2.py diff --git a/bitcoinlib/services/baseclient.py b/bitcoinlib/services/baseclient.py index 79c5908d..42ef78cf 100644 --- a/bitcoinlib/services/baseclient.py +++ b/bitcoinlib/services/baseclient.py @@ -50,6 +50,7 @@ def __init__(self, network, provider, base_url, denominator, api_key='', provide if not isinstance(network, Network): self.network = Network(network) self.provider = provider + self.base_url = base_url self.resp = None self.units = denominator diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index 6bdb1878..f8196e88 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -223,7 +223,7 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): if len(txs_list) >= MAX_WALLET_TRANSACTIONS: raise ClientError("Bitcoind wallet contains too many transactions %d, use other service provider for this " "wallet" % MAX_WALLET_TRANSACTIONS) - txids = list(set([(tx['txid'], tx['blockheight']) for tx in txs_list if tx['address'] == address])) + txids = list(set([(tx['txid'], tx.get('blockheight')) for tx in txs_list if tx['address'] == address])) for (txid, blockheight) in txids: tx_raw = self.proxy.getrawtransaction(txid, 1) t = self._parse_transaction(tx_raw, blockheight) diff --git a/examples/wallet_bitcoind.py b/examples/wallet_bitcoind.py deleted file mode 100644 index 9003aaf3..00000000 --- a/examples/wallet_bitcoind.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# -# BitcoinLib - Python Cryptocurrency Library -# -# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib -# -# © 2024 April - 1200 Web Development -# - -import os -from bitcoinlib.wallets import wallet_create_or_open -from bitcoinlib.services.bitcoind import BitcoindClient - -# -# Settings and Initialization -# - -# Generate your own private key with: HDKey(network='testnet').wif_private() -pkwif = 'vprv9DMUxX4ShgxMKxHYfZ7Z35RxRLC9Av59MyJaMmCFRqfvUdUZdBB1awTDvTfDzZbtsPzZVCcpCunaELcuPsnLeLMg634hsxSSvwpTdfgCYMX' -# Put connection string with format http://bitcoinlib:password@localhost:18332) -# to Bitcoin Core node in the following file: -bitcoind_url = open(os.path.join(os.path.expanduser('~'), ".bitcoinlib/.bitcoind_connection_string")).read() -bcc = BitcoindClient(base_url=bitcoind_url) -lastblock = bcc.proxy.getblockcount() -print("Connected to bitcoind, last block: " + str(lastblock)) - -# -# Create Wallets -# - - - - -# -# Using wallets -# - diff --git a/examples/wallet_bitcoind_connected_wallets.py b/examples/wallet_bitcoind_connected_wallets.py new file mode 100644 index 00000000..3bcb931d --- /dev/null +++ b/examples/wallet_bitcoind_connected_wallets.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib +# +# Method 1 - Create wallet in Bitcoin Core and use the same wallet in Bitcoinlib using the bitcoin node to +# receive and send bitcoin transactions. +# +# © 2024 April - 1200 Web Development +# + +from bitcoinlib.wallets import * +from bitcoinlib.services.bitcoind import BitcoindClient + +# +# Settings and Initialization +# + +# Call bitcoin-cli dumpwallet and look for extended private masterkey at top off the export. Then copy the +# bitcoin core node masterseed here: +pkwif = 'tprv8ZgxMBicQKsPe2iVrERVdAgjcqHhxvZcWeS2Va6nvgddpDH1r33A4aTtdYYkoFDY6CCf5fogwLYmAdQQNxkk7W3ygwFd6hquJVLmmpbJRp2' +enable_verify_wallet = False + +# Put connection string with format http://bitcoinlib:password@localhost:18332) +# to Bitcoin Core node in the following file: +bitcoind_url = open(os.path.join(os.path.expanduser('~'), ".bitcoinlib/.bitcoind_connection_string")).read() +bcc = BitcoindClient(base_url=bitcoind_url) +lastblock = bcc.proxy.getblockcount() +print("Connected to bitcoind, last block: " + str(lastblock)) + +# +# Create a copy of the Bitcoin Core Wallet in Bitcoinlib +# +w = wallet_create_or_open('wallet_bitcoincore', pkwif, network='testnet', witness_type='segwit', + key_path=KEY_PATH_BITCOINCORE) +addr = bcc.proxy.getnewaddress() +addrinfo = bcc.proxy.getaddressinfo(addr) +bcl_addr = w.key_for_path(addrinfo['hdkeypath']).address + +# Verify if we are using the same wallet +if enable_verify_wallet and addr == bcl_addr: + print("Address %s with path %s, is identical in Bitcoin core and Bitcoinlib" % (addr, addrinfo['hdkeypath'])) +elif not addr == bcl_addr: + print ("Address %s with path %s, is NOT identical in Bitcoin core and Bitcoinlib" % (addr, addrinfo['hdkeypath'])) + raise ValueError("Wallets not identical in Bitcoin core and Bitcoinlib") + +# +# Using wallets +# + +# Now pick an address from your wallet and send some testnet coins to it, for example by using another wallet or a +# testnet faucet. +w.providers = ['bitcoind'] +w.scan() +# w.info() + +if not w.balance(): + print("No testnet coins available") +else: + print("Found testnet coins. Wallet balance: %d" % w.balance()) + # Send some coins to our own wallet + t = w.send_to(w.get_key().address, 1000, fee=200, offline=False) + t.info() + +# If you now run bitcoin-cli listunspent 0, you should see the 1 or 2 new utxo's for this transaction. diff --git a/examples/wallet_bitcoind_connected_wallets2.py b/examples/wallet_bitcoind_connected_wallets2.py new file mode 100644 index 00000000..37968d0d --- /dev/null +++ b/examples/wallet_bitcoind_connected_wallets2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# BitcoinLib - Python Cryptocurrency Library +# +# EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib +# +# Method 2 - ... +# +# © 2024 April - 1200 Web Development +# + +from bitcoinlib.wallets import * +from bitcoinlib.services.bitcoind import BitcoindClient + +# +# Settings and Initialization +# + +pkwif = 'cTAyLb37Sr4XQPzWCiwihJxdFpkLKeJBFeSnd5hwNiW8aqrbsZCd' + +w = wallet_create_or_open("wallet_bitcoincore2", keys=pkwif, network='testnet', witness_type='segwit', + key_path=KEY_PATH_BITCOINCORE) +w.providers=['bitcoind'] +w.get_key() +w.scan(scan_gap_limit=1) +w.info() + From 07b84644eea432914b54361b200876db1dd8f8f4 Mon Sep 17 00:00:00 2001 From: nima Date: Mon, 22 Jan 2024 22:22:29 +0100 Subject: [PATCH 158/207] Merge again: Fix multi wallet connection bitcoind client with optional walletname --- bitcoinlib/services/bitcoind.py | 4 ++-- bitcoinlib/services/services.py | 10 ++++++++-- bitcoinlib/wallets.py | 21 +++++++++++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index e6afbf43..464941f4 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -54,7 +54,7 @@ class BitcoindClient(BaseClient): @staticmethod @deprecated - def from_config(configfile=None, network='bitcoin', *args): + def from_config(configfile=None, network='bitcoin', **kwargs): """ Read settings from bitcoind config file @@ -116,7 +116,7 @@ def from_config(configfile=None, network='bitcoin', *args): server = _read_from_config(config, 'rpc', 'externalip', server) url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) - return BitcoindClient(network, url, *args) + return BitcoindClient(network, url, **kwargs) def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args): """ diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 630f298f..f8d6a9d8 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -55,7 +55,7 @@ class Service(object): def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, providers=None, timeout=TIMEOUT_REQUESTS, cache_uri=None, ignore_priority=False, exclude_providers=None, - max_errors=SERVICE_MAX_ERRORS, strict=True): + max_errors=SERVICE_MAX_ERRORS, strict=True, wallet_name=None): """ Create a service object for the specified network. By default, the object connect to 1 service provider, but you can specify a list of providers or a minimum or maximum number of providers. @@ -131,6 +131,7 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr self._blockcount = None self.cache = None self.cache_uri = cache_uri + self.wallet_name = wallet_name try: self.cache = Cache(self.network, db_uri=cache_uri) except Exception as e: @@ -166,8 +167,13 @@ def _provider_execute(self, method, *arguments): continue client = getattr(services, self.providers[sp]['provider']) providerclient = getattr(client, self.providers[sp]['client_class']) + + base_url = self.providers[sp]['url'] + if 'bitcoind' in sp and self.wallet_name is not None: + base_url = f"{base_url}/wallet/{self.wallet_name}" + pc_instance = providerclient( - self.network, self.providers[sp]['url'], self.providers[sp]['denominator'], + self.network, base_url, self.providers[sp]['denominator'], self.providers[sp]['api_key'], self.providers[sp]['provider_coin_id'], self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict) if not hasattr(pc_instance, method): diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index eed70a90..86dc0174 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -835,7 +835,7 @@ def send(self, offline=False): if offline: return None - srv = Service(network=self.network.name, providers=self.hdwallet.providers, + srv = Service(network=self.network.name, wallet_name=self.hdwallet.name, providers=self.hdwallet.providers, cache_uri=self.hdwallet.db_cache_uri) res = srv.sendrawtransaction(self.raw_hex()) if not res: @@ -2801,7 +2801,7 @@ def balance_update_from_serviceprovider(self, account_id=None, network=None): """ network, account_id, acckey = self._get_account_defaults(network, account_id) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) balance = srv.getbalance(self.addresslist(account_id=account_id, network=network)) if srv.results: new_balance = { @@ -3025,7 +3025,7 @@ def utxos_update(self, account_id=None, used=None, networks=None, key_id=None, d addresslist = self.addresslist(account_id=account_id, used=used, network=network, key_id=key_id, change=change, depth=depth) random.shuffle(addresslist) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) utxos = [] for address in addresslist: if rescan_all: @@ -3226,7 +3226,7 @@ def transactions_update_confirmations(self): :return: """ network = self.network.name - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) blockcount = srv.blockcount() self.session.query(DbTransaction).\ filter(DbTransaction.wallet_id == self.wallet_id, @@ -3249,7 +3249,7 @@ def transactions_update_by_txids(self, txids): txids = list(dict.fromkeys(txids)) txs = [] - srv = Service(network=self.network.name, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=self.network.name, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) for txid in txids: tx = srv.gettransaction(to_hexstring(txid)) if tx: @@ -3304,7 +3304,7 @@ def transactions_update(self, account_id=None, used=None, network=None, key_id=N # Update number of confirmations and status for already known transactions self.transactions_update_confirmations() - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) # Get transactions for wallet's addresses txs = [] @@ -3545,6 +3545,11 @@ def transaction_spent(self, txid, output_n): if qr: return qr.transaction.txid.hex() + + def update_transactions_from_block(block, network=None): + pass + + def _objects_by_key_id(self, key_id): key = self.session.query(DbKey).filter_by(id=key_id).scalar() if not key: @@ -3737,7 +3742,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco addr = addr.key() transaction.add_output(value, addr) - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) if not locktime and self.anti_fee_sniping: srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) @@ -4244,7 +4249,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma continue input_arr.append((utxo['txid'], utxo['output_n'], utxo['key_id'], utxo['value'])) total_amount += utxo['value'] - srv = Service(network=network, providers=self.providers, cache_uri=self.db_cache_uri) + srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) if isinstance(fee, str): n_outputs = 1 if not isinstance(to_address, list) else len(to_address) From 41bd1bc3d3640c1cb742cb763c5aadfe0a3ecd66 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 8 Apr 2024 18:28:14 +0200 Subject: [PATCH 159/207] Add unittest for bitcoincore style key derivation --- bitcoinlib/services/bitcoind.py | 4 +- tests/test_wallets.py | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index 464941f4..88c52f50 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -118,7 +118,7 @@ def from_config(configfile=None, network='bitcoin', **kwargs): url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) return BitcoindClient(network, url, **kwargs) - def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args): + def __init__(self, network='bitcoin', base_url='', denominator=100000000, wallet_name='', *args): """ Open connection to bitcoin node @@ -136,6 +136,8 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args) bdc = self.from_config('', network) base_url = bdc.base_url network = bdc.network + if wallet_name: + base_url += "/wallet/%s" % wallet_name _logger.info("Connect to bitcoind") self.proxy = AuthServiceProxy(base_url) super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, *args) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 547960c4..b15aaead 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -684,6 +684,73 @@ def test_wallet_key_public_leaks(self): self.assertFalse(w2.main_key.is_private) self.assertIsNone(w2.main_key.key_private) + def test_wallet_key_derivation_bitcoin_core(self): + pkwif = \ + "tprv8ZgxMBicQKsPdwXQbgq89TZiKB2jiPQqpG86D2TxkikdjuPSFBJX7kiuSqXUR3vjAajk84ekJAgDbNtm72DcNUqgrasvVyL4ugqg9M2jLNk" + wallet_delete_if_exists('wallet_bitcoinnode_keytest', force=True) + w = wallet_create_or_open('wallet_bitcoinnode_keytest', pkwif, network='testnet', witness_type='segwit', + key_path=KEY_PATH_BITCOINCORE) + expected_address = "tb1qet37ftfmj9qlnajf5370zymwdx0dxrg5s08406" + self.assertEqual(w.get_key().address, expected_address) + + bitcoincore_legacy_keys = [ + ("cT6RRQfmAKws5DiXeMqDoKQkdkpDeTxjR6hQSUMbAEYrQLowE2dA", "tb1qqqa20559evephf7k5vdl4kmkkkwzwq4r3fdqjn", + "m/0'/1'/33'"), + ("cRoLN8YaDq3MTGJZh4c2eA5kNvFRaiTy1t6o2tbs2FdSF3aWjYoE", "tb1qqzckpfc338h67c3wvdfxy80xyl3duerr9lzzeh", + "m/0'/0'/945'"), + ("cNRJJBHoVYYgVaZg4cighgh8dTnGN35fFAbSAak9hbsRybi88uUU", "tb1qqrvr94pkh57n3gqxz9wdvcuqqjrf7syr9lcezn", + "m/0'/0'/179'"), + ("cRsyfCoRxb37FR55gsdePjGvc2yP2b9zmK7xAcZnrfBDYi6qLpZH", "tb1qqrnwa6047e9weadsjkw4yvmq2hpcnsez5fscvg", + "m/0'/1'/114'"), + ("cPGrz5DJChNGUhXNp7uLTN1QyL8uSvDCeaQXE8XGtDnGpiFkwP2t", "tb1qqywxch6yqg0n2n43p8mh2jj8xn45zu0jd6w5qr", + "m/0'/0'/661'"), + ("cN3ftZiCws7rpibSN4bgwxNFjrwKFv8vBAhsGfWS181wvrQdvyW4", "tb1qqxxnzft5dcarhdke6jpqjxrh5pnj3jx28rquef", + "m/0'/0'/508'"), + ("cV7oNGCZedJ5AB6C8no5CPAJFFCL8562CzHV2WbTAbic3nFWCKWa", "tb1qqxaf72y6jsdly92d5djp0xgghunrctu69ej4l3", + "m/0'/0'/703'"), + ("cPHpS6LfJBuuKCwx9zBsbiuriGF84Vz7tZVbDeQSJ5eB8nFbmXN7", "tb1qq8l2ue64setgk5jyepk8vycs0qydxy4496tx5m", + "m/0'/0'/296'"), + ("cSTyAscq5QdPKEnBxs5hLHgMkfd2a6xVwU8QTh8DJTKVZK6Q535Q", "tb1qqgplnyw697xsfvpccy4qtzyflzn9ke7s97l8pq", + "m/0'/0'/335'"), + ("cUTMrBYCya9gMsxm5aGquQGNkcyc4eduuuzY3v6NpVbWqG6ixonP", "tb1qqg8x4cdcrv83nl0zv8djzhea4kl2647zsxh36l", + "m/0'/0'/589'"), + ("cVzxpZERuRgwvAcWUr4U7EtMSyzEsBCziwxTofnCTQN2EVBGjM5y", "tb1qqg8fzfddxet3zq5h334jsvl8x92p04hkfj9nf9", + "m/0'/0'/928'"), + ("cVhPAPYdZH6wbegMiuxYcNpGuxpbrTX519n5zKVjv3EGhd9w3xDx", "tb1qq2pe4z4undtc6ry38peefsw98apxaq0w0tdyvg", + "m/0'/0'/998'"), + ("cPyj6ziaPWDMX1rQQ2AYgtGmCWZkqyM9tuTdwiDvuEmQ6ZkzX7NQ", "tb1qq2epkw3vph82pl4ykjpvnnjxnka2grk8uv5d6h", + "m/0'/0'/232'"), + ("cRgxMBzg7vLmHjpzZW7ZsFBMEhFGHZxTU8oLLMUX8EbxXjrkRzxi", "tb1qqtqk529kenvdgymmdkg070kkyfc56lf0x9rqqw", + "m/0'/0'/375'"), + ("cQpyG8EaGfCzq5ficwsSz73krYS1LjELg9pRiydE7K9suMmNZirm", "tb1qqt93cacv5u8e40k995qxdfmzupnjst4h44px48", + "m/0'/0'/711'"), + ("cNyweGkXaFJ24k91tjpSGqbpzVvwu2c63FVKtJjztBGmoKtjhjDP", "tb1qqt4uwz330sfxkh7ug9tgpc4083r7ky3vpg22w6", + "m/0'/0'/320'"), + ("cQzDqSPb8KUeZ48BBaQt8DuLrsP1oQUgJDnWpnBVXnhrJ7ap4vjh", "tb1qqv8qkr0g76s6m4mguxag56hf6pgtyvy2c78c77", + "m/0'/0'/245'"), + ("cSCv6ByFqi6FGSA3yeb2oKfvPzokeKNGWZ1q3tSkovnBkDGvxJBR", "tb1qqdj4ruf23szh4jmkjt4nzx83utp7dlhydn2532", + "m/0'/0'/336'"), + ("cSpXvuvzNxSBp3YwUnXTbj2qa6MBMAdcK2gZXFnG6DoXUUzJj5ga", "tb1qqdax64eywry5yjw7npll7r6jg5afe4kc3a2ysa", + "m/0'/1'/185'"), + ("cUE6aGgcGGCidEqHUEFamsbVitQU3wtFnMU75oz36vQj2WPRAXfc", "tb1qqwgfqwm20kryx8py0n6escew8xgjqp5whpnfx0", + "m/0'/1'/54'"), + ("cVdndV7n3C2ga1uXHEWuehKWnfJxidcGf5tAxEPZgSxkGgQ5qmgr", "tb1qqw3fj4v90yy0at0muk8p9pjf25tjzpgw9wgy0t", + "m/0'/0'/302'"), + ("cVHfUViBhR62SE3R5RBiQeTYHVFVVh4YGy9YtiGg7auMrro646Cq", "tb1qqw5gcq4rk63sxrpy2s2prtylmgkrfa2sezck6d", + "m/0'/0'/716'"), + ("cQGYzBYwfuyuSGeTP3iKvUxnayXuDMcXbtQ1EwjfyLUuCHnv2qR4", "tb1qq0cy7pae98rrf6688xz47ryl90kuxh4jp65ll6", + "m/0'/0'/624'"), + ("cNS1Ket4FxnKBa93cmgTU3psQqwGnEMGjUHeMeNHgbE2KeVDqZQd", "tb1qqs6pr5jyautze32ummxpnpwhsyzf2rvptxxm0n", + "m/0'/1'/150'"), + ("cMrbo3BTBYLGHry8oeXhMaWGU8GWXSLFFAyLoEWMTPeZxwxxLMRN", "tb1qqsuuw0rfrz83svvsuatg8q7j2ea5z3geahr32g", + "m/0'/0'/157'"), + ] + + for bc_key in bitcoincore_legacy_keys: + k = w.key_for_path(bc_key[2]) + self.assertEqual(k.address, bc_key[1]) + self.assertEqual(k.key().wif_key(), bc_key[0]) + @classmethod def tearDownClass(cls): del cls.database_uri From b267e67d1a9b61d9b05dab6128185bbec3160fed Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 8 Apr 2024 20:59:26 +0200 Subject: [PATCH 160/207] Move bitcoind wallet name to providerclient --- bitcoinlib/services/baseclient.py | 3 ++- bitcoinlib/services/bitcoind.py | 7 +++++-- bitcoinlib/services/services.py | 9 +++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/bitcoinlib/services/baseclient.py b/bitcoinlib/services/baseclient.py index 42ef78cf..2e7889c2 100644 --- a/bitcoinlib/services/baseclient.py +++ b/bitcoinlib/services/baseclient.py @@ -44,7 +44,7 @@ def __str__(self): class BaseClient(object): def __init__(self, network, provider, base_url, denominator, api_key='', provider_coin_id='', - network_overrides=None, timeout=TIMEOUT_REQUESTS, latest_block=None, strict=True): + network_overrides=None, timeout=TIMEOUT_REQUESTS, latest_block=None, strict=True, wallet_name=''): try: self.network = network if not isinstance(network, Network): @@ -62,6 +62,7 @@ def __init__(self, network, provider, base_url, denominator, api_key='', provide if network_overrides is not None: self.network_overrides = network_overrides self.strict = strict + self.wallet_name = wallet_name except Exception: raise ClientError("This Network is not supported by %s Client" % provider) diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index 88c52f50..5980ccf8 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -118,7 +118,7 @@ def from_config(configfile=None, network='bitcoin', **kwargs): url = "http://%s:%s@%s:%s" % (config.get('rpc', 'rpcuser'), config.get('rpc', 'rpcpassword'), server, port) return BitcoindClient(network, url, **kwargs) - def __init__(self, network='bitcoin', base_url='', denominator=100000000, wallet_name='', *args): + def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args): """ Open connection to bitcoin node @@ -128,6 +128,8 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, wallet :type: str :param denominator: Denominator for this currency. Should be always 100000000 (Satoshi's) for bitcoin :type: str + :param wallet_name: Name of Bitcoin core wallet to use. Make sure the wallet exists, otherwise connection will fail. + :type str """ if isinstance(network, Network): network = network.name @@ -136,8 +138,9 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, wallet bdc = self.from_config('', network) base_url = bdc.base_url network = bdc.network + wallet_name = '' if not len(args) >= 6 else args[6] if wallet_name: - base_url += "/wallet/%s" % wallet_name + base_url = base_url.replace("{wallet_name}", wallet_name) _logger.info("Connect to bitcoind") self.proxy = AuthServiceProxy(base_url) super(self.__class__, self).__init__(network, PROVIDERNAME, base_url, denominator, *args) diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index f8d6a9d8..d43a1a5f 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -168,14 +168,11 @@ def _provider_execute(self, method, *arguments): client = getattr(services, self.providers[sp]['provider']) providerclient = getattr(client, self.providers[sp]['client_class']) - base_url = self.providers[sp]['url'] - if 'bitcoind' in sp and self.wallet_name is not None: - base_url = f"{base_url}/wallet/{self.wallet_name}" - pc_instance = providerclient( - self.network, base_url, self.providers[sp]['denominator'], + self.network, self.providers[sp]['url'], self.providers[sp]['denominator'], self.providers[sp]['api_key'], self.providers[sp]['provider_coin_id'], - self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict) + self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict, + self.wallet_name) if not hasattr(pc_instance, method): _logger.debug("Method %s not found for provider %s" % (method, sp)) continue From a5958543503bdae4e67a98cf48f23e7f29b1917f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 12 Apr 2024 18:29:16 +0200 Subject: [PATCH 161/207] Fix issue with 1 byte data items --- bitcoinlib/scripts.py | 2 +- bitcoinlib/services/bitcoind.py | 2 +- examples/wallet_bitcoind_connected_wallets2.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 124f9f6c..77144d37 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -127,7 +127,7 @@ def get_data_type(data): elif ((data.startswith(b'\x02') or data.startswith(b'\x03')) and len(data) == 33) or \ (data.startswith(b'\x04') and len(data) == 65): return 'key' - elif len(data) == 20 or len(data) == 32 or len(data) == 64 or 1 < len(data) <= 4: + elif len(data) == 20 or len(data) == 32 or len(data) == 64 or 1 <= len(data) <= 4: return 'data-%d' % len(data) else: return 'other' diff --git a/bitcoinlib/services/bitcoind.py b/bitcoinlib/services/bitcoind.py index 5980ccf8..9d7c0fd0 100644 --- a/bitcoinlib/services/bitcoind.py +++ b/bitcoinlib/services/bitcoind.py @@ -138,7 +138,7 @@ def __init__(self, network='bitcoin', base_url='', denominator=100000000, *args) bdc = self.from_config('', network) base_url = bdc.base_url network = bdc.network - wallet_name = '' if not len(args) >= 6 else args[6] + wallet_name = '' if not len(args) > 6 else args[6] if wallet_name: base_url = base_url.replace("{wallet_name}", wallet_name) _logger.info("Connect to bitcoind") diff --git a/examples/wallet_bitcoind_connected_wallets2.py b/examples/wallet_bitcoind_connected_wallets2.py index 37968d0d..f37c60ee 100644 --- a/examples/wallet_bitcoind_connected_wallets2.py +++ b/examples/wallet_bitcoind_connected_wallets2.py @@ -25,3 +25,5 @@ w.scan(scan_gap_limit=1) w.info() +# TODO +# FIXME From 8822adbed92289fa5c9c60f002581d1bc0544db5 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 12 Apr 2024 22:40:41 +0200 Subject: [PATCH 162/207] Add Script.view method --- bitcoinlib/scripts.py | 39 ++++++++++++++++++++++++--------------- tests/test_script.py | 21 ++++++++++++++++++++- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 77144d37..28e5e9d1 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -223,7 +223,7 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True raise ScriptError("Cannot create script, please supply %s" % (tc if tc != 'data' else 'public key hash')) self.commands += command - if not (self.keys and self.signatures and self.blueprint): + if not (self.keys and self.signatures and self._blueprint): self._blueprint = [] for c in self.commands: if isinstance(c, int): @@ -471,22 +471,11 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level) def __repr__(self): - s_items = [] - for command in self.blueprint: - if isinstance(command, int): - s_items.append('op.' + opcodenames.get(command, 'unknown-op-%s' % command).lower()) - else: - s_items.append(command) + s_items = self.view(blueprint=True, as_list=True) return '' def __str__(self): - s_items = [] - for command in self.blueprint: - if isinstance(command, int): - s_items.append(opcodenames.get(command, 'unknown-op-%s' % command)) - else: - s_items.append(command) - return ' '.join(s_items) + return self.view(blueprint=True) def __add__(self, other): self.commands += other.commands @@ -512,7 +501,6 @@ def __hash__(self): @property def blueprint(self): - # TODO: create blueprint from commands if empty return self._blueprint @property @@ -558,6 +546,27 @@ def serialize_list(self): clist.append(bytes(cmd)) return clist + def view(self, blueprint=False, as_list=False, op_code_numbers=False): + s_items = [] + i = 0 + for command in self.commands: + if isinstance(command, int): + if op_code_numbers: + s_items.append(command) + else: + s_items.append(opcodenames.get(command, 'unknown-op-%s' % command)) + else: + if blueprint: + if self.blueprint and len(self.blueprint) >= i: + s_items.append(self.blueprint[i]) + else: + s_items.append('data-%d' % len(command)) + else: + s_items.append(command.hex()) + i += 1 + + return s_items if as_list else ' '.join(str(i) for i in s_items) + def evaluate(self, message=None, env_data=None): """ Evaluate script, run all commands and check if it is valid diff --git a/tests/test_script.py b/tests/test_script.py index 9dd9c9f7..8ad731a4 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -754,12 +754,18 @@ def test_script_add(self): def test_script_create_simple(self): script = Script([op.op_2, op.op_5, op.op_sub, op.op_1]) self.assertEqual(str(script), 'OP_2 OP_5 OP_SUB OP_1') - self.assertEqual(repr(script), '') + self.assertEqual(repr(script), '') self.assertEqual(script.serialize().hex(), '52559451') self.assertEqual(script.serialize_list(), [b'R', b'U', b'\x94', b'Q']) self.assertTrue(script.evaluate()) self.assertEqual(script.stack, [b'\3']) + def test_script_calc_evaluate(self): + s = Script.parse('0101016293016387') + self.assertListEqual(s.blueprint, ['data-1', 'data-1', 147, 'data-1', 135]) + self.assertTrue(s.view(), '01 62 OP_ADD 63 OP_EQUAL') + self.assertTrue(s.evaluate()) + def test_script_serialize(self): # Serialize p2sh_p2wsh tx 77ad5a0f9447dbfb9adcdb9b2437e91780519ec8ee24a8eda91b25a0666205cb from sigs and keys sig1 = b'0E\x02!\x00\xde\x8fDH\xe2\xd2\xe7F\x18>B\xe4\xfd\x87\xb8\x0b\x87\xfb\xb1\xd7ZYL\xa4\x08\x12\xe5\x07v' \ @@ -882,6 +888,19 @@ def test_script_large_redeemscript_packing(self): self.assertRaisesRegex(ScriptError, "Malformed script, not enough data found", Script.parse_hex, redeemscript_error) + def test_script_view(self): + script = bytes.fromhex( + '483045022100ba2ec7c40257b3d22864c9558738eea4d8771ab97888368124e176fdd6d7cd8602200f47c8d0c437df1ea8f98' + '19d344e05b9c93e38e88df1fc46abb6194506c50ce1012103e481f20561573cfd800e64efda61405917cb29e4bd20bed168c5' + '2b674937f53576a914f9cc73824051cc82d64a716c836c54467a21e22c88ac') + s = Script.parse(script) + expected_str = ('3045022100ba2ec7c40257b3d22864c9558738eea4d8771ab97888368124e176fdd6d7cd8602200f47c8d0c437' + 'df1ea8f9819d344e05b9c93e38e88df1fc46abb6194506c50ce101 03e481f20561573cfd800e64efda6140591' + '7cb29e4bd20bed168c52b674937f535 OP_DUP OP_HASH160 f9cc73824051cc82d64a716c836c54467a21e22c' + ' OP_EQUALVERIFY OP_CHECKSIG') + self.assertEqual(s.view(), expected_str) + self.assertEqual(s.blueprint, s.view(blueprint=True, as_list=True, op_code_numbers=True)) + self.assertEqual(str(s), s.view(blueprint=True)) class TestScriptMPInumbers(unittest.TestCase): From b85cc5091fa41778d41efb0800be1a39d7c671d5 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 09:25:44 +0200 Subject: [PATCH 163/207] Add as_bytes and as_hex method to Script --- bitcoinlib/keys.py | 5 +++++ bitcoinlib/scripts.py | 22 ++++++++++++++++++---- tests/test_script.py | 6 +++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 26b00e6c..255e21a6 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -858,6 +858,11 @@ def __init__(self, data='', hashed_data='', prefix=None, script_type=None, self.address_index = address_index if self.encoding is None: + # FIXME: Address should default to segwit if nothing is provided + # if self.script_type in ['p2pkh', 'p2sh', 'multisig', 'p2pk'] or self.witness_type == 'legacy': + # self.encoding = 'base58' + # else: + # self.encoding = 'bech32' if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr'] or self.witness_type == 'segwit': self.encoding = 'bech32' else: diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 28e5e9d1..9e7166ff 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -399,7 +399,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric # Extract extra information from script data for st in s.script_types[:1]: if st == 'multisig': - s.redeemscript = s.raw + s.redeemscript = s.as_bytes() s.sigs_required = s.commands[0] - 80 if s.sigs_required > len(keys): raise ScriptError("Number of signatures required (%d) is higher then number of keys (%d)" % @@ -479,7 +479,7 @@ def __str__(self): def __add__(self, other): self.commands += other.commands - self._raw += other.raw + self._raw += other.as_bytes() if other.message and not self.message: self.message = other.message self.is_locking = None @@ -497,18 +497,29 @@ def __bool__(self): return bool(self.commands) def __hash__(self): - return hash160(self.raw) + return hash160(self.as_bytes()) @property def blueprint(self): return self._blueprint @property + @deprecated def raw(self): if not self._raw: self._raw = self.serialize() return self._raw + def as_bytes(self): + if not self._raw: + self._raw = self.serialize() + return self._raw + + def as_hex(self): + if not self._raw: + self._raw = self.serialize() + return self._raw.hex() + def serialize(self): """ Serialize script. Return all commands and data as bytes @@ -588,7 +599,10 @@ def evaluate(self, message=None, env_data=None): :param message: Signed message to verify, normally a transaction hash. Leave empty to use Script.message. If supplied Script.message will be ignored. :type message: bytes - :param data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.data. If supplied Script.data will be ignored + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for + :type env_data: dict() + + multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.data. If supplied Script.data will be ignored :return bool: Valid or not valid """ diff --git a/tests/test_script.py b/tests/test_script.py index 8ad731a4..4a2ed5e1 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -559,7 +559,7 @@ def test_script_multisig_errors(self): def test_script_type_empty_unknown(self): s = Script.parse(b'') self.assertEqual(s.commands, []) - self.assertEqual(s.raw, b'') + self.assertEqual(s.as_bytes(), b'') def test_script_deserialize_sig_pk(self): scr = '493046022100cf4d7571dd47a4d47f5cb767d54d6702530a3555726b27b6ac56117f5e7808fe0221008cbb42233bb04d7f28a' \ @@ -778,7 +778,7 @@ def test_script_serialize(self): key3 = bytes.fromhex('0221b302fb92b25f171f1cd57bd22e60a1d2956f5831df17d94b3e9c3490aad598') redeemscript = Script([op.op_2, key1, key2, key3, op.op_3, op.op_checkmultisig]) script_hash = bytes.fromhex('b0fcc0caed77aeba9786f39920151162dfaf90e679aafab7a71e9b978e7d3f39') - self.assertEqual(redeemscript.raw.hex(), + self.assertEqual(redeemscript.as_hex(), '522102cd9107f8f1505ffd779bb7d8596ee686afc116e340f01b435871a038922255eb210297faa15d33e14e80c' 'a8a8616030b677941245fea12c4ef2ca28b14bd35ed42e1210221b302fb92b25f171f1cd57bd22e60a1d2956f58' '31df17d94b3e9c3490aad59853ae') @@ -809,7 +809,7 @@ def test_deserialize_script_with_sizebyte(self): script = b'\x00\x14y\t\x19r\x18lD\x9e\xb1\xde\xd2+x\xe4\r\x00\x9b\xdf\x00\x89' s1 = Script.parse(script_size_byte) s2 = Script.parse(script) - s1._raw = s2.raw + s1._raw = s2.as_bytes() self.assertDictEqualExt(s1.__dict__, s2.__dict__) def test_script_parse_redeemscript(self): From 48aabd86179eaa2fa73dd39eede28aaf7113b4ab Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 13:24:34 +0200 Subject: [PATCH 164/207] Add method to print Script object in human-readable string --- bitcoinlib/scripts.py | 62 +++++++++++++++++++++++++++++++++++++++++-- tests/test_script.py | 13 +++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 9e7166ff..5d0cf414 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -470,6 +470,46 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) data_length = len(script) return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level) + @classmethod + def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): + """ + Parse script in string format and return Script object. + Extracts script commands, keys, signatures and other data. + + >>> s = Script.parse_str("1 98 OP_ADD 99 OP_EQUAL") + >>> s + data-1 data-1 OP_ADD data-1 OP_EQUAL + >>> s.evaluate() + True + + :param script: Raw script to parse in bytes format + :type script: str + :param message: Signed message to verify, normally a transaction hash + :type message: bytes + :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts + :type env_data: dict + :param strict: Raise exception when script is malformed or incomplete + :type strict: bool + :param _level: Internal argument used to avoid recursive depth + :type _level: int + + :return Script: + """ + items = script.split(' ') + s_items = [] + for item in items: + if item.isdigit(): + ival = int(item) + if 0 < ival <= 16: + s_items.append(ival.to_bytes(1, 'big')) + else: + s_items.append(int_to_varbyteint(ival)) + elif item.startswith('OP_'): + s_items.append(getattr(op, item.lower(), 'unknown-command-%s' % item)) + else: + s_items.append(bytes.fromhex(item)) + return cls(s_items, message, env_data, strict, _level) + def __repr__(self): s_items = self.view(blueprint=True, as_list=True) return '' @@ -557,7 +597,21 @@ def serialize_list(self): clist.append(bytes(cmd)) return clist - def view(self, blueprint=False, as_list=False, op_code_numbers=False): + def view(self, blueprint=False, as_list=False, op_code_numbers=False, show_1_byte_data_as_int=True): + """ + View script as string in human-readable format. + + :param blueprint: Show blueprint only, without detailed data. + :type blueprint: bool + :param as_list: Show script as list + :type as_list: bool + :param op_code_numbers: Show opcodes as numbers instead of string. + :type op_code_numbers: bool + :param show_1_byte_data_as_int: Show 1 byte data objects as integers. + :type show_1_byte_data_as_int: bool + + :return str: + """ s_items = [] i = 0 for command in self.commands: @@ -573,7 +627,11 @@ def view(self, blueprint=False, as_list=False, op_code_numbers=False): else: s_items.append('data-%d' % len(command)) else: - s_items.append(command.hex()) + chex = command.hex() + if len(chex) == 2 and show_1_byte_data_as_int: + s_items.append(int(chex, 16)) + else: + s_items.append(chex) i += 1 return s_items if as_list else ' '.join(str(i) for i in s_items) diff --git a/tests/test_script.py b/tests/test_script.py index 4a2ed5e1..b0951662 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -902,6 +902,19 @@ def test_script_view(self): self.assertEqual(s.blueprint, s.view(blueprint=True, as_list=True, op_code_numbers=True)) self.assertEqual(str(s), s.view(blueprint=True)) + def test_script_str(self): + script_str = "1 98 OP_ADD 99 OP_EQUAL" + s = Script.parse_str(script_str) + self.assertEqual(s.view(), script_str) + self.assertTrue(s.evaluate()) + self.assertEqual(s.as_hex(), '0101016293016387') + + script_str_2 = "OP_DUP OP_HASH160 af8e14a2cecd715c363b3a72b55b59a31e2acac9 OP_EQUALVERIFY OP_CHECKSIG" + s = Script.parse_str(script_str_2) + clist = [118, 169, b'\xaf\x8e\x14\xa2\xce\xcdq\\6;:r\xb5[Y\xa3\x1e*\xca\xc9', 136, 172] + self.assertListEqual(s.commands, clist) + self.assertEqual(s.view(), script_str_2) + class TestScriptMPInumbers(unittest.TestCase): def test_encode_decode_numbers(self): From c6271c093d4de2bd89605a1e4836cb8e0a8cdddb Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 15:30:05 +0200 Subject: [PATCH 165/207] Fix wrong p2pk input address --- bitcoinlib/transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index f2d1e5b2..1b0e826f 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -478,7 +478,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): if self.keys: self.script_code = varstr(self.keys[0].public_byte) + b'\xac' self.unlocking_script_unsigned = self.script_code - addr_data = self.keys[0].public_byte + addr_data = hash160(self.keys[0].public_byte) if self.signatures and not self.unlocking_script: self.unlocking_script = varstr(self.signatures[0].as_der_encoded()) elif self.script_type == 'p2tr': # segwit_v1 From 1ea94430c27d2008e4b5ebe30b9dbf8696fca28b Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 18:24:25 +0200 Subject: [PATCH 166/207] Add method to update Transactions input scripts --- bitcoinlib/transactions.py | 13 +++++++++++++ tests/test_transactions.py | 3 +++ 2 files changed, 16 insertions(+) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 1b0e826f..fb9302d4 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -2065,6 +2065,19 @@ def update_totals(self): if self.vsize: self.fee_per_kb = int((self.fee / float(self.vsize)) * 1000) + def update_inputs(self, input_n=None): + """ + Update input scripts to reflect changes you made to one of more inputs. All inputs will be updated unless + you specificy a specific input. + + :param input_n: Input to update, leave empty to update all input scripts + + :return: + """ + input_list = range(0, len(self.inputs)) if input_n is None else [input_n] + for inp in input_list: + self.inputs[inp].update_scripts() + def save(self, filename=None): """ Store transaction object as file, so it can be imported in bitcoinlib later with the :func:`load` method. diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 37fb1ed4..d0360f51 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1089,6 +1089,9 @@ def test_transaction_sign_p2pk(self): self.assertTrue(t.verify()) self.assertEqual(t.signature_hash(sign_id=0).hex(), '67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986') + t.sign_and_update() + self.assertEqual(t.txid, "a3e18689f2b03659ae10735e332277e451b4270dbc46072b196baf63fb9a838b") + def test_transaction_sign_p2pk_value(self): wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ From 5938b3f3cbc681fdcfb5244395924036c4a286a1 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 18:38:55 +0200 Subject: [PATCH 167/207] Add p2pk unittest --- tests/test_script.py | 10 ++++++++-- tests/test_transactions.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_script.py b/tests/test_script.py index b0951662..cd857722 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -726,8 +726,14 @@ def test_script_verify_transaction_input_p2wsh(self): self.assertTrue(s.evaluate(message=transaction_hash, env_data=data)) def test_script_verify_transaction_input_p2pk(self): - pass - # TODO + p2pk_lockscript = '210312ed54eee6c84b440dd90623a714360196bebd842bfa64c7c7767b71b92a238dac' # key + checksig + p2pk_unlockscript = \ + ('463043021f52f02788988b941e3b810357762ccea5148e405edf124ea6b3b7eb9eba15430220609a9261612aaaa7544b7dae34' + '7b5dc3e53b0fc304957d6c4a46e1ae90a5d30001') # signature + script = p2pk_unlockscript + p2pk_lockscript + s = Script.parse_hex(script) + transaction_hash = bytes.fromhex("67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986") + self.assertTrue(s.evaluate(message=transaction_hash)) def test_script_verify_transaction_output_return(self): script = bytes.fromhex('6a26062c74e4b802d60ffdd1daa37b848e39a2b0ecb2de72c6ca24d71b87813b5e056cb7f1e8c8b0') diff --git a/tests/test_transactions.py b/tests/test_transactions.py index d0360f51..b8cad4fa 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1026,7 +1026,7 @@ def test_transaction_errors(self): class TestTransactionsScripts(unittest.TestCase, CustomAssertions): # def test_transaction_redeemscript_errors(self): - # exp_error = "Redeemscripts with more then 15 keys are non-standard and could result in locked up funds" + # exp_error = "Redeemscripts with more than 15 keys are non-standard and could result in locked up funds" # keys = [] # for n in range(20): # keys.append(HDKey().public_hex) @@ -1062,6 +1062,19 @@ def test_transaction_p2pk_script(self): self.assertEqual(t.inputs[0].script_type, 'signature') self.assertEqual(t.outputs[0].script_type, 'p2pk') + wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \ + 'nQETYvZu3j' + k = HDKey(wif) + rawtx = ('0100000001cb7b368efcf5f17b09e9e43ec3907cbed622a5b4b33addb4c9c6f0b8ce855c9f0000000047463043021f52f0278' + '8988b941e3b810357762ccea5148e405edf124ea6b3b7eb9eba15430220609a9261612aaaa7544b7dae347b5dc3e53b0fc304' + '957d6c4a46e1ae90a5d30001ffffffff01581b00000000000023210312ed54eee6c84b440dd90623a714360196bebd842bfa6' + '4c7c7767b71b92a238dac00000000') + t = Transaction.parse_hex(rawtx, network='testnet') + t.inputs[0].keys = [k.public()] + t.update_inputs(0) + t.sign_and_update() + self.assertTrue(t.verify()) + def test_transaction_sign_uncompressed(self): ki = Key('cTuDU2P6AhB72ZrhHRnFTcZRoHdnoWkp7sSMPCBnrMG23nRNnjUX', compressed=False) From 7c72d9445f65280e10eabd5172053687ca4c671e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 15 Apr 2024 22:43:01 +0200 Subject: [PATCH 168/207] Add as_hex and as_bytes to other classes --- bitcoinlib/keys.py | 32 ++++++++++++++++++++++++++++++++ bitcoinlib/transactions.py | 16 ++++++++++++++++ tests/test_transactions.py | 3 ++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 255e21a6..5b8565b4 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -1335,6 +1335,32 @@ def public_uncompressed_byte(self): def hex(self): return self.public_hex + def as_hex(self, private=False): + """ + Return hex representation of private or public key + + :param private: Private or public key + + :return str: + """ + if private: + return self.private_byte + else: + return self.public_hex + + def as_bytes(self, private=False): + """ + Return bytes representation of private or public key + + :param private: Private or public key + + :return bytes: + """ + if private: + return self.private_byte + else: + return self.public_byte + def as_dict(self, include_private=False): """ Get current Key class as dictionary. Byte values are represented by hexadecimal strings. @@ -2566,6 +2592,12 @@ def bytes(self): self._signature = self.r.to_bytes(32, 'big') + self.s.to_bytes(32, 'big') return self._signature + def as_hex(self): + return self.hex() + + def as_bytes(self): + return self.bytes() + def as_der_encoded(self, as_hex=False, include_hash_type=True): """ Get DER encoded signature diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index fb9302d4..2534728f 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1286,6 +1286,22 @@ def as_json(self): adict = self.as_dict() return json.dumps(adict, indent=4, default=str) + def as_bytes(self): + """ + Return raw serialized transaction as bytes string + + :return bytes: + """ + return self.raw() + + def as_hex(self): + """ + Return raw hex string of transaction as hex string + + :return: + """ + return self.raw_hex() + def info(self): """ Prints transaction information to standard output diff --git a/tests/test_transactions.py b/tests/test_transactions.py index b8cad4fa..818d7da6 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1103,7 +1103,8 @@ def test_transaction_sign_p2pk(self): self.assertEqual(t.signature_hash(sign_id=0).hex(), '67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986') t.sign_and_update() - self.assertEqual(t.txid, "a3e18689f2b03659ae10735e332277e451b4270dbc46072b196baf63fb9a838b") + if USE_FASTECDSA: + self.assertEqual(t.txid, "a3e18689f2b03659ae10735e332277e451b4270dbc46072b196baf63fb9a838b") def test_transaction_sign_p2pk_value(self): From 00b8029a275e2b065916631fd244bf0ef94e5cd2 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 16 Apr 2024 09:09:13 +0200 Subject: [PATCH 169/207] Use segwit as default for Address class --- bitcoinlib/keys.py | 12 ++++-------- bitcoinlib/wallets.py | 5 ++--- tests/test_keys.py | 4 ++-- tests/test_transactions.py | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 5b8565b4..10edd939 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -858,15 +858,11 @@ def __init__(self, data='', hashed_data='', prefix=None, script_type=None, self.address_index = address_index if self.encoding is None: - # FIXME: Address should default to segwit if nothing is provided - # if self.script_type in ['p2pkh', 'p2sh', 'multisig', 'p2pk'] or self.witness_type == 'legacy': - # self.encoding = 'base58' - # else: - # self.encoding = 'bech32' - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr'] or self.witness_type == 'segwit': - self.encoding = 'bech32' - else: + if (self.script_type in ['p2pkh', 'p2sh', 'multisig', 'p2pk'] or self.witness_type == 'legacy' or + self.witness_type == 'p2sh-segwit'): self.encoding = 'base58' + else: + self.encoding = 'bech32' self.hash_bytes = to_bytes(hashed_data) self.prefix = prefix self.redeemscript = b'' diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 86dc0174..5bbc687c 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1727,9 +1727,8 @@ def _new_key_multisig(self, public_keys, name, account_id, change, cosigner_id, # todo: pass key object, reuse key objects redeemscript = Script(script_types=['multisig'], keys=public_key_list, sigs_required=self.multisig_n_required).serialize() - script_type = 'p2sh' - if witness_type == 'p2sh-segwit': - script_type = 'p2sh_p2wsh' + script_type = 'p2sh' if witness_type == 'legacy' else \ + ('p2sh_p2wsh' if witness_type == 'p2sh-segwit' else 'p2wsh') address = Address(redeemscript, script_type=script_type, network=network, witness_type=witness_type) already_found_key = self.session.query(DbKey).filter_by(wallet_id=self.wallet_id, address=address.address).first() diff --git a/tests/test_keys.py b/tests/test_keys.py index 057ca558..b9c5712e 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -806,10 +806,10 @@ def test_keys_address_import_conversion(self): def test_keys_address_encodings(self): pk = '7cc7ed043b4240945e744387f8943151de86843025682bf40fa94ef086eeb686' - a = Address(pk, network='testnet') + a = Address(pk, network='testnet', witness_type='legacy') self.assertEqual(a.address, 'mmAXD1HJtV9pdffPvBJkuT4qQrbFMwb4pR') self.assertEqual(a.with_prefix(b'\x88'), 'wpcbpijWdzjj5W9ZXfdj2asW9U2q7gYCmw') - a = Address(pk, script_type='p2sh', network='testnet') + a = Address(pk, script_type='p2sh', network='testnet', witness_type='legacy') self.assertEqual(a.address, '2MxtnuEcoEpYJ9WWkzqcr87ChujVRk1DFsZ') a = Address(pk, encoding='bech32', network='testnet') self.assertEqual(a.address, 'tb1q8hehumvm039nxnwwtqdjr7qmm46sfxrdw7vc3g') diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 818d7da6..c269ab1b 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1009,7 +1009,7 @@ def test_transaction_create_with_address_objects(self): transaction_output = Output(value=91234, address=addr) t = Transaction([transaction_input], [transaction_output]) self.assertEqual(t.inputs[0].address, "1MMMMSUb1piy2ufrSguNUdFmAcvqrQF8M5") - self.assertEqual(t.outputs[0].address, "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa") + self.assertEqual(t.outputs[0].address, "bc1qer5sn9k8ccyqacrzs3sqc6zwmyzdznzupzevph") def test_transaction_info(self): t = Transaction() From 7171d5f8a9a328f936a165eee2900495713a6ad5 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 16 Apr 2024 10:51:13 +0200 Subject: [PATCH 170/207] Rename script to locking_script --- bitcoinlib/services/bitaps.py | 2 +- bitcoinlib/services/blockchair.py | 2 +- bitcoinlib/services/blocksmurfer.py | 2 +- bitcoinlib/services/blockstream.py | 2 +- bitcoinlib/services/mempool.py | 2 +- bitcoinlib/transactions.py | 41 ++++++++++++++++------------- bitcoinlib/wallets.py | 6 ++--- tests/import_test.tx | 2 +- tests/test_services.py | 2 +- tests/test_transactions.py | 6 ++--- 10 files changed, 35 insertions(+), 32 deletions(-) diff --git a/bitcoinlib/services/bitaps.py b/bitcoinlib/services/bitaps.py index d7b718b0..a31c6aab 100644 --- a/bitcoinlib/services/bitaps.py +++ b/bitcoinlib/services/bitaps.py @@ -75,7 +75,7 @@ def _parse_transaction(self, tx): sequence=ti['sequence'], index_n=int(n), value=0) else: t.add_input(prev_txid=ti['txId'], output_n=ti['vOut'], unlocking_script=ti['scriptSig'], - unlocking_script_unsigned=ti['scriptPubKey'], witnesses=ti.get('txInWitness', []), + locking_script=ti['scriptPubKey'], witnesses=ti.get('txInWitness', []), address='' if 'address' not in ti else ti['address'], sequence=ti['sequence'], index_n=int(n), value=ti['amount'], strict=self.strict) diff --git a/bitcoinlib/services/blockchair.py b/bitcoinlib/services/blockchair.py index 6133c5e4..dcefecd2 100644 --- a/bitcoinlib/services/blockchair.py +++ b/bitcoinlib/services/blockchair.py @@ -139,7 +139,7 @@ def gettransaction(self, tx_id): else: t.add_input(prev_txid=ti['transaction_hash'], output_n=ti['index'], unlocking_script=ti['spending_signature_hex'], index_n=index_n, value=ti['value'], - address=ti['recipient'], unlocking_script_unsigned=ti['script_hex'], + address=ti['recipient'], locking_script=ti['script_hex'], sequence=ti['spending_sequence'], strict=self.strict) index_n += 1 for to in res['data'][tx_id]['outputs']: diff --git a/bitcoinlib/services/blocksmurfer.py b/bitcoinlib/services/blocksmurfer.py index 95e172ce..ab8e56d4 100644 --- a/bitcoinlib/services/blocksmurfer.py +++ b/bitcoinlib/services/blocksmurfer.py @@ -100,7 +100,7 @@ def _parse_transaction(self, tx, block_height=None): public_hash=bytes.fromhex(ti['public_hash']), address=ti['address'], witness_type=ti['witness_type'], locktime_cltv=ti['locktime_cltv'], locktime_csv=ti['locktime_csv'], signatures=ti['signatures'], compressed=ti['compressed'], - encoding=ti['encoding'], unlocking_script_unsigned=ti['script_code'], + encoding=ti['encoding'], locking_script=ti['script_code'], sigs_required=ti['sigs_required'], sequence=ti['sequence'], witnesses=[bytes.fromhex(w) for w in ti['witnesses']], script_type=ti['script_type'], strict=self.strict) diff --git a/bitcoinlib/services/blockstream.py b/bitcoinlib/services/blockstream.py index 40933b56..cdabfb51 100644 --- a/bitcoinlib/services/blockstream.py +++ b/bitcoinlib/services/blockstream.py @@ -114,7 +114,7 @@ def _parse_transaction(self, tx): unlocking_script=ti['scriptsig'], value=ti['prevout']['value'], address='' if 'scriptpubkey_address' not in ti['prevout'] else ti['prevout']['scriptpubkey_address'], sequence=ti['sequence'], - unlocking_script_unsigned=ti['prevout']['scriptpubkey'], witnesses=witnesses, strict=self.strict) + locking_script=ti['prevout']['scriptpubkey'], witnesses=witnesses, strict=self.strict) index_n += 1 index_n = 0 if len(tx['vout']) > 101: diff --git a/bitcoinlib/services/mempool.py b/bitcoinlib/services/mempool.py index 31615524..419eb44b 100644 --- a/bitcoinlib/services/mempool.py +++ b/bitcoinlib/services/mempool.py @@ -111,7 +111,7 @@ def _parse_transaction(self, tx): t.add_input(prev_txid=ti['txid'], output_n=ti['vout'], unlocking_script=ti['scriptsig'], value=ti['prevout']['value'], address=ti['prevout'].get('scriptpubkey_address', ''), - unlocking_script_unsigned=ti['prevout']['scriptpubkey'], sequence=ti['sequence'], + locking_script=ti['prevout']['scriptpubkey'], sequence=ti['sequence'], witnesses=None if 'witness' not in ti else [bytes.fromhex(w) for w in ti['witness']], strict=self.strict) for to in tx['vout']: diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 2534728f..c7d17f9a 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -146,7 +146,7 @@ class Input(object): """ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - unlocking_script_unsigned=None, script=None, script_type=None, address='', + locking_script=None, script=None, script_type=None, address='', sequence=0xffffffff, compressed=None, sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True, network=DEFAULT_NETWORK): @@ -165,8 +165,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :type public_hash: bytes :param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional :type unlocking_script: bytes, hexstring - :param unlocking_script_unsigned: Unlocking script for signing transaction - :type unlocking_script_unsigned: bytes, hexstring + :param locking_script: Unlocking script for signing transaction + :type locking_script: bytes, hexstring :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Address string or object for input @@ -210,8 +210,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.output_n_int = int.from_bytes(output_n, 'big') self.output_n = output_n self.unlocking_script = b'' if unlocking_script is None else to_bytes(unlocking_script) - self.unlocking_script_unsigned = b'' if unlocking_script_unsigned is None \ - else to_bytes(unlocking_script_unsigned) + self.locking_script = b'' if locking_script is None \ + else to_bytes(locking_script) self.script = None self.hash_type = SIGHASH_ALL if isinstance(sequence, numbers.Number): @@ -294,8 +294,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= elif 'p2wsh' in self.script.script_types: self.script_type = 'p2sh_p2wsh' self.witness_type = 'p2sh-segwit' - if self.unlocking_script_unsigned and not self.signatures: - ls = Script.parse_bytes(self.unlocking_script_unsigned, strict=strict) + if self.locking_script and not self.signatures: + ls = Script.parse_bytes(self.locking_script, strict=strict) self.public_hash = self.public_hash if not ls.public_hash else ls.public_hash if ls.script_types[0] in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' @@ -417,7 +417,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): if not self.keys and not self.public_hash: return self.script_code = b'\x76\xa9\x14' + self.public_hash + b'\x88\xac' - self.unlocking_script_unsigned = self.script_code + self.locking_script = self.script_code addr_data = self.public_hash if self.signatures and self.keys: self.witnesses = [self.signatures[0].as_der_encoded() if hash_type else b'', self.keys[0].public_byte] @@ -439,7 +439,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): else: self.public_hash = hash160(self.redeemscript) addr_data = self.public_hash - self.unlocking_script_unsigned = self.redeemscript + # self.locking_script = self.redeemscript if self.redeemscript and self.keys: n_tag = self.redeemscript[0:1] @@ -477,7 +477,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): elif self.script_type == 'signature': if self.keys: self.script_code = varstr(self.keys[0].public_byte) + b'\xac' - self.unlocking_script_unsigned = self.script_code + self.locking_script = self.script_code addr_data = hash160(self.keys[0].public_byte) if self.signatures and not self.unlocking_script: self.unlocking_script = varstr(self.signatures[0].as_der_encoded()) @@ -491,14 +491,14 @@ def update_scripts(self, hash_type=SIGHASH_ALL): script_type=self.script_type, witness_type=self.witness_type).address if self.locktime_cltv: - self.unlocking_script_unsigned = script_add_locktime_cltv(self.locktime_cltv, - self.unlocking_script_unsigned) + self.locking_script = script_add_locktime_cltv(self.locktime_cltv, + self.locking_script) # if self.unlocking_script: # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) # if self.witness_type == 'segwit': # self.witnesses.insert(0, script_add_locktime_cltv(self.locktime_cltv, b'')) if self.locktime_csv: - self.unlocking_script_unsigned = script_add_locktime_csv(self.locktime_csv, self.unlocking_script_unsigned) + self.locking_script = script_add_locktime_csv(self.locktime_csv, self.locking_script) self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) return True @@ -580,7 +580,7 @@ def as_dict(self): 'public_hash': self.public_hash.hex(), 'script_code': self.script_code.hex(), 'unlocking_script': self.unlocking_script.hex(), - 'unlocking_script_unsigned': self.unlocking_script_unsigned.hex(), + 'locking_script': self.locking_script.hex(), 'witness_type': self.witness_type, 'witness': b''.join(self.witnesses).hex(), 'sort': self.sort, @@ -1618,7 +1618,10 @@ def raw(self, sign_id=None, hash_type=SIGHASH_ALL, witness_type=None): r += b'\1' r += varstr(i.unlocking_script) elif sign_id == i.index_n: - r += varstr(i.unlocking_script_unsigned) + if i.script_type == 'p2sh_multisig': + r += varstr(i.redeemscript) + else: + r += varstr(i.locking_script) else: r += b'\0' r += i.sequence.to_bytes(4, 'little') @@ -1807,7 +1810,7 @@ def sign_and_update(self, index_n=None): self.fee_per_kb = int((self.fee / float(self.vsize)) * 1000) def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - unlocking_script_unsigned=None, script_type=None, address='', + locking_script=None, script_type=None, address='', sequence=0xffffffff, compressed=True, sigs_required=None, sort=False, index_n=None, value=None, double_spend=False,locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True): @@ -1828,8 +1831,8 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash :type public_hash: bytes :param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional :type unlocking_script: bytes, hexstring - :param unlocking_script_unsigned: TODO: find better name... - :type unlocking_script_unsigned: bytes, str + :param locking_script: TODO: find better name... + :type locking_script: bytes, str :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Specify address of input if known, default is to derive from key or scripts @@ -1876,7 +1879,7 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash self.version_int = 2 self.inputs.append( Input(prev_txid=prev_txid, output_n=output_n, keys=keys, signatures=signatures, public_hash=public_hash, - unlocking_script=unlocking_script, unlocking_script_unsigned=unlocking_script_unsigned, + unlocking_script=unlocking_script, locking_script=locking_script, script_type=script_type, address=address, sequence=sequence, compressed=compressed, sigs_required=sigs_required, sort=sort, index_n=index_n, value=value, double_spend=double_spend, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, key_path=key_path, witness_type=witness_type, diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 5bbc687c..0c1e757d 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3795,7 +3795,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco for inp in input_arr: locktime_cltv = None locktime_csv = None - unlocking_script_unsigned = None + locking_script = None unlocking_script_type = '' if isinstance(inp, Input): prev_txid = inp.prev_txid @@ -3804,7 +3804,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco value = inp.value signatures = inp.signatures unlocking_script = inp.unlocking_script - unlocking_script_unsigned = inp.unlocking_script_unsigned + locking_script = inp.locking_script unlocking_script_type = inp.script_type address = inp.address sequence = inp.sequence @@ -3862,7 +3862,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco sigs_required=self.multisig_n_required, sort=self.sort_keys, compressed=key.compressed, value=value, signatures=signatures, unlocking_script=unlocking_script, address=address, - unlocking_script_unsigned=unlocking_script_unsigned, + locking_script=locking_script, sequence=sequence, locktime_cltv=locktime_cltv, locktime_csv=locktime_csv, witness_type=witness_type, key_path=key.path) # Calculate fees diff --git a/tests/import_test.tx b/tests/import_test.tx index c0de7cbb..faad7bc2 100644 --- a/tests/import_test.tx +++ b/tests/import_test.tx @@ -29,7 +29,7 @@ 'sigs_required': 2, 'sort': True, 'unlocking_script': '', - 'unlocking_script_unsigned': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', + 'locking_script': '52210289d3f95b15f53c666a4b70391e9a7cf6c771f6177d745557750a4160929a932e210331271d364803fd05e4a5b95acb2b0f200e9634dd75e95a577477762b8dacbcd32103547034e1e807362c5edd66d6951381ac2bde926b5244d5ce9cb1a82a4240bc8953ae', 'valid': None, 'value': 100000000, 'witness': '', diff --git a/tests/test_services.py b/tests/test_services.py index 19d56dbb..df6aac28 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -545,7 +545,7 @@ def test_service_gettransaction_segwit_p2wpkh(self): 'script_code': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', 'sequence': 4294967295, 'sigs_required': 1, - 'unlocking_script_unsigned': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', + 'locking_script': '76a9140ca7deb0a467679f0011efb2906a6e528a8d22ef88ac', 'value': 506323064} ], 'locktime': 0, diff --git a/tests/test_transactions.py b/tests/test_transactions.py index c269ab1b..68a5d83e 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -73,7 +73,7 @@ def test_transaction_input_add_public_key(self): def test_transaction_input_locking_script(self): ph = "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48" - ti = Input(ph, 0, unlocking_script_unsigned='76a91423e102597c4a99516f851406f935a6e634dbccec88ac', + ti = Input(ph, 0, locking_script='76a91423e102597c4a99516f851406f935a6e634dbccec88ac', witness_type='legacy') self.assertEqual(ti.address, '14GiCdJHj3bznWpcocjcu9ByCmDPEhEoP8') @@ -1638,7 +1638,7 @@ def test_transaction_locktime_cltv(self): pass # rawtx = '' # print(t.raw_hex()) - # print(t.inputs[0].unlocking_script_unsigned) + # print(t.inputs[0].locking_script) def test_transaction_cltv_error(self): # TODO @@ -1827,7 +1827,7 @@ def test_transaction_segwit_redeemscript_bug(self): keys=['0236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95cef6b7', '038b46795c92b8c9a6b40644a082be367e740f09b581a8b8c26b71d4b39f5cd963', '03f93f3a5633478ed3ab3ff9d2fe5ddeecb8e755fb267c9a3bc9d7242f58e4f258'], - unlocking_script_unsigned='52210236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95ce' + locking_script='52210236ab0a160e381d2eb6f01119937d29e697b78ca8be115617db972e0d95ce' 'f6b721038b46795c92b8c9a6b40644a082be367e740f09b581a8b8c26b71d4b39f' '5cd9632103f93f3a5633478ed3ab3ff9d2fe5ddeecb8e755fb267c9a3bc9d7242f' '58e4f25853ae', From 3de9927ef78c078b545c48893686f1ac9eebe956 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 16 Apr 2024 22:59:21 +0200 Subject: [PATCH 171/207] Cleanup Input method --- bitcoinlib/transactions.py | 31 ++++++++++++------------------- bitcoinlib/wallets.py | 10 ---------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index c7d17f9a..0702cc29 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -146,7 +146,7 @@ class Input(object): """ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - locking_script=None, script=None, script_type=None, address='', + locking_script=None, script_type=None, address='', sequence=0xffffffff, compressed=None, sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True, network=DEFAULT_NETWORK): @@ -272,26 +272,24 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= elif witnesses: self.witnesses = [bytes.fromhex(w) if isinstance(w, str) else w for w in witnesses] self.script_code = b'' - self.script = script # If unlocking script is specified extract keys, signatures, type from script - if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys) and not script: - self.script = Script.parse_bytes(self.unlocking_script, strict=strict) - self.keys = self.script.keys - self.signatures = self.script.signatures + if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys): + script = Script.parse_bytes(self.unlocking_script, strict=strict) + self.keys = script.keys + self.signatures = script.signatures if len(self.signatures): self.hash_type = self.signatures[0].hash_type - sigs_required = self.script.sigs_required - self.redeemscript = self.script.redeemscript if self.script.redeemscript else self.redeemscript - if len(self.script.script_types) == 1 and not self.script_type: - self.script_type = self.script.script_types[0] - elif self.script.script_types == ['signature_multisig', 'multisig']: + sigs_required = script.sigs_required + if len(script.script_types) == 1 and not self.script_type: + self.script_type = script.script_types[0] + elif script.script_types == ['signature_multisig', 'multisig']: self.script_type = 'p2sh_multisig' # TODO: Check if this if is necessary - if 'p2wpkh' in self.script.script_types: + if 'p2wpkh' in script.script_types: self.script_type = 'p2sh_p2wpkh' self.witness_type = 'p2sh-segwit' - elif 'p2wsh' in self.script.script_types: + elif 'p2wsh' in script.script_types: self.script_type = 'p2sh_p2wsh' self.witness_type = 'p2sh-segwit' if self.locking_script and not self.signatures: @@ -323,9 +321,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= else: kobj = key if kobj not in self.keys: - # if self.compressed is not None and kobj.compressed != self.compressed: - # _logger.warning("Key compressed is %s but Input class compressed argument is %s " % - # (kobj.compressed, self.compressed)) self.compressed = kobj.compressed self.keys.append(kobj) if self.compressed is None: @@ -439,7 +434,6 @@ def update_scripts(self, hash_type=SIGHASH_ALL): else: self.public_hash = hash160(self.redeemscript) addr_data = self.public_hash - # self.locking_script = self.redeemscript if self.redeemscript and self.keys: n_tag = self.redeemscript[0:1] @@ -491,8 +485,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): script_type=self.script_type, witness_type=self.witness_type).address if self.locktime_cltv: - self.locking_script = script_add_locktime_cltv(self.locktime_cltv, - self.locking_script) + self.locking_script = script_add_locktime_cltv(self.locktime_cltv, self.locking_script) # if self.unlocking_script: # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) # if self.witness_type == 'segwit': diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 0c1e757d..55f20f56 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3811,16 +3811,6 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco locktime_cltv = inp.locktime_cltv locktime_csv = inp.locktime_csv witness_type = inp.witness_type - # elif isinstance(inp, DbTransactionOutput): - # prev_txid = inp.transaction.txid - # output_n = inp.output_n - # key_id = inp.key_id - # value = inp.value - # signatures = None - # # FIXME: This is probably not an unlocking_script - # unlocking_script = inp.script - # unlocking_script_type = get_unlocking_script_type(inp.script_type) - # address = inp.key.address else: prev_txid = inp[0] output_n = inp[1] From 96006b51654d3eb1383ff37e0118536240ace369 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 19 Apr 2024 19:14:32 +0200 Subject: [PATCH 172/207] Rename offline parameter for broadcast in send methods --- bitcoinlib/tools/benchmark.py | 5 +- bitcoinlib/tools/clw.py | 6 +- bitcoinlib/wallets.py | 36 ++++----- examples/wallet_bitcoind_connected_wallets.py | 2 +- examples/wallet_multisig_3of5.py | 2 +- examples/wallets_segwit_testnet.py | 8 +- tests/test_transactions.py | 2 +- tests/test_wallets.py | 76 +++++++++---------- 8 files changed, 70 insertions(+), 67 deletions(-) diff --git a/bitcoinlib/tools/benchmark.py b/bitcoinlib/tools/benchmark.py index db07579e..facac8b6 100644 --- a/bitcoinlib/tools/benchmark.py +++ b/bitcoinlib/tools/benchmark.py @@ -102,7 +102,10 @@ def benchmark_wallets_multisig(): w.get_key(number_of_keys=2) w.utxos_update() to_address = HDKey(network=network).address() - t = w.sweep(to_address, offline=True) + if BITCOINLIB_VERSION >= '0.7.0': + t = w.sweep(to_address, broadcast=False) + else: + t = w.sweep(to_address, offline=True) key_pool = [i for i in range(0, n_keys - 1) if i != pk_n] while len(t.inputs[0].signatures) < sigs_req: co_id = random.choice(key_pool) diff --git a/bitcoinlib/tools/clw.py b/bitcoinlib/tools/clw.py index 27bf3a15..78263455 100644 --- a/bitcoinlib/tools/clw.py +++ b/bitcoinlib/tools/clw.py @@ -384,11 +384,11 @@ def main(): if not args.quiet: print_transaction(wt) elif args.sweep: - offline = True + broadcast = False print("Sweep wallet. Send all funds to %s" % args.sweep, file=output_to) if args.push: - offline = False - wt = wlt.sweep(args.sweep, offline=offline, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee, + broadcast = True + wt = wlt.sweep(args.sweep, broadcast=broadcast, network=args.network, fee_per_kb=args.fee_per_kb, fee=args.fee, replace_by_fee=args.rbf) if not wt: raise WalletError("Error occurred when sweeping wallet: %s. Are UTXO's available and updated?" % wt) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 55f20f56..c3b6fc50 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -816,12 +816,12 @@ def sign(self, keys=None, index_n=0, multisig_key_n=None, hash_type=SIGHASH_ALL, self.verify() self.error = "" - def send(self, offline=False): + def send(self, broadcast=True): """ Verify and push transaction to network. Update UTXO's in database after successful send - :param offline: Just return the transaction object and do not send it when offline = True. Default is False - :type offline: bool + :param broadcast: Verify transaction and broadcast, if set to False the transaction is verified but not broadcasted, i. Default is True + :type broadcast: bool :return None: @@ -832,7 +832,7 @@ def send(self, offline=False): self.error = "Cannot verify transaction" return None - if offline: + if not broadcast: return None srv = Service(network=self.network.name, wallet_name=self.hdwallet.name, providers=self.hdwallet.providers, @@ -4056,7 +4056,7 @@ def transaction_import_raw(self, rawtx, network=None): return rt def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, - min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, offline=True, number_of_change_outputs=1, + min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, broadcast=False, number_of_change_outputs=1, replace_by_fee=False): """ Create a new transaction with specified outputs and push it to the network. @@ -4066,7 +4066,7 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n Uses the :func:`transaction_create` method to create a new transaction, and uses a random service client to send the transaction. >>> w = Wallet('bitcoinlib_legacy_wallet_test') - >>> t = w.send([('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000)], offline=True) + >>> t = w.send([('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000)]) >>> t >>> t.outputs # doctest:+ELLIPSIS @@ -4092,8 +4092,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n :type max_utxos: int :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :type locktime: int - :param offline: Just return the transaction object and do not send it when offline = True. Default is True - :type offline: bool + :param broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE @@ -4128,18 +4128,18 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n transaction.calc_weight_units() transaction.fee_per_kb = int(float(transaction.fee) / float(transaction.vsize) * 1000) transaction.txid = transaction.signature_hash()[::-1].hex() - transaction.send(offline) + transaction.send(broadcast) return transaction def send_to(self, to_address, amount, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, - priv_keys=None, locktime=0, offline=True, number_of_change_outputs=1, replace_by_fee=False): + priv_keys=None, locktime=0, broadcast=False, number_of_change_outputs=1, replace_by_fee=False): """ Create transaction and send it with default Service objects :func:`services.sendrawtransaction` method. Wrapper for wallet :func:`send` method. >>> w = Wallet('bitcoinlib_legacy_wallet_test') - >>> t = w.send_to('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000, offline=True) + >>> t = w.send_to('1J9GDZMKEr3ZTj8q6pwtMy4Arvt92FDBTb', 200000) >>> t >>> t.outputs # doctest:+ELLIPSIS @@ -4163,8 +4163,8 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ :type priv_keys: HDKey, list :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :type locktime: int - :param offline: Just return the transaction object and do not send it when offline = True. Default is True - :type offline: bool + :param broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE @@ -4175,11 +4175,11 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ outputs = [(to_address, amount)] return self.send(outputs, input_key_id=input_key_id, account_id=account_id, network=network, fee=fee, - min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, offline=offline, + min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, broadcast=broadcast, number_of_change_outputs=number_of_change_outputs, replace_by_fee=replace_by_fee) def sweep(self, to_address, account_id=None, input_key_id=None, network=None, max_utxos=999, min_confirms=1, - fee_per_kb=None, fee=None, locktime=0, offline=True, replace_by_fee=False): + fee_per_kb=None, fee=None, locktime=0, broadcast=False, replace_by_fee=False): """ Sweep all unspent transaction outputs (UTXO's) and send them to one or more output addresses. @@ -4216,8 +4216,8 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma :type fee: int, str :param locktime: Transaction level locktime. Locks the transaction until a specified block (value from 1 to 5 million) or until a certain time (Timestamp in seconds after 1-jan-1970). Default value is 0 for transactions without locktime :type locktime: int - :param offline: Just return the transaction object and do not send it when offline = True. Default is True - :type offline: bool + :param broadcast: Just return the transaction object and do not send it when broadcast = False. Default is False + :type broadcast: bool :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE :type replace_by_fee: bool @@ -4271,7 +4271,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma "outputs, use amount value = 0 to indicate a change/rest output") return self.send(to_list, input_arr, network=network, fee=fee, min_confirms=min_confirms, locktime=locktime, - offline=offline, replace_by_fee=replace_by_fee) + broadcast=broadcast, replace_by_fee=replace_by_fee) def wif(self, is_private=False, account_id=0): """ diff --git a/examples/wallet_bitcoind_connected_wallets.py b/examples/wallet_bitcoind_connected_wallets.py index 3bcb931d..f17b479b 100644 --- a/examples/wallet_bitcoind_connected_wallets.py +++ b/examples/wallet_bitcoind_connected_wallets.py @@ -60,7 +60,7 @@ else: print("Found testnet coins. Wallet balance: %d" % w.balance()) # Send some coins to our own wallet - t = w.send_to(w.get_key().address, 1000, fee=200, offline=False) + t = w.send_to(w.get_key().address, 1000, fee=200, broadcast=True) t.info() # If you now run bitcoin-cli listunspent 0, you should see the 1 or 2 new utxo's for this transaction. diff --git a/examples/wallet_multisig_3of5.py b/examples/wallet_multisig_3of5.py index 9e1f62bc..51998414 100644 --- a/examples/wallet_multisig_3of5.py +++ b/examples/wallet_multisig_3of5.py @@ -96,7 +96,7 @@ print("\nNew unspent outputs found!") print("Now a new transaction will be created to sweep this wallet and send bitcoins to a testnet faucet") send_to_address = '2NGZrVvZG92qGYqzTLjCAewvPZ7JE8S8VxE' - t = wallet3o5.sweep(send_to_address, min_confirms=0, offline=True) + t = wallet3o5.sweep(send_to_address, min_confirms=0, broadcast=False) print("Now send the raw transaction hex to one of the other cosigners to sign using sign_raw.py") print("Raw transaction: %s" % t.raw_hex()) else: diff --git a/examples/wallets_segwit_testnet.py b/examples/wallets_segwit_testnet.py index 25338ef3..dc8a24af 100644 --- a/examples/wallets_segwit_testnet.py +++ b/examples/wallets_segwit_testnet.py @@ -70,7 +70,7 @@ print("Balance to low, please deposit at least %s to %s" % (((tx_fee+tx_amount)*4)-w1.balance(), w1_key.address)) print("Sending transaction from wallet #1 to wallet #2:") - t = w1.send_to(w2_key.address, 4 * tx_amount, fee=tx_fee, offline=False) + t = w1.send_to(w2_key.address, 4 * tx_amount, fee=tx_fee, broadcast=True) t.info() while True: @@ -79,7 +79,7 @@ sleep(1) if w2.utxos(): print("Sending transaction from wallet #2 to wallet #3:") - t2 = w2.send_to(w3_key.address, 3 * tx_amount, fee=tx_fee, offline=False) + t2 = w2.send_to(w3_key.address, 3 * tx_amount, fee=tx_fee, broadcast=True) t2.info() break @@ -89,7 +89,7 @@ sleep(1) if w3.utxos(): print("Sending transaction from wallet #3 to wallet #4:") - t3 = w3.send_to(w4_key.address, 2 * tx_amount, fee=tx_fee, offline=False) + t3 = w3.send_to(w4_key.address, 2 * tx_amount, fee=tx_fee, broadcast=True) t3.sign(wif2) t3.send() t3.info() @@ -101,7 +101,7 @@ sleep(1) if w4.utxos(): print("Sending transaction from wallet #4 to wallet #1:") - t4 = w4.send_to(w1_key.address, tx_amount, fee=tx_fee, offline=False) + t4 = w4.send_to(w1_key.address, tx_amount, fee=tx_fee, broadcast=True) t4.sign(wif2) t4.send() t4.info() diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 68a5d83e..90544b0a 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1454,7 +1454,7 @@ def test_transaction_multisig_same_sigs_for_keys(self): 'aeffffffff02a0acb903000000001976a9146170e2cc18a4415f807cc4b29c50e52bd1157c4b88ac787bb6030000000017a' \ '914bb87f55537ee62a232f042f39fbc0d86b77d07fb8700000000' t = Transaction.parse(bytes.fromhex(traw)) - t.inputs[0].value = 972612109 + t.inputs[0].value = 124798308 self.assertTrue(t.verify()) def test_transaction_multisig_1_key_15_signatures(self): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index b15aaead..a0b05297 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -562,7 +562,7 @@ def test_wallet_create_uncompressed_masterkey(self): network='bitcoinlib_test', witness_type='legacy', db_uri=self.database_uri) wlt.get_key() wlt.utxos_update() - self.assertIsNone(wlt.sweep('216xtQvbcG4o7Yz33n7VCGyaQhiytuvoqJY', offline=False).error) + self.assertIsNone(wlt.sweep('216xtQvbcG4o7Yz33n7VCGyaQhiytuvoqJY', broadcast=True).error) def test_wallet_create_invalid_key(self): # Test for issue #206 @@ -871,7 +871,7 @@ def test_wallet_multiple_networks_value(self): self.assertGreaterEqual(len(w.utxos(network='testnet')), 2) w.utxos_update(networks='bitcoinlib_test') self.assertEqual(len(w.utxos(network='bitcoinlib_test')), 4) - t = w.send_to('blt1qctnl4yk3qepjy3uu36kved5ds6q9g8c6raan7l', '50 mTST', offline=False) + t = w.send_to('blt1qctnl4yk3qepjy3uu36kved5ds6q9g8c6raan7l', '50 mTST', broadcast=True) self.assertTrue(t.pushed) t = w.send_to('tb1qhq6x777xpj32jm005qppxa6gyxt3qrc376ye6c', '0.1 mTBTC', fee=1000) self.assertFalse(t.pushed) @@ -913,7 +913,7 @@ def test_wallet_multi_networks_send_transaction(self): self.assertListEqual(wallet.addresslist(network='testnet'), tbtc_addresses) t = wallet.send_to('21EsLrvFQdYWXoJjGX8LSEGWHFJDzSs2F35', 10000000, account_id=1, - network='bitcoinlib_test', fee=1000, offline=False) + network='bitcoinlib_test', fee=1000, broadcast=True) self.assertIsNone(t.error) self.assertTrue(t.verified) self.assertEqual(wallet.balance(network='bitcoinlib_test', account_id=1), 589999000) @@ -965,9 +965,9 @@ def test_wallet_bitcoinlib_testnet_sendto(self): w.new_key() w.utxos_update() - self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', 5000000, offline=False).error) - self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', '10000 satTST', offline=False).error) - self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', Value('40 mTST'), offline=False).error) + self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', 5000000, broadcast=True).error) + self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', '10000 satTST', broadcast=True).error) + self.assertIsNone(w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', Value('40 mTST'), broadcast=True).error) def test_wallet_bitcoinlib_testnet_send_utxos_updated(self): w = Wallet.create( @@ -977,7 +977,7 @@ def test_wallet_bitcoinlib_testnet_send_utxos_updated(self): w.utxos_update() self.assertEqual(len(w.utxos()), 2) - t = w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', 10000, offline=False) + t = w.send_to('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', 10000, broadcast=True) self.assertTrue(t.pushed) def test_wallet_bitcoinlib_testnet_sendto_no_funds_txfee(self): @@ -989,7 +989,7 @@ def test_wallet_bitcoinlib_testnet_sendto_no_funds_txfee(self): w.utxos_update() balance = w.balance() self.assertRaisesRegex(WalletError, "Not enough unspent transaction outputs found", - w.send_to, '21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', balance, offline=False) + w.send_to, '21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', balance, broadcast=True) def test_wallet_bitcoinlib_testnet_sweep(self): w = Wallet.create( @@ -1000,10 +1000,10 @@ def test_wallet_bitcoinlib_testnet_sweep(self): w.new_key() w.new_key() w.utxos_update() - self.assertIsNone(w.sweep('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', offline=False).error) + self.assertIsNone(w.sweep('21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', broadcast=True).error) self.assertEqual(w.utxos(), []) self.assertRaisesRegex(WalletError, "Cannot sweep wallet, no UTXO's found", - w.sweep, '21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', offline=False) + w.sweep, '21DBmFUMQMP7A6KeENXgZQ4wJdSCeGc2zFo', broadcast=True) class TestWalletMultisig(unittest.TestCase): @@ -1085,7 +1085,7 @@ def test_wallet_multisig_bitcoin_transaction_send_offline(self): 0, 1) t = wl.transaction_create([('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', 100000)], fee=10000) t.sign(pk2.subkey_for_path("m/45'/2/0/0")) - t.send(offline=True) + t.send(broadcast=False) self.assertTrue(t.verify()) self.assertIsNone(t.error) self.assertEqual(t.export()[0][2], 'out') @@ -1104,7 +1104,7 @@ def test_wallet_multisig_bitcoin_transaction_send_no_subkey_for_path(self): 0, 1) t = wl.transaction_create([('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', 100000)], fee=10000) t.sign(pk2) - t.send(offline=True) + t.send(broadcast=False) self.assertTrue(t.verify()) self.assertIsNone(t.error) @@ -1125,7 +1125,7 @@ def test_wallet_multisig_bitcoin_transaction_send_fee_priority(self): t2 = wl.transaction_create([('3CuJb6XrBNddS79vr27SwqgR4oephY6xiJ', 100000)], fee='low') t2.sign(pk2) - t2.send(offline=True) + t2.send(broadcast=False) self.assertTrue(t2.verify()) self.assertIsNone(t2.error) @@ -1148,7 +1148,7 @@ def test_wallet_multisig_litecoin_transaction_send_offline(self): 0, 1) t = wl.transaction_create([('3DrP2R8XmHswUyeK9GeYgHJxvyxTfMNkid', 100000)], fee=10000) t.sign(pk2.subkey_for_path("m/45'/2/0/0")) - t.send(offline=True) + t.send(broadcast=False) self.assertTrue(t.verify()) self.assertIsNone(t.error) @@ -1348,7 +1348,7 @@ def test_wallet_multisig_sign_with_external_single_key(self): key_list, sigs_required=2, network=network, db_uri=self.database_uri) wallet.new_key() wallet.utxos_update() - wt = wallet.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, offline=False) + wt = wallet.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, broadcast=True) self.assertFalse(wt.verify()) wt.sign(hdkey) self.assertTrue(wt.verify()) @@ -1524,7 +1524,7 @@ def test_wallet_multisig_replace_sig_bug(self): w.get_keys() w.utxos_update() to_address = HDKey(network=network, witness_type=witness_type).address() - t = w.send_to(to_address, 1000000, offline=False) + t = w.send_to(to_address, 1000000, broadcast=True) key_pool = [i for i in range(0, len(key_list) - 1) if i != 0] co_ids = [4, 2] while len(t.inputs[0].signatures) < sigs_req: @@ -1574,7 +1574,7 @@ def test_wallet_key_import_and_sign_multisig(self): db_uri=self.database_uri) as wlt: wlt.new_key() wlt.utxos_update() - wt = wlt.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, offline=False) + wt = wlt.send_to('21A6yyUPRL9hZZo1Rw4qP5G6h9idVVLUncE', 10000000, broadcast=True) wt.sign(hdkey) wt.send() self.assertIsNone(wt.error) @@ -1619,17 +1619,17 @@ def test_wallet_import_private_for_known_public_p2sh_segwit(self): witness_type='p2sh-segwit', network='bitcoinlib_test', db_uri=self.database_uri) w.get_key() w.utxos_update() - t = w.sweep('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', offline=False) + t = w.sweep('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', broadcast=True) self.assertEqual(len(t.inputs[0].signatures), 1) self.assertFalse(t.verify()) w.import_key(pk2) wc0 = w.cosigner[0] self.assertEqual(len(wc0.keys(is_private=False)), 0) - t2 = w.send_to('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', 1000000, offline=False) + t2 = w.send_to('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', 1000000, broadcast=True) self.assertEqual(len(t2.inputs[0].signatures), 2) self.assertTrue(t2.verify()) - t3 = w.sweep('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', min_confirms=0, offline=False) + t3 = w.sweep('23CvEnQKsTVGgqCZzW6ewXPSJH9msFPsBt3', min_confirms=0, broadcast=True) self.assertEqual(len(t3.inputs[0].signatures), 2) self.assertTrue(t3.verify()) self.assertAlmostEqual(t3.outputs[0].value, 198981935, delta=100000) @@ -1697,7 +1697,7 @@ def test_wallet_transactions_full(self): self.assertIsInstance(tx, WalletTransaction) def test_wallet_sweep_public_wallet(self): - tx = self.wallet.sweep('mwCvJviVTzjEKLZ1UW5jaepjWHUeoYrEe7', fee_per_kb=50000, offline=False) + tx = self.wallet.sweep('mwCvJviVTzjEKLZ1UW5jaepjWHUeoYrEe7', fee_per_kb=50000, broadcast=True) prev_tx_list_check = [ '4fffbf7c50009e5477ac06b9f1741890f7237191d1cf5489c7b4039df2ebd626', '9423919185b15c633d2fcd5095195b521a8970f01ca6413c43dbe5646e5b8e1e', @@ -1781,7 +1781,7 @@ def test_wallet_balance_update(self): wlt.utxos_update() self.assertEqual(wlt.balance(), 200000000) - t = wlt.send_to(to_key.address, 9000, offline=False) + t = wlt.send_to(to_key.address, 9000, broadcast=True) self.assertEqual(wlt.balance(), 200000000 - t.fee) self.assertEqual(t.txid, wlt.transaction_spent(t.inputs[0].prev_txid, t.inputs[0].output_n)) self.assertEqual(t.txid, wlt.transaction_spent(t.inputs[0].prev_txid.hex(), t.inputs[0].output_n_int)) @@ -1815,7 +1815,7 @@ def test_wallet_add_dust_to_fee(self): db_uri=self.database_uri) to_key = wlt.get_key() wlt.utxos_update() - t = wlt.send_to(to_key.address, 99992000, offline=False) + t = wlt.send_to(to_key.address, 99992000, broadcast=True) self.assertEqual(t.fee, 8000) del wlt @@ -1825,7 +1825,7 @@ def test_wallet_transactions_send_update_utxos(self): to_keys = wlt.get_keys(number_of_keys=5) wlt.utxos_update() self.assertEqual(wlt.balance(), 1000000000) - t = wlt.send_to(to_keys[0].address, 550000000, offline=False) + t = wlt.send_to(to_keys[0].address, 550000000, broadcast=True) wlt._balance_update(min_confirms=0) self.assertEqual(wlt.balance(), 1000000000 - t.fee) self.assertEqual(len(wlt.utxos()), 6) @@ -1881,7 +1881,7 @@ def test_wallet_transaction_load_segwit_size(self): wlt = Wallet.create('bcltestwlt2-size', keys=pk, network='bitcoinlib_test', witness_type='segwit', db_uri=self.database_uri) wlt.utxos_update() - t = wlt.send_to(wlt.get_key().address, 50000000, offline=False) + t = wlt.send_to(wlt.get_key().address, 50000000, broadcast=True) t.verify() self.assertTrue(t.verified) @@ -1914,7 +1914,7 @@ def test_wallet_transaction_fee_zero_problem(self): wlt = Wallet.create(name='bcltestwlt7', network='bitcoinlib_test', db_uri=self.database_uri) nk = wlt.get_key() wlt.utxos_update() - t = wlt.send_to(nk.address, 100000000, offline=False) + t = wlt.send_to(nk.address, 100000000, broadcast=True) self.assertTrue(t.pushed) self.assertNotEqual(t.fee, 0) @@ -2004,7 +2004,7 @@ def test_wallet_transaction_sign_with_wif(self): db_uri=self.database_uri) w.get_key() w.utxos_update() - t = w.send_to('blt1q285vnphcs4r0t5dw06tmxl7aryj3jnx88duehv4p7eldsshrmygsmlq84z', 2000, fee=1000, offline=False) + t = w.send_to('blt1q285vnphcs4r0t5dw06tmxl7aryj3jnx88duehv4p7eldsshrmygsmlq84z', 2000, fee=1000, broadcast=True) t.sign(wif2) self.assertIsNone(t.send()) self.assertTrue(t.pushed) @@ -2042,7 +2042,7 @@ def test_wallet_transaction_send_keyid(self): keys = w.get_keys(number_of_keys=2) w.utxos_update() t = w.send_to('blt1qtk5swtntg8gvtsyr3kkx3mjcs5ncav84exjvde', 150000000, input_key_id=keys[1].key_id, - offline=False) + broadcast=True) self.assertEqual(t.inputs[0].address, keys[1].address) self.assertTrue(t.verified) self.assertRaisesRegex(WalletError, "Not enough unspent transaction outputs found", w.send_to, @@ -2154,7 +2154,7 @@ def test_wallet_sweep_multiple_inputs_or_outputs(self): w.utxos_update(utxos=utxos) t = w.sweep([('14pThTJoEnQxbJJVYLhzSKcs6EmZgShscX', 11000), ('1GSffHyTGyKvQWpKHc7Mjd8K2bmbt7g9Xx', 0)], - offline=True, fee=2000) + broadcast=False, fee=2000) self.assertIn(362000, [o.value for o in t.outputs]) self.assertTrue(t.verified) @@ -2241,13 +2241,13 @@ def test_wallet_avoid_forced_address_reuse(self): k1 = w.get_key() w.utxos_update() k2 = w.new_key() - w.sweep(k2.address, offline=False) + w.sweep(k2.address, broadcast=True) # Send dust to used address - w.send_to(k1, 400, min_confirms=0, offline=False) + w.send_to(k1, 400, min_confirms=0, broadcast=True) # Try to spend dust - t = w.sweep('zz3nA9VNyXwwyKKALckuhQ5sYdxMuzCQuQ', min_confirms=0, offline=False) + t = w.sweep('zz3nA9VNyXwwyKKALckuhQ5sYdxMuzCQuQ', min_confirms=0, broadcast=True) self.assertEqual(len(t.inputs), 1) def test_wallet_avoid_forced_address_reuse2(self): @@ -2313,8 +2313,8 @@ def test_wallet_merge_transactions(self): w.utxos_update() u = w.utxos() - t1 = w.send_to(w.get_key(), 200000, input_key_id=u[0]['key_id'], offline=False) - t2 = w.send_to(w.get_key(), 300000, input_key_id=u[1]['key_id'], offline=False) + t1 = w.send_to(w.get_key(), 200000, input_key_id=u[0]['key_id'], broadcast=True) + t2 = w.send_to(w.get_key(), 300000, input_key_id=u[1]['key_id'], broadcast=True) t = t1 + t2 self.assertTrue(t.verified) self.assertEqual(t1.input_total + t2.input_total, t.input_total) @@ -2405,7 +2405,7 @@ def test_wallet_segwit_p2wpkh_send(self): db_uri=self.database_uri) w.get_key() w.utxos_update() - t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) + t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, broadcast=True) self.assertEqual(t.witness_type, 'segwit') self.assertEqual(t.inputs[0].script_type, 'sig_pubkey') self.assertEqual(t.inputs[0].witness_type, 'segwit') @@ -2419,7 +2419,7 @@ def test_wallet_segwit_p2wsh_send(self): k = w.get_key() w.utxos_update() w.utxos_update(key_id=k.key_id) # Test db updates after second request and only update single key - t = w.send_to('blt1q7r60he62p52u6h9zyxl6ew4dmmshpmk5sluaax48j9c7zyxu6m0smrjqxa', 10000, offline=False) + t = w.send_to('blt1q7r60he62p52u6h9zyxl6ew4dmmshpmk5sluaax48j9c7zyxu6m0smrjqxa', 10000, broadcast=True) self.assertEqual(t.witness_type, 'segwit') self.assertEqual(t.inputs[0].script_type, 'p2sh_multisig') self.assertEqual(t.inputs[0].witness_type, 'segwit') @@ -2431,7 +2431,7 @@ def test_wallet_segwit_p2sh_p2wpkh_send(self): db_uri=self.database_uri) w.get_key() w.utxos_update() - t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) + t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, broadcast=True) self.assertEqual(t.witness_type, 'segwit') self.assertEqual(t.inputs[0].script_type, 'sig_pubkey') self.assertEqual(t.inputs[0].witness_type, 'p2sh-segwit') @@ -2445,7 +2445,7 @@ def test_wallet_segwit_p2sh_p2wsh_send(self): db_uri=self.database_uri) w.get_key() w.utxos_update() - t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, offline=False) + t = w.send_to('blt1q7ywlg3lsyntsmp74jh65pnkntk3csagdwpz78k', 10000, broadcast=True) self.assertEqual(t.witness_type, 'segwit') self.assertEqual(t.inputs[0].script_type, 'p2sh_multisig') self.assertEqual(t.inputs[0].witness_type, 'p2sh-segwit') From fcb3eb1f34a784b0914e3e354512f76cb7c4d5a6 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Apr 2024 14:05:14 +0200 Subject: [PATCH 173/207] Fix removing wallet transactions --- bitcoinlib/wallets.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index c3b6fc50..cb928fa9 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1035,9 +1035,19 @@ def delete(self): tx_query = session.query(DbTransaction).filter_by(txid=txid) tx = tx_query.scalar() session.query(DbTransactionOutput).filter_by(transaction_id=tx.id).delete() + for inp in tx.inputs: + prev_utxos = session.query(DbTransactionOutput).join(DbTransaction).\ + filter(DbTransaction.txid == inp.prev_txid, DbTransactionOutput.output_n == inp.output_n, + DbTransactionOutput.spent.is_(True), DbTransaction.wallet_id == self.hdwallet.wallet_id).all() + for u in prev_utxos: + u.spent = False session.query(DbTransactionInput).filter_by(transaction_id=tx.id).delete() - session.query(DbKey).filter_by(latest_txid=txid).update({DbKey.latest_txid: None}) + qr = session.query(DbKey).filter_by(latest_txid=txid) + qr.update({DbKey.latest_txid: None, DbKey.used: False}) res = tx_query.delete() + key = qr.scalar() + if key: + self.hdwallet._balance_update(key_id=key.id) self.hdwallet._commit() return res @@ -3167,7 +3177,7 @@ def utxo_add(self, address, value, txid, output_n, confirmations=1, script=''): """ Add a single UTXO to the wallet database. To update all utxo's use :func:`utxos_update` method. - Use this method for testing, offline wallets or if you wish to override standard method of retreiving UTXO's + Use this method for testing, offline wallets or if you wish to override standard method of retrieving UTXO's This method does not check if UTXO exists or is still spendable. @@ -3195,7 +3205,7 @@ def utxo_add(self, address, value, txid, output_n, confirmations=1, script=''): 'txid': txid, 'value': value } - return self.utxos_update(utxos=[utxo]) + return self.utxos_update(utxos=[utxo], rescan_all=False) def utxo_last(self, address): """ From 87c1f07dbea567dac7215806cd80fda786bb15fa Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Apr 2024 14:39:25 +0200 Subject: [PATCH 174/207] Allow to disable random tx outputs order --- bitcoinlib/wallets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index cb928fa9..54be7cfd 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -4067,7 +4067,7 @@ def transaction_import_raw(self, rawtx, network=None): def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, priv_keys=None, max_utxos=None, locktime=0, broadcast=False, number_of_change_outputs=1, - replace_by_fee=False): + random_output_order=True, replace_by_fee=False): """ Create a new transaction with specified outputs and push it to the network. Inputs can be specified but if not provided they will be selected from wallets utxo's @@ -4106,6 +4106,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n :type broadcast: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int + :param random_output_order: Shuffle order of transaction outputs to increase privacy. Default is True + :type random_output_order: bool :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE :type replace_by_fee: bool @@ -4117,8 +4119,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n (len(input_arr), max_utxos)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee, - min_confirms, max_utxos, locktime, number_of_change_outputs, True, - replace_by_fee) + min_confirms, max_utxos, locktime, number_of_change_outputs, + random_output_order, replace_by_fee) transaction.sign(priv_keys) # Calculate exact fees and update change output if necessary if fee is None and transaction.fee_per_kb and transaction.change: @@ -4130,7 +4132,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n "Recreate transaction with correct fee" % (transaction.fee, fee_exact)) transaction = self.transaction_create(output_arr, input_arr, input_key_id, account_id, network, fee_exact, min_confirms, max_utxos, locktime, - number_of_change_outputs, True, replace_by_fee) + number_of_change_outputs, random_output_order, + replace_by_fee) transaction.sign(priv_keys) transaction.rawtx = transaction.raw() @@ -4142,7 +4145,8 @@ def send(self, output_arr, input_arr=None, input_key_id=None, account_id=None, n return transaction def send_to(self, to_address, amount, input_key_id=None, account_id=None, network=None, fee=None, min_confirms=1, - priv_keys=None, locktime=0, broadcast=False, number_of_change_outputs=1, replace_by_fee=False): + priv_keys=None, locktime=0, broadcast=False, number_of_change_outputs=1, random_output_order=True, + replace_by_fee=False): """ Create transaction and send it with default Service objects :func:`services.sendrawtransaction` method. @@ -4177,6 +4181,8 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ :type broadcast: bool :param number_of_change_outputs: Number of change outputs to create when there is a change value. Default is 1. Use 0 for random number of outputs: between 1 and 5 depending on send and change amount :type number_of_change_outputs: int + :param random_output_order: Shuffle order of transaction outputs to increase privacy. Default is True + :type random_output_order: bool :param replace_by_fee: Signal replace by fee and allow to send a new transaction with higher fees. Sets sequence value to SEQUENCE_REPLACE_BY_FEE :type replace_by_fee: bool @@ -4186,7 +4192,8 @@ def send_to(self, to_address, amount, input_key_id=None, account_id=None, networ outputs = [(to_address, amount)] return self.send(outputs, input_key_id=input_key_id, account_id=account_id, network=network, fee=fee, min_confirms=min_confirms, priv_keys=priv_keys, locktime=locktime, broadcast=broadcast, - number_of_change_outputs=number_of_change_outputs, replace_by_fee=replace_by_fee) + number_of_change_outputs=number_of_change_outputs, random_output_order=random_output_order, + replace_by_fee=replace_by_fee) def sweep(self, to_address, account_id=None, input_key_id=None, network=None, max_utxos=999, min_confirms=1, fee_per_kb=None, fee=None, locktime=0, broadcast=False, replace_by_fee=False): From 0150aa43678362e9e4c45542adf8e61e1f13fa19 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 22 Apr 2024 16:28:21 +0200 Subject: [PATCH 175/207] Adjust fee estimates for segwit transactions --- bitcoinlib/services/services.py | 8 ++++---- bitcoinlib/wallets.py | 6 ++++-- tests/test_wallets.py | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index d43a1a5f..59934ec3 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -436,12 +436,12 @@ def sendrawtransaction(self, rawtx): """ return self._provider_execute('sendrawtransaction', rawtx) - def estimatefee(self, blocks=3, priority=''): + def estimatefee(self, blocks=5, priority=''): """ Estimate fee per kilobyte for a transaction for this network with expected confirmation within a certain amount of blocks - :param blocks: Expected confirmation time in blocks. Default is 3. + :param blocks: Expected confirmation time in blocks. :type blocks: int :param priority: Priority for transaction: can be 'low', 'medium' or 'high'. Overwrites value supplied in 'blocks' argument :type priority: str @@ -451,9 +451,9 @@ def estimatefee(self, blocks=3, priority=''): self.results_cache_n = 0 if priority: if priority == 'low': - blocks = 10 + blocks = 25 elif priority == 'high': - blocks = 1 + blocks = 2 if self.min_providers <= 1: # Disable cache if comparing providers fee = self.cache.estimatefee(blocks) if fee: diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 54be7cfd..59547465 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -4247,6 +4247,7 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma utxos = utxos[0:max_utxos] input_arr = [] total_amount = 0 + if not utxos: raise WalletError("Cannot sweep wallet, no UTXO's found") for utxo in utxos: @@ -4257,17 +4258,18 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma total_amount += utxo['value'] srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) + fee_modifier = 1 if self.witness_type == 'legacy' else 0.5 if isinstance(fee, str): n_outputs = 1 if not isinstance(to_address, list) else len(to_address) fee_per_kb = srv.estimatefee(priority=fee) tr_size = 125 + (len(input_arr) * (77 + self.multisig_n_required * 72)) + n_outputs * 30 - fee = 100 + int((tr_size / 1000.0) * fee_per_kb) + fee = int(100 + ((tr_size / 1000.0) * fee_per_kb * fee_modifier)) if not fee: if fee_per_kb is None: fee_per_kb = srv.estimatefee() tr_size = 125 + (len(input_arr) * 125) - fee = int((tr_size / 1000.0) * fee_per_kb) + fee = int((tr_size / 1000.0) * fee_per_kb * fee_modifier) if total_amount - fee <= self.network.dust_amount: raise WalletError("Amount to send is smaller then dust amount: %s" % (total_amount - fee)) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index a0b05297..d4f8d5a9 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2280,6 +2280,29 @@ def test_wallet_transactions_delete(self): w.transactions()[0].delete() self.assertEqual(len(w.transactions()), 1) + def test_wallet_transaction_delete_reverse_latest(self): + pkm = 'elephant dust deer company win final' + expected_utxos = ['520208458b4f93ef7f1a4df447b6fedb50888aaa098ab501b32b1df3f88daa86', + 'ea7bd8fe970ca6430cebbbf914ce2feeb369c3ae95edc117725dbe21519ccdab'] + expected_txid = '00c6f17bab32ac30979c284a36537f288ed85648810d5d479fcf2a526cdcd3f6' + + w = Wallet.create("remove_utxos_test", keys=pkm, network="bitcoinlib_test", db_uri=self.database_uri) + w.utxos_update() + self.assertEqual([], [False for u in w.utxos() if u['txid'] not in expected_utxos]) + + w.get_key() + t = w.send_to("blt1qad80unqvexkhm96rxysra2mczy74zlszjr4ty9", "0.5 TST", broadcast=True, + fee=4799, random_output_order=False) + self.assertEqual(w.balance(), 199995201) + self.assertEqual(t.txid, expected_txid) + + wlt_utxos = [u['txid'] for u in w.utxos()] + self.assertEqual(wlt_utxos[2], expected_txid) + + wt = w.transaction(t.txid) + wt.delete() + self.assertEqual([], [False for u in w.utxos() if u['txid'] not in expected_utxos]) + def test_wallet_create_import_key(self): w = wallet_create_or_open("test_wallet_create_import_key_private", network='bitcoinlib_test', db_uri=self.database_uri) From 1f0c5db126aee1bb9f209e099847aee38e9c14ac Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 28 Apr 2024 19:52:18 +0200 Subject: [PATCH 176/207] Add Transaction method bumpfee --- bitcoinlib/config/config.py | 2 + bitcoinlib/transactions.py | 63 +++++++++++++++-- bitcoinlib/wallets.py | 4 +- tests/test_transactions.py | 133 ++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 9 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 7a637c8e..12f8e4bc 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -104,6 +104,8 @@ SIGNATURE_VERSION_STANDARD = 0 SIGNATURE_VERSION_SEGWIT = 1 +BUMPFEE_DEFAULT_MULTIPLIER = 5 + # Mnemonics DEFAULT_LANGUAGE = 'english' diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 0702cc29..9ca4b491 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -146,10 +146,10 @@ class Input(object): """ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - locking_script=None, script_type=None, address='', - sequence=0xffffffff, compressed=None, sigs_required=None, sort=False, index_n=0, - value=0, double_spend=False, locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, - witnesses=None, encoding=None, strict=True, network=DEFAULT_NETWORK): + locking_script=None, script_type=None, address='', sequence=0xffffffff, compressed=None, + sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, locktime_cltv=None, + locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True, + network=DEFAULT_NETWORK): """ Create a new transaction input @@ -594,7 +594,7 @@ class Output(object): def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_script=b'', spent=False, output_n=0, script_type=None, witver=0, encoding=None, spending_txid='', spending_index_n=None, - strict=True, network=DEFAULT_NETWORK): + strict=True, change=None, network=DEFAULT_NETWORK): """ Create a new transaction output @@ -631,6 +631,8 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri :type spending_index_n: int :param strict: Raise exception when output is malformed, incomplete or not understood :type strict: bool + :param change: Is this a change output back to own wallet or not? Used for replace-by-fee. + :type change: bool :param network: Network, leave empty for default :type network: str, Network """ @@ -639,6 +641,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri raise TransactionError("Please specify address, lock_script, public key or public key hash when " "creating output") + self.change = change self.network = network if not isinstance(network, Network): self.network = Network(network) @@ -1354,6 +1357,8 @@ def info(self): spent_str = 'S' elif to.spent is False: spent_str = 'U' + if to.change: + spent_str += 'C' print("-", to.address, Value.from_satoshi(to.value, network=self.network).str(1), to.script_type, spent_str) if replace_by_fee: @@ -1880,7 +1885,8 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash return index_n def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_script=b'', spent=False, - output_n=None, encoding=None, spending_txid=None, spending_index_n=None, strict=True): + output_n=None, encoding=None, spending_txid=None, spending_index_n=None, strict=True, + change=None): """ Add an output to this transaction @@ -1908,6 +1914,8 @@ def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_sc :type spending_index_n: int :param strict: Raise exception when output is malformed or incomplete :type strict: bool + :param change: Is this a change output back to own wallet or not? Used for replace-by-fee. + :type change: bool :return int: Transaction output number (output_n) """ @@ -1923,7 +1931,7 @@ def add_output(self, value, address='', public_hash=b'', public_key=b'', lock_sc self.outputs.append(Output(value=int(value), address=address, public_hash=public_hash, public_key=public_key, lock_script=lock_script, spent=spent, output_n=output_n, encoding=encoding, spending_txid=spending_txid, spending_index_n=spending_index_n, - strict=strict, network=self.network.name)) + strict=strict, change=change, network=self.network.name)) return output_n def merge_transaction(self, transaction): @@ -2137,3 +2145,44 @@ def shuffle(self): """ self.shuffle_inputs() self.shuffle_outputs() + + def bumpfee(self, fee=0, extra_fee=0): + if not self.fee: + raise TransactionError("Current transaction fee is zero, cannot increase fee") + if not self.vsize: + self.estimate_size() + + minimal_required_fee = self.vsize + if fee: + if fee < self.fee + minimal_required_fee: + raise TransactionError("Fee cannot be less than minimal required fee") + extra_fee = fee - self.fee + elif extra_fee: + if extra_fee < minimal_required_fee: + raise TransactionError("Extra fee cannot be less than minimal required fee") + fee = self.fee + extra_fee + else: + fee = self.fee + (minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER) + extra_fee = fee - self.fee + + remaining_fee = extra_fee + outputs_to_delete = [] + for outp in [o for o in self.outputs if o.change]: + if not remaining_fee: + break + if outp.value > remaining_fee * 2: + outp.value -= extra_fee + remaining_fee = 0 + elif outp.value < remaining_fee: + remaining_fee -= outp.value + outputs_to_delete.append(outp) + else: + outputs_to_delete.append(outp) + remaining_fee = 0 + + if remaining_fee: + raise TransactionError("Not enough unspent outputs to bump transaction fee") + self.fee = fee + for o in outputs_to_delete: + self.outputs.remove(o) + self.sign_and_update() diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 59547465..0451b271 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3749,7 +3749,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco addr = o[0] if isinstance(addr, WalletKey): addr = addr.key() - transaction.add_output(value, addr) + transaction.add_output(value, addr, change=False) srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) @@ -3940,7 +3940,7 @@ def transaction_create(self, output_arr, input_arr=None, input_key_id=None, acco change_amounts = [transaction.change] for idx, ck in enumerate(change_keys): - on = transaction.add_output(change_amounts[idx], ck.address, encoding=self.encoding) + on = transaction.add_output(change_amounts[idx], ck.address, encoding=self.encoding, change=True) transaction.outputs[on].key_id = ck.key_id # Shuffle output order to increase privacy diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 90544b0a..d2ad82cf 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1273,6 +1273,139 @@ def test_transaction_non_standard_input_script_0001(self): self.assertEqual(t.txid, txid) self.assertEqual(traw, t.raw_hex()) + def test_transaction_bumpfee(self): + prev_txid = '67f621f333f59492ac4652900bef1b803eb5d04b71dc363a815bbde0ffe374ab' + output_n = 0 + value = 100000 + + # Test 1 - bumpfee, extra_fee and remove output + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(extra_fee=5000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 2 - bumpfee, extra_fee, round dust and remove output + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(extra_fee=4000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 3 - bumpfee, fee and remove output + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(10000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 4 - bumpfee, fee, round dust and remove output + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + self.assertEqual(t.fee, 5000) + self.assertEqual(len(t.outputs), 2) + txid_before = t.txid + t.bumpfee(10000) + self.assertEqual(t.fee, 10000) + self.assertEqual(len(t.outputs), 1) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 5 - bumpfee, 2 change outputs, no parameters + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True), + Output(6667, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + t.sign_and_update() + self.assertEqual(t.fee, 3333) + txid_before = t.txid + t.bumpfee() + self.assertEqual(t.fee, 4068) + self.assertNotEqual(t.txid, txid_before) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + # Test 6 - bumpfee, fee, 2 change outputs + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True), + Output(6667, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test') + t.sign_and_update() + self.assertEqual(t.fee, 3333) + self.assertEqual(len(t.outputs), 3) + t.bumpfee(fee=18000) + self.assertEqual(t.fee, 20000) + self.assertEqual(len(t.outputs), 1) + self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) + + def test_transaction_bumpfee_errors(self): + prev_txid = '67f621f333f59492ac4652900bef1b803eb5d04b71dc363a815bbde0ffe374ab' + output_n = 0 + value = 100000 + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + outputs = [ + Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(100000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', + change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=0) + self.assertEqual(t.fee, 0) + self.assertRaisesRegex(TransactionError, "Current transaction fee is zero, cannot increase fee", t.bumpfee) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(100, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9900) + self.assertRaisesRegex(TransactionError, "Not enough unspent outputs to bump transaction fee", t.bumpfee) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(500, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9500) + self.assertRaisesRegex(TransactionError, "Fee cannot be less than minimal required fee", t.bumpfee, fee=100) + + inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + outputs = [ + Output(190000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), + Output(500, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] + t = Transaction([inp], outputs, network='bitcoinlib_test', fee=9500) + self.assertRaisesRegex(TransactionError, "Extra fee cannot be less than minimal required fee", t.bumpfee, + extra_fee=100) class TestTransactionsMultisigSoroush(unittest.TestCase): # Source: Example from From 5413b31cb93b21d1f7a5e547acbf2641e492a315 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 28 Apr 2024 21:00:27 +0200 Subject: [PATCH 177/207] Improve sweep method fee estimation --- bitcoinlib/wallets.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 0451b271..2b134f92 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -4258,18 +4258,17 @@ def sweep(self, to_address, account_id=None, input_key_id=None, network=None, ma total_amount += utxo['value'] srv = Service(network=network, wallet_name=self.name, providers=self.providers, cache_uri=self.db_cache_uri) - fee_modifier = 1 if self.witness_type == 'legacy' else 0.5 + fee_modifier = 1 if self.witness_type == 'legacy' else 0.6 if isinstance(fee, str): - n_outputs = 1 if not isinstance(to_address, list) else len(to_address) fee_per_kb = srv.estimatefee(priority=fee) - tr_size = 125 + (len(input_arr) * (77 + self.multisig_n_required * 72)) + n_outputs * 30 - fee = int(100 + ((tr_size / 1000.0) * fee_per_kb * fee_modifier)) - + fee = None if not fee: if fee_per_kb is None: fee_per_kb = srv.estimatefee() - tr_size = 125 + (len(input_arr) * 125) - fee = int((tr_size / 1000.0) * fee_per_kb * fee_modifier) + n_outputs = 1 if not isinstance(to_address, list) else len(to_address) + tr_size = 125 + (len(input_arr) * (77 + self.multisig_n_required * 72)) + n_outputs * 30 + fee = int(100 + ((tr_size / 1000.0) * fee_per_kb * fee_modifier)) + if total_amount - fee <= self.network.dust_amount: raise WalletError("Amount to send is smaller then dust amount: %s" % (total_amount - fee)) From b7bed7c47a6d800c216690f60d5a02a210bb0f3c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 28 Apr 2024 21:20:25 +0200 Subject: [PATCH 178/207] Fix Windows delete tx unittest --- tests/test_wallets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index d4f8d5a9..3cf517e2 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2294,11 +2294,13 @@ def test_wallet_transaction_delete_reverse_latest(self): t = w.send_to("blt1qad80unqvexkhm96rxysra2mczy74zlszjr4ty9", "0.5 TST", broadcast=True, fee=4799, random_output_order=False) self.assertEqual(w.balance(), 199995201) - self.assertEqual(t.txid, expected_txid) + if USE_FASTECDSA: + self.assertEqual(t.txid, expected_txid) + else: + expected_txid = t.txid wlt_utxos = [u['txid'] for u in w.utxos()] self.assertEqual(wlt_utxos[2], expected_txid) - wt = w.transaction(t.txid) wt.delete() self.assertEqual([], [False for u in w.utxos() if u['txid'] not in expected_utxos]) From e975baf19db051596df5f6ba3fd886007e15531f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 28 Apr 2024 21:36:57 +0200 Subject: [PATCH 179/207] Fix unittest for providers returning out-of-date data --- tests/test_services.py | 2 +- tests/test_wallets.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index df6aac28..85d9a815 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -609,7 +609,7 @@ def test_service_blockcount(self): n_blocks = None for provider in srv.results: if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=10000 if nw == 'testnet' else 3, + self.assertAlmostEqual(srv.results[provider], n_blocks, delta=25000 if nw == 'testnet' else 3, msg="Network %s, provider %s value %d != %d" % (nw, provider, srv.results[provider], n_blocks)) n_blocks = srv.results[provider] diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3cf517e2..a76708ef 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1729,6 +1729,9 @@ def test_wallet_offline_create_transaction(self): del wlt def test_wallet_scan(self): + # TODO: Fix MySQL scan errors + if self.database_uri.startswith('mysql'): + self.skipTest('TODO: Fix MySQL scan errors') account_key = 'tpubDCmJWqxWch7LYDhSuE1jEJMbAkbkDm3DotWKZ69oZfNMzuw7U5DwEaTVZHGPzt5j9BJDoxqVkPHt2EpUF66FrZhpfq' \ 'ZY6DFj6x61Wwbrg8Q' wallet = wallet_create_or_open('scan-test', keys=account_key, network='testnet', db_uri=self.database_uri) @@ -2363,8 +2366,8 @@ def test_wallet_anti_fee_sniping(self): w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) block_height = Service(network='testnet').blockcount() - # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta to 5000 - self.assertAlmostEqual(t.locktime, block_height+1, delta=10000) + # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta + self.assertAlmostEqual(t.locktime, block_height+1, delta=25000) w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) From 814abfe553bc63f39da182bf2fb99dcc1b403fcd Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 5 May 2024 21:53:53 +0200 Subject: [PATCH 180/207] Remove bitaps, bitgo from some unittests --- bitcoinlib/data/providers.json | 4 ++-- tests/test_services.py | 2 +- tests/test_wallets.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/data/providers.json b/bitcoinlib/data/providers.json index 2ba2be0a..40190a30 100644 --- a/bitcoinlib/data/providers.json +++ b/bitcoinlib/data/providers.json @@ -224,7 +224,7 @@ "network": "testnet", "client_class": "LitecoinBlockexplorerClient", "provider_coin_id": "", - "url": "https://tbtc1.blockbook.bitaccess.net/api/v1/", + "url": "https://tbtc1.trezor.io/api/v1/", "api_key": "", "priority": 10, "denominator": 100000000, @@ -279,7 +279,7 @@ "network": "testnet", "client_class": "BlockbookClient", "provider_coin_id": "", - "url": "https://tbtc1.blockbook.bitaccess.net/api/v2/", + "url": "https://tbtc2.trezor.io/api/v2/", "api_key": "", "priority": 10, "denominator": 100000000, diff --git a/tests/test_services.py b/tests/test_services.py index 85d9a815..effe7e58 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -604,7 +604,7 @@ def test_service_network_litecoin_legacy(self): def test_service_blockcount(self): for nw in ['bitcoin', 'litecoin', 'testnet']: - srv = ServiceTest(min_providers=3, cache_uri='', network=nw) + srv = ServiceTest(min_providers=3, cache_uri='', network=nw, exclude_providers=['bitgo', 'bitaps']) srv.blockcount() n_blocks = None for provider in srv.results: diff --git a/tests/test_wallets.py b/tests/test_wallets.py index a76708ef..837f5dd2 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2365,9 +2365,9 @@ def test_wallet_anti_fee_sniping(self): w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - block_height = Service(network='testnet').blockcount() + block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps']).blockcount() # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta - self.assertAlmostEqual(t.locktime, block_height+1, delta=25000) + self.assertAlmostEqual(t.locktime, block_height+1, delta=3) w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) From a2784a1ba2798bd7deef262d19b97265c299e169 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 6 May 2024 17:45:52 +0200 Subject: [PATCH 181/207] Fix issue with excluded service providers for non-bitcoin networks --- bitcoinlib/services/services.py | 9 +++++---- tests/test_wallets.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 59934ec3..7c476f97 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -113,9 +113,9 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr if (self.providers_defined[p]['network'] == network or self.providers_defined[p]['network'] == '') and \ (not providers or self.providers_defined[p]['provider'] in providers): self.providers.update({p: self.providers_defined[p]}) - for nop in exclude_providers: - if nop in self.providers: - del(self.providers[nop]) + exclude_providers_keys = {pi: self.providers[pi]['provider'] for pi in self.providers if self.providers[pi]['provider'] in exclude_providers}.keys() + for provider_key in exclude_providers_keys: + del(self.providers[provider_key]) if not self.providers: raise ServiceError("No providers found for network %s" % network) @@ -141,7 +141,8 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr self.ignore_priority = ignore_priority self.strict = strict if self.min_providers > 1: - self._blockcount = Service(network=network, cache_uri=cache_uri).blockcount() + self._blockcount = Service(network=network, cache_uri=cache_uri, providers=providers, + exclude_providers=exclude_providers, timeout=timeout).blockcount() else: self._blockcount = self.blockcount() diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 837f5dd2..a010b488 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2365,7 +2365,7 @@ def test_wallet_anti_fee_sniping(self): w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps']).blockcount() + block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount() # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta self.assertAlmostEqual(t.locktime, block_height+1, delta=3) From 3e496289340976e2c35aeaac2dcea90ce261e94f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 6 May 2024 20:22:26 +0200 Subject: [PATCH 182/207] Add exclude providers unittest --- bitcoinlib/wallets.py | 3 ++- tests/test_services.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 2b134f92..00a30a5f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -2485,7 +2485,8 @@ def keys(self, account_id=None, name=None, key_id=None, change=None, depth=None, keys2.append({k: v for (k, v) in key.items() if k[:1] != '_' and k != 'wallet' and k not in private_fields}) return keys2 - qr.session.close() + # qr.session.close() + qr.session.commit() return keys def keys_networks(self, used=None, as_dict=False): diff --git a/tests/test_services.py b/tests/test_services.py index effe7e58..4f9b00cf 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -792,6 +792,12 @@ def test_service_transaction_unconfirmed(self): self.assertIsNone(t.date) self.assertIsNone(t.block_height) + def test_service_exlude_providers(self): + srv = ServiceTest(network='testnet', cache_uri='') + providers = [srv.providers[pi]['provider'] for pi in srv.providers] + srv2 = ServiceTest(network='testnet', exclude_providers=providers[1:], cache_uri='') + self.assertEqual(len(srv2.providers), 1) + class TestServiceCache(unittest.TestCase): From 19ca898fa56c833d7bdefb51d37d1871bc226587 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 8 May 2024 00:12:02 +0200 Subject: [PATCH 183/207] Add bumpfee method to wallet --- bitcoinlib/db.py | 1 + bitcoinlib/transactions.py | 28 +++++++++--- bitcoinlib/wallets.py | 91 ++++++++++++++++++++++++++++++++++++-- tests/test_transactions.py | 2 +- tests/test_wallets.py | 65 ++++++++++++++++++++++++++- 5 files changed, 175 insertions(+), 12 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 01a1c9cf..d84a0e7c 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -496,6 +496,7 @@ class DbTransactionOutput(Base): spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction") spending_txid = Column(LargeBinary(33), doc="Transaction hash of input which spends this output") spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output") + is_change = Column(Boolean, default=False, doc="Is this a change output / output to own wallet?") __table_args__ = (UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique'),) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 9ca4b491..cec139fc 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -158,7 +158,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :param output_n: Output number in previous transaction. :type output_n: bytes, int :param keys: A list of Key objects or public / private key string in various formats. If no list is provided but a bytes or string variable, a list with one item will be created. Optional - :type keys: list (bytes, str, Key) + :type keys: list (bytes, str, Key, HDKey) :param signatures: Specify optional signatures :type signatures: list (bytes, str, Signature) :param public_hash: Public key hash or script hash. Specify if key is not available @@ -1822,15 +1822,15 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash :param output_n: Output number in previous transaction. :type output_n: bytes, int :param keys: Public keys can be provided to construct an Unlocking script. Optional - :type keys: bytes, str + :type keys: list (bytes, str, Key, HDKey) :param signatures: Add signatures to input if already known :type signatures: bytes, str :param public_hash: Specify public hash from key or redeemscript if key is not available :type public_hash: bytes :param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional :type unlocking_script: bytes, hexstring - :param locking_script: TODO: find better name... - :type locking_script: bytes, str + :param locking_script: Locking script (scriptPubKey) of previous output if known + :type locking_script: bytes, hexstring :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Specify address of input if known, default is to derive from key or scripts @@ -2147,6 +2147,23 @@ def shuffle(self): self.shuffle_outputs() def bumpfee(self, fee=0, extra_fee=0): + """ + Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be + increased to speed up inclusion on the blockchain. + + If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code + below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings. + + The extra fee will be deducted from change output. This method fails if there are not enough change outputs + to cover fees. + + :param fee: New fee for this transaction + :type fee: int + :param extra_fee: Extra fee to add to current transaction fee + :type extra_fee: int + + :return None: + """ if not self.fee: raise TransactionError("Current transaction fee is zero, cannot increase fee") if not self.vsize: @@ -2162,7 +2179,8 @@ def bumpfee(self, fee=0, extra_fee=0): raise TransactionError("Extra fee cannot be less than minimal required fee") fee = self.fee + extra_fee else: - fee = self.fee + (minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER) + fee = int(self.fee * (1.03 ** BUMPFEE_DEFAULT_MULTIPLIER) + + (minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER)) extra_fee = fee - self.fee remaining_fee = extra_fee diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 00a30a5f..d758d655 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -30,7 +30,7 @@ from bitcoinlib.networks import Network from bitcoinlib.values import Value, value_to_satoshi from bitcoinlib.services.services import Service -from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type +from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type, TransactionError from bitcoinlib.scripts import Script from sqlalchemy import func, or_ @@ -753,7 +753,7 @@ def from_txid(cls, hdwallet, txid): public_key = key.key().public_hex outputs.append(Output(value=out.value, address=address, public_key=public_key, lock_script=out.script, spent=out.spent, output_n=out.output_n, - script_type=out.script_type, network=network)) + script_type=out.script_type, network=network, change=out.is_change)) return cls(hdwallet=hdwallet, inputs=inputs, outputs=outputs, locktime=db_tx.locktime, version=db_tx.version, network=network, fee=db_tx.fee, fee_per_kb=fee_per_kb, @@ -949,7 +949,7 @@ def store(self): if not tx_output: new_tx_item = DbTransactionOutput( transaction_id=txidn, output_n=to.output_n, key_id=key_id, address=to.address, value=to.value, - spent=spent, script=to.lock_script, script_type=to.script_type) + spent=spent, script=to.lock_script, script_type=to.script_type, is_change=to.change) sess.add(new_tx_item) elif key_id: tx_output.key_id = key_id @@ -1040,7 +1040,10 @@ def delete(self): filter(DbTransaction.txid == inp.prev_txid, DbTransactionOutput.output_n == inp.output_n, DbTransactionOutput.spent.is_(True), DbTransaction.wallet_id == self.hdwallet.wallet_id).all() for u in prev_utxos: - u.spent = False + # Check if output is spent in another transaction + if session.query(DbTransactionInput).filter(DbTransactionInput.transaction_id == + inp.transaction_id).first(): + u.spent = False session.query(DbTransactionInput).filter_by(transaction_id=tx.id).delete() qr = session.query(DbKey).filter_by(latest_txid=txid) qr.update({DbKey.latest_txid: None, DbKey.used: False}) @@ -1051,6 +1054,80 @@ def delete(self): self.hdwallet._commit() return res + def bumpfee(self, fee=0, extra_fee=0, broadcast=False): + """ + Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be + increased to speed up inclusion on the blockchain. + + If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code + below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings. + + The extra fee will be deducted from change output. This method fails if there are not enough change outputs + to cover fees. + + If this transaction does not have enough inputs to cover extra fee, an extra wallet utxo will be aaded to + inputs if available. + + Previous broadcasted transaction will be removed from wallet with this replace-by-fee transaction and wallet + information updated. + + :param fee: New fee for this transaction + :type fee: int + :param extra_fee: Extra fee to add to current transaction fee + :type extra_fee: int + :param broadcast: Increase fee and directly broadcast transaction to the network + :type broadcast: bool + + :return None: + """ + fees_not_provided = not (fee or extra_fee) + old_txid = self.txid + try: + super(WalletTransaction, self).bumpfee(fee, extra_fee) + except TransactionError as e: + if str(e) != "Not enough unspent outputs to bump transaction fee": + raise TransactionError(str(e)) + else: + # Add extra input to cover fee + if fees_not_provided: + extra_fee = int(self.fee * (0.03 ** BUMPFEE_DEFAULT_MULTIPLIER) + + (self.vsize * BUMPFEE_DEFAULT_MULTIPLIER)) + new_inp = self.add_input_from_wallet(amount_min=extra_fee) + # Add value of extra input to change output + change_outputs = [o for o in self.outputs if o.change] + if change_outputs: + change_outputs[0].value += self.inputs[new_inp].value + else: + self.add_output(self.inputs[new_inp].value, self.hdwallet.get_key().address, change=True) + if fees_not_provided: + extra_fee += 25 * BUMPFEE_DEFAULT_MULTIPLIER + super(WalletTransaction, self).bumpfee(fee, extra_fee) + # remove previous transaction and update wallet + if self.pushed: + self.hdwallet.transaction_delete(old_txid) + if broadcast: + self.send() + + def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=0): + if not amount_min: + amount_min = self.network.dust_amount + utxos = self.hdwallet.utxos(self.account_id, network=self.network.name, min_confirms=min_confirms, + key_id=key_id) + current_inputs = [(i.prev_txid.hex(), i.output_n_int) for i in self.inputs] + unused_inputs = [u for u in utxos + if (u['txid'], u['output_n']) not in current_inputs and u['value'] >= amount_min] + if not unused_inputs: + raise TransactionError("Not enough unspent inputs found for transaction %s" % + self.txid) + # take first input + utxo = unused_inputs[0] + inp_keys, key = self.hdwallet._objects_by_key_id(utxo['key_id']) + unlock_script_type = get_unlocking_script_type(utxo['script_type'], self.witness_type, + multisig=self.hdwallet.multisig) + return self.add_input(utxo['txid'], utxo['output_n'], keys=inp_keys, script_type=unlock_script_type, + sigs_required=self.hdwallet.multisig_n_required, sort=self.hdwallet.sort_keys, + compressed=key.compressed, value=utxo['value'], address=utxo['address'], + locking_script=utxo['script'], witness_type=key.witness_type) class Wallet(object): """ @@ -3559,6 +3636,12 @@ def transaction_spent(self, txid, output_n): def update_transactions_from_block(block, network=None): pass + def transaction_delete(self, txid): + wt = self.transaction(txid) + if wt: + wt.delete() + else: + raise WalletError("Transaction %s not found in this wallet" % txid) def _objects_by_key_id(self, key_id): key = self.session.query(DbKey).filter_by(id=key_id).scalar() diff --git a/tests/test_transactions.py b/tests/test_transactions.py index d2ad82cf..fb8f6c4f 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1351,7 +1351,7 @@ def test_transaction_bumpfee(self): self.assertEqual(t.fee, 3333) txid_before = t.txid t.bumpfee() - self.assertEqual(t.fee, 4068) + self.assertEqual(t.fee, 4598) self.assertNotEqual(t.txid, txid_before) self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index a010b488..6540c6d1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2365,8 +2365,8 @@ def test_wallet_anti_fee_sniping(self): w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + # FIXME: Bitaps and Bitgo return incorrect blockcount for testnet block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount() - # Bitaps and Bitgo return incorrect blockcount for testnet, so set delta self.assertAlmostEqual(t.locktime, block_height+1, delta=3) w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) @@ -2834,4 +2834,65 @@ def test_wallet_mixed_witness_types_passphrase(self): expected_addresslist = \ ['39h96ozh8F8W2sVrc2EhEbFwwdRoLHJAfB', '3LdJC6MSmFqKrn2WrxRfhd8DYkYYr8FNDr', 'MTSW4eC7xJiyp4YjwGZqpGmubsdm28Cdvc', 'bc1qgw8rg0057q9fmupx7ru6vtkxzy03gexc9ljycagj8z3hpzdfg7usvu56dp'] - self.assertListEqual(sorted(w.addresslist()), expected_addresslist) \ No newline at end of file + self.assertListEqual(sorted(w.addresslist()), expected_addresslist) + + def test_wallet_transactions_add_input_from_wallet(self): + w = wallet_create_or_open('add_input_from_wallet_test', network='bitcoinlib_test', + db_uri=self.database_uri) + w.utxos_update() + t = WalletTransaction(w, network='bitcoinlib_test') + t.add_input_from_wallet() + t.add_output(100000, 'blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag') + t.add_output(99000000, 'blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', change=True) + t.sign_and_update() + self.assertTrue(t.verified) + self.assertEqual(t.fee, 900000) + self.assertEqual(len(t.outputs), 2) + self.assertEqual(t.inputs[0].witness_type, 'segwit') + + def test_wallet_transactions_bumpfee(self): + pkm = 'elephant dust deer company win final' + wallet_delete_if_exists('bumpfeetest01', force=True) + w = wallet_create_or_open('bumpfeetest01', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri) + w.utxos_update() + t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 99900000, fee=100000, + broadcast=True) + self.assertEqual(w.balance(), 100000000) + self.assertEqual(len(t.inputs), 1) + self.assertEqual(len(t.outputs), 1) + self.assertEqual(w.utxos()[0]['txid'], 'ea7bd8fe970ca6430cebbbf914ce2feeb369c3ae95edc117725dbe21519ccdab') + t.bumpfee(broadcast=True) + self.assertEqual(len(t.inputs), 2) + self.assertEqual(len(t.outputs), 2) + self.assertEqual(w.balance(), 99999325) + + w = wallet_create_or_open('bumpfeetest02', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri) + w.utxos_update() + t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 50000000, fee=100000, + broadcast=True) + self.assertEqual(w.balance(), 149900000) + self.assertEqual(len(t.inputs), 1) + self.assertEqual(len(t.outputs), 2) + t.bumpfee(fee=200000, broadcast=True) + self.assertEqual(len(t.inputs), 1) + self.assertEqual(len(t.outputs), 2) + self.assertEqual(w.balance(), 149800000) + + w = wallet_create_or_open('bumpfeetest03', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri) + w.utxos_update() + t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 99900000, fee=50000, + broadcast=True) + self.assertEqual(w.balance(), 100050000) + self.assertEqual(len(t.inputs), 1) + self.assertEqual(len(t.outputs), 2) + t.bumpfee(extra_fee=50000, broadcast=True) + self.assertEqual(len(t.inputs), 1) + self.assertEqual(len(t.outputs), 1) + self.assertEqual(w.balance(), 100000000) + self.assertEqual(len(w.utxos()), 1) + + w = wallet_create_or_open('bumpfeetest04', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri) + w.utxos_update() + t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 199900000, fee=100000, + broadcast=True) + self.assertRaisesRegex(TransactionError, "Not enough unspent inputs found for transaction", t.bumpfee) From 702acbe64beb98eb7445dd71ef26a26ae3e9900d Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 8 May 2024 23:02:26 +0200 Subject: [PATCH 184/207] Add method to remove unconfirmed transactions --- bitcoinlib/wallets.py | 46 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index d758d655..f525bac4 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1108,7 +1108,22 @@ def bumpfee(self, fee=0, extra_fee=0, broadcast=False): if broadcast: self.send() - def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=0): + def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=1): + """ + Add a new input from an utxo of this wallet. If not key_id is specified it adds the first input it finds with + the minimum amount and minimum confirms specified. + + WARNING: Change output and fees are not updated, so you risk overpaying fees! + + :param amount_min: Minimum value of new input + :type amount_min: int + :param key_id: Filter by this key id + :type key_id: int + :param min_confirms: Minimum confirms of utxo + :type min_confirms: int + + :return int: Index number of new input + """ if not amount_min: amount_min = self.network.dust_amount utxos = self.hdwallet.utxos(self.account_id, network=self.network.name, min_confirms=min_confirms, @@ -1127,7 +1142,7 @@ def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=0): return self.add_input(utxo['txid'], utxo['output_n'], keys=inp_keys, script_type=unlock_script_type, sigs_required=self.hdwallet.multisig_n_required, sort=self.hdwallet.sort_keys, compressed=key.compressed, value=utxo['value'], address=utxo['address'], - locking_script=utxo['script'], witness_type=key.witness_type) + witness_type=key.witness_type) class Wallet(object): """ @@ -2815,15 +2830,13 @@ def witness_types(self, account_id=None, network=None): :return list of str: """ - # network, account_id, _ = self._get_account_defaults(network, account_id) qr = self.session.query(DbKey.witness_type).filter_by(wallet_id=self.wallet_id) if network is not None: qr = qr.filter(DbKey.network_name == network) if account_id is not None: qr = qr.filter(DbKey.account_id == account_id) qr = qr.group_by(DbKey.witness_type).all() - return [x[0] for x in qr] - + return [x[0] for x in qr] if qr else [self.witness_type] def networks(self, as_dict=False): """ @@ -3637,12 +3650,35 @@ def update_transactions_from_block(block, network=None): pass def transaction_delete(self, txid): + """ + Remove specified transaction from wallet and update related transactions. + + :param txid: Transaction ID of transaction to remove + :type txid: str + + :return: + """ wt = self.transaction(txid) if wt: wt.delete() else: raise WalletError("Transaction %s not found in this wallet" % txid) + def transactions_remove_unconfirmed(self, account_id=None, network=None): + """ + Removes all unconfirmed transactions from this wallet and updates related transactions / utxos. + + :param account_id: Filter by Account ID. Leave empty for default account_id + :type account_id: int, None + :param network: Filter by network name. Leave empty for default network + :type network: str, None + :return: + """ + txs = self.transactions(account_id=account_id, network=network) + for tx in txs: + if not tx.confirmations: + self.transaction_delete(tx.txid) + def _objects_by_key_id(self, key_id): key = self.session.query(DbKey).filter_by(id=key_id).scalar() if not key: From fc64228552cee4e97acc000b9d1e75d510899995 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 9 May 2024 17:27:22 +0200 Subject: [PATCH 185/207] Allow to specify age when deleting txs --- bitcoinlib/wallets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index f525bac4..8ae7657f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -23,6 +23,7 @@ from operator import itemgetter import numpy as np import pickle +from datetime import timedelta from bitcoinlib.db import * from bitcoinlib.encoding import * from bitcoinlib.keys import Address, BKeyError, HDKey, check_network_and_key, path_expand @@ -3664,10 +3665,12 @@ def transaction_delete(self, txid): else: raise WalletError("Transaction %s not found in this wallet" % txid) - def transactions_remove_unconfirmed(self, account_id=None, network=None): + def transactions_remove_unconfirmed(self, hours_old=0, account_id=None, network=None): """ Removes all unconfirmed transactions from this wallet and updates related transactions / utxos. + :param hours_old: Only delete unconfirmed transaction which are x hours old. You can also use decimals, ie: 0.5 for half an hour + :type hours_old: int, float :param account_id: Filter by Account ID. Leave empty for default account_id :type account_id: int, None :param network: Filter by network name. Leave empty for default network @@ -3675,8 +3678,9 @@ def transactions_remove_unconfirmed(self, account_id=None, network=None): :return: """ txs = self.transactions(account_id=account_id, network=network) + t = datetime.now() - timedelta(hours=hours_old) for tx in txs: - if not tx.confirmations: + if not tx.confirmations and tx.date < t: self.transaction_delete(tx.txid) def _objects_by_key_id(self, key_id): From ad6e32ce6d597af6ff3a7ec8fa0f8eb723bc5e38 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 09:14:05 +0200 Subject: [PATCH 186/207] Fix: Compare correct UTC times --- bitcoinlib/wallets.py | 4 ++-- tests/test_wallets.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 8ae7657f..6eabd9ff 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3678,9 +3678,9 @@ def transactions_remove_unconfirmed(self, hours_old=0, account_id=None, network= :return: """ txs = self.transactions(account_id=account_id, network=network) - t = datetime.now() - timedelta(hours=hours_old) + td = datetime.utcnow() - timedelta(hours=hours_old) for tx in txs: - if not tx.confirmations and tx.date < t: + if not tx.confirmations and tx.date < td: self.transaction_delete(tx.txid) def _objects_by_key_id(self, key_id): diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 6540c6d1..7b37f5a5 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2896,3 +2896,53 @@ def test_wallet_transactions_bumpfee(self): t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 199900000, fee=100000, broadcast=True) self.assertRaisesRegex(TransactionError, "Not enough unspent inputs found for transaction", t.bumpfee) + + def test_wallet_transaction_remove_unconfirmed(self): + pkm = 'monitor orphan turtle stage december special' + w = wallet_create_or_open("wallet_remove_old_unconfirmed_transactions", keys=pkm, + network='bitcoinlib_test', db_uri=self.database_uri) + w.get_keys(number_of_keys=4) + utxos = [ + { + "address": "blt1q4dugy6d7qz7226mk6ast3nz23z7ctd80mymle3", + "script": "", + "confirmations": 2, + "output_n": 1, + "txid": "e6192f6dafa689ac8889b466d2dd3eb2bb55b76c7305b4a2a6a31de6c9991aeb", + "value": 1829810 + }, + { + "address": "blt1q82l3c2d37yjxe0r9a7qn9v7c9y7hnaxp398kc0", + "script": "", + "confirmations": 0, + "output_n": 0, + "txid": "5891c85595193d0565fe418d5c5192c1297eafbef36c28bcab2ac3341ee68e71", + "value": 2389180 + }, + { + "address": "blt1qdtez8t797m74ar8wuvedw50jmycefwstfk8ulz", + "script": "", + "confirmations": 100, + "output_n": 0, + "txid": "7e87a63a0233615a5719a782a0b1c85de521151d8648e7d7244155a2caf7dd47", + "value": 99389180 + }, + { + "address": "blt1qdtez8t797m74ar8wuvedw50jmycefwstfk8ulz", + "script": "", + "confirmations": 100, + "output_n": 0, + "txid": "a4ef4aef09839a681419b80d5b6228b0089af39a4483896c9ac106192ac1ec34", + "value": 838180 + }, + ] + w.utxos_update(utxos=utxos) + w.send_to('blt1qvtaw9m9ut96ykt2n2kdra8jpv3m5z2s8krqwsv', 50000, broadcast=True) + self.assertEqual(len(w.utxos()), 5) + self.assertEqual(w.balance(), 104441651) + w.transactions_remove_unconfirmed(1) + self.assertEqual(len(w.utxos()), 5) + self.assertEqual(w.balance(), 104441651) + w.transactions_remove_unconfirmed(0) + self.assertEqual(len(w.utxos()), 3) + self.assertEqual(w.balance(), 102057170) From 4c3ed2a1918d729daf6e85d9a2b82f0a13d546a6 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 11:46:56 +0200 Subject: [PATCH 187/207] Try to fix session problems --- bitcoinlib/db.py | 2 +- bitcoinlib/wallets.py | 5 ++- tests/test_wallets.py | 88 ++++++++++++++++++++++--------------------- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index d84a0e7c..6c863b02 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -71,7 +71,7 @@ def __init__(self, db_uri=None, password=None): db_uri = self.o._replace(scheme="postgresql+psycopg").geturl() self.engine = create_engine(db_uri, isolation_level='READ UNCOMMITTED') - Session = sessionmaker(bind=self.engine) + Session = sessionmaker(bind=self.engine, expire_on_commit=False) Base.metadata.create_all(self.engine) self._import_config_data(Session) self.session = Session() diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 6eabd9ff..2129a94f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -1236,7 +1236,7 @@ def _commit(self): self.session.commit() except Exception: self.session.rollback() - raise + raise WalletError("Could not commit to database, rollback performed!") @classmethod def create(cls, name, keys=None, owner='', network=None, account_id=0, purpose=0, scheme='bip32', @@ -1673,6 +1673,7 @@ def name(self, value): @property def session(self): if not self._session: + logger.info("Opening database session %s" % self.db_uri) dbinit = Db(db_uri=self.db_uri, password=self._db_password) self._session = dbinit.session self._engine = dbinit.engine @@ -1737,7 +1738,6 @@ def import_master_key(self, hdkey, name='Masterkey (imported)'): if kp and kp[0] == 'M': kp = self.key_path[:self.depth_public_master+1] + kp[1:] self.key_for_path(kp, recreate=True) - self._commit() return self.main_key @@ -3684,6 +3684,7 @@ def transactions_remove_unconfirmed(self, hours_old=0, account_id=None, network= self.transaction_delete(tx.txid) def _objects_by_key_id(self, key_id): + self.session.expire_all() key = self.session.query(DbKey).filter_by(id=key_id).scalar() if not key: raise WalletError("Key '%s' not found in this wallet" % key_id) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 7b37f5a5..31b613e0 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2012,32 +2012,33 @@ def test_wallet_transaction_sign_with_wif(self): self.assertIsNone(t.send()) self.assertTrue(t.pushed) - def test_wallet_transaction_restore_saved_tx(self): - w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - db_uri=self.database_uri) - wk = w.get_key() - if not USE_FASTECDSA: - self.skipTest("Need fastecdsa module with deterministic txid's to run this test") - utxos = [{ - 'address': wk.address, - 'script': '', - 'confirmations': 10, - 'output_n': 1, - 'txid': '9f5d4004c7cc5a31a735bddea6ff517e52f1cd700df208d2c39ddc536670f1fe', - 'value': 1956783097 - }] - w.utxos_update(utxos=utxos) - to = w.get_key_change() - t = w.sweep(to.address) - tx_id = t.store() - wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) - w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - db_uri=self.database_uri) - w.get_key() - w.utxos_update(utxos=utxos) - to = w.get_key_change() - t = w.sweep(to.address) - self.assertEqual(t.store(), tx_id) + #FIXME + # def test_wallet_transaction_restore_saved_tx(self): + # w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', + # db_uri=self.database_uri) + # wk = w.get_key() + # if not USE_FASTECDSA: + # self.skipTest("Need fastecdsa module with deterministic txid's to run this test") + # utxos = [{ + # 'address': wk.address, + # 'script': '', + # 'confirmations': 10, + # 'output_n': 1, + # 'txid': '9f5d4004c7cc5a31a735bddea6ff517e52f1cd700df208d2c39ddc536670f1fe', + # 'value': 1956783097 + # }] + # w.utxos_update(utxos=utxos) + # to = w.get_key_change() + # t = w.sweep(to.address) + # tx_id = t.store() + # wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) + # w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', + # db_uri=self.database_uri) + # w.get_key() + # w.utxos_update(utxos=utxos) + # to = w.get_key_change() + # t = w.sweep(to.address) + # self.assertEqual(t.store(), tx_id) def test_wallet_transaction_send_keyid(self): w = Wallet.create('wallet_send_key_id', witness_type='segwit', network='bitcoinlib_test', @@ -2361,23 +2362,24 @@ def test_wallet_transaction_replace_by_fee(self): self.assertTrue(t2.replace_by_fee) self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) - def test_wallet_anti_fee_sniping(self): - w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) - w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - # FIXME: Bitaps and Bitgo return incorrect blockcount for testnet - block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount() - self.assertAlmostEqual(t.locktime, block_height+1, delta=3) - - w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) - w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) - self.assertEqual(t.locktime, 1901070183) - - w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) - w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) - t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - self.assertEqual(t.locktime, 0) + # fiXME + # def test_wallet_anti_fee_sniping(self): + # w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) + # w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) + # t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + # # FIXME: Bitaps and Bitgo return incorrect blockcount for testnet + # block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount() + # self.assertAlmostEqual(t.locktime, block_height+1, delta=3) + # + # w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) + # w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) + # t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) + # self.assertEqual(t.locktime, 1901070183) + # + # w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) + # w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) + # t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + # self.assertEqual(t.locktime, 0) @classmethod def tearDownClass(cls): From 1393e9736e9d979603492802f35b1c5567260fe1 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 19:28:28 +0200 Subject: [PATCH 188/207] Remove bitgo and bitaps providers for testnet, reenable blockcypher --- bitcoinlib/data/providers.json | 33 +++------ bitcoinlib/services/bitaps.py | 2 + bitcoinlib/services/bitgo.py | 2 + bitcoinlib/services/blockcypher.py | 65 +++++++++--------- tests/test_wallets.py | 104 ++++++++++++++--------------- 5 files changed, 97 insertions(+), 109 deletions(-) diff --git a/bitcoinlib/data/providers.json b/bitcoinlib/data/providers.json index 40190a30..d7fd2dc2 100644 --- a/bitcoinlib/data/providers.json +++ b/bitcoinlib/data/providers.json @@ -32,17 +32,6 @@ "denominator": 1, "network_overrides": null }, - "bitgo.testnet": { - "provider": "bitgo", - "network": "testnet", - "client_class": "BitGoClient", - "provider_coin_id": "", - "url": "https://test.bitgo.com/api/v1/", - "api_key": "", - "priority": 10, - "denominator": 1, - "network_overrides": null - }, "blockcypher.litecoin": { "provider": "blockcypher", "network": "litecoin", @@ -142,17 +131,6 @@ "denominator": 100000000, "network_overrides": null }, - "bitaps.testnet": { - "provider": "bitaps", - "network": "testnet", - "client_class": "BitapsClient", - "provider_coin_id": "", - "url": "https://api.bitaps.com/btc/testnet/v1/", - "api_key": "", - "priority": 10, - "denominator": 100000000, - "network_overrides": null - }, "bitaps.litecoin": { "provider": "bitaps", "network": "litecoin", @@ -329,6 +307,17 @@ "denominator": 1, "network_overrides": null }, + "blockcypher.testnet": { + "provider": "blockcypher", + "network": "testnet", + "client_class": "BlockCypher", + "provider_coin_id": "", + "url": "https://api.blockcypher.com/v1/btc/test3/", + "api_key": "", + "priority": 10, + "denominator": 1, + "network_overrides": null + }, "mempool": { "provider": "mempool", "network": "bitcoin", diff --git a/bitcoinlib/services/bitaps.py b/bitcoinlib/services/bitaps.py index a31c6aab..29ec78ef 100644 --- a/bitcoinlib/services/bitaps.py +++ b/bitcoinlib/services/bitaps.py @@ -172,6 +172,8 @@ def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS): # def estimatefee def blockcount(self): + if self.network == 'testnet': + raise ClientError('Providers return incorrect blockcount for testnet') return self.compose_request('block', 'last')['data']['height'] # def mempool(self, txid): diff --git a/bitcoinlib/services/bitgo.py b/bitcoinlib/services/bitgo.py index 1013b425..eab4c353 100644 --- a/bitcoinlib/services/bitgo.py +++ b/bitcoinlib/services/bitgo.py @@ -166,6 +166,8 @@ def estimatefee(self, blocks): return res['feePerKb'] def blockcount(self): + if self.network == 'testnet': + raise ClientError('Providers return incorrect blockcount for testnet') return self.compose_request('block', 'latest')['height'] # def mempool diff --git a/bitcoinlib/services/blockcypher.py b/bitcoinlib/services/blockcypher.py index 882ec7ad..f313e496 100644 --- a/bitcoinlib/services/blockcypher.py +++ b/bitcoinlib/services/blockcypher.py @@ -56,39 +56,38 @@ def getbalance(self, addresslist): balance += float(rec['final_balance']) return int(balance * self.units) - # Disabled: Invalid results for https://api.blockcypher.com/v1/ltc/main/addrs/LVqLipGhyQ1nWtPPc8Xp3zn6JxcU1Hi8eG?unspentOnly=1&limit=2000 - # def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): - # address = self._address_convert(address) - # res = self.compose_request('addrs', address.address, variables={'unspentOnly': 1, 'limit': 2000}) - # transactions = [] - # if not isinstance(res, list): - # res = [res] - # for a in res: - # txrefs = a.setdefault('txrefs', []) + a.get('unconfirmed_txrefs', []) - # if len(txrefs) > 500: - # _logger.warning("BlockCypher: Large number of transactions for address %s, " - # "Transaction list may be incomplete" % address) - # for tx in txrefs: - # if tx['tx_hash'] == after_txid: - # break - # tdate = None - # if 'confirmed' in tx: - # try: - # tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%SZ") - # except ValueError: - # tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%S.%fZ") - # transactions.append({ - # 'address': address.address_orig, - # 'txid': tx['tx_hash'], - # 'confirmations': tx['confirmations'], - # 'output_n': tx['tx_output_n'], - # 'index': 0, - # 'value': int(round(tx['value'] * self.units, 0)), - # 'script': '', - # 'block_height': None, - # 'date': tdate - # }) - # return transactions[::-1][:limit] + def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS): + address = self._address_convert(address) + res = self.compose_request('addrs', address.address, variables={'unspentOnly': 1, 'limit': 2000}) + transactions = [] + if not isinstance(res, list): + res = [res] + for a in res: + txrefs = a.setdefault('txrefs', []) + a.get('unconfirmed_txrefs', []) + if len(txrefs) > 500: + _logger.warning("BlockCypher: Large number of transactions for address %s, " + "Transaction list may be incomplete" % address) + for tx in txrefs: + if tx['tx_hash'] == after_txid: + break + tdate = None + if 'confirmed' in tx: + try: + tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%SZ") + except ValueError: + tdate = datetime.strptime(tx['confirmed'], "%Y-%m-%dT%H:%M:%S.%fZ") + transactions.append({ + 'address': address.address_orig, + 'txid': tx['tx_hash'], + 'confirmations': tx['confirmations'], + 'output_n': tx['tx_output_n'], + 'index': 0, + 'value': int(round(tx['value'] * self.units, 0)), + 'script': '', + 'block_height': None, + 'date': tdate + }) + return transactions[::-1][:limit] def gettransaction(self, txid): tx = self.compose_request('txs', txid, variables={'includeHex': 'true'}) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 31b613e0..1730acb6 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1408,7 +1408,7 @@ def test_wallet_multisig_info(self): HDKey(network='bitcoinlib_test')], network='bitcoinlib_test', cosigner_id=0, db_uri=self.database_uri) w.utxos_update() - w.info(detail=6) + self.assertIsNone(w.info(detail=6)) def test_wallets_multisig_missing_private_and_cosigner(self): k0 = 'xprv9s21ZrQH143K459uwGGCU3Wj3v1LFFJ42tgyTsNnr6p2BS6FZ9jQ7fmZMMnqsWSi2BBgpX3hFbR4ode8Jx58ibSNeaBLFQ68Xs3' \ @@ -1729,18 +1729,16 @@ def test_wallet_offline_create_transaction(self): del wlt def test_wallet_scan(self): - # TODO: Fix MySQL scan errors - if self.database_uri.startswith('mysql'): - self.skipTest('TODO: Fix MySQL scan errors') account_key = 'tpubDCmJWqxWch7LYDhSuE1jEJMbAkbkDm3DotWKZ69oZfNMzuw7U5DwEaTVZHGPzt5j9BJDoxqVkPHt2EpUF66FrZhpfq' \ 'ZY6DFj6x61Wwbrg8Q' wallet = wallet_create_or_open('scan-test', keys=account_key, network='testnet', db_uri=self.database_uri) - wallet.scan(scan_gap_limit=8) - self.assertEqual(len(wallet.keys()), 27) - self.assertEqual(wallet.balance(), 60500000) - self.assertEqual(len(wallet.transactions()), 4) - self.assertEqual(len(wallet.transactions(as_dict=True)), 4) + wallet.scan(scan_gap_limit=1) + self.assertEqual(len(wallet.keys()), 6) + self.assertEqual(wallet.balance(), 60000000) + self.assertEqual(len(wallet.transactions()), 3) + self.assertEqual(len(wallet.transactions(as_dict=True)), 3) + def test_wallet_scan_tx_order_same_block(self): # Check tx order in same block address = 'tb1qlh9x3jwhfqspp7u9w6l7zqxpmuvplzaczaele3' w = wallet_create_or_open('fix-multiple-tx-1-block', keys=address, db_uri=self.database_uri) @@ -2012,33 +2010,33 @@ def test_wallet_transaction_sign_with_wif(self): self.assertIsNone(t.send()) self.assertTrue(t.pushed) - #FIXME - # def test_wallet_transaction_restore_saved_tx(self): - # w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - # db_uri=self.database_uri) - # wk = w.get_key() - # if not USE_FASTECDSA: - # self.skipTest("Need fastecdsa module with deterministic txid's to run this test") - # utxos = [{ - # 'address': wk.address, - # 'script': '', - # 'confirmations': 10, - # 'output_n': 1, - # 'txid': '9f5d4004c7cc5a31a735bddea6ff517e52f1cd700df208d2c39ddc536670f1fe', - # 'value': 1956783097 - # }] - # w.utxos_update(utxos=utxos) - # to = w.get_key_change() - # t = w.sweep(to.address) - # tx_id = t.store() - # wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) - # w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', - # db_uri=self.database_uri) - # w.get_key() - # w.utxos_update(utxos=utxos) - # to = w.get_key_change() - # t = w.sweep(to.address) - # self.assertEqual(t.store(), tx_id) + def test_wallet_transaction_restore_saved_tx(self): + w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', + db_uri=self.database_uri) + wk = w.get_key() + if not USE_FASTECDSA: + self.skipTest("Need fastecdsa module with deterministic txid's to run this test") + utxos = [{ + 'address': wk.address, + 'script': '', + 'confirmations': 10, + 'output_n': 1, + 'txid': '9f5d4004c7cc5a31a735bddea6ff517e52f1cd700df208d2c39ddc536670f1fe', + 'value': 1956783097 + }] + w.utxos_update(utxos=utxos) + to = w.get_key_change() + t = w.sweep(to.address) + tx_id = t.store() + del w + wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) + w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', + db_uri=self.database_uri) + w.get_key() + w.utxos_update(utxos=utxos) + to = w.get_key_change() + t = w.sweep(to.address) + self.assertEqual(t.store(), tx_id) def test_wallet_transaction_send_keyid(self): w = Wallet.create('wallet_send_key_id', witness_type='segwit', network='bitcoinlib_test', @@ -2362,24 +2360,22 @@ def test_wallet_transaction_replace_by_fee(self): self.assertTrue(t2.replace_by_fee) self.assertEqual(t2.inputs[0].sequence, SEQUENCE_REPLACE_BY_FEE) - # fiXME - # def test_wallet_anti_fee_sniping(self): - # w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) - # w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) - # t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - # # FIXME: Bitaps and Bitgo return incorrect blockcount for testnet - # block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount() - # self.assertAlmostEqual(t.locktime, block_height+1, delta=3) - # - # w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) - # w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) - # t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) - # self.assertEqual(t.locktime, 1901070183) - # - # w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) - # w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) - # t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) - # self.assertEqual(t.locktime, 0) + def test_wallet_anti_fee_sniping(self): + w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri) + w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + block_height = Service(network='testnet', cache_uri='').blockcount() + self.assertAlmostEqual(t.locktime, block_height+1, delta=3) + + w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True) + w2.utxo_add(w2.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w2.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456, locktime=1901070183) + self.assertEqual(t.locktime, 1901070183) + + w3 = wallet_create_or_open('antifeesnipingtestwallet3', network='testnet', anti_fee_sniping=False) + w3.utxo_add(w3.get_key().address, 1234567, os.urandom(32).hex(), 1) + t = w3.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456) + self.assertEqual(t.locktime, 0) @classmethod def tearDownClass(cls): From fc3a0abcdaef7496322a21ab4c08ecfc8e121c75 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 19:56:31 +0200 Subject: [PATCH 189/207] Fix issue in unittest if provider blockcount is not working --- tests/test_services.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_services.py b/tests/test_services.py index 4f9b00cf..62a68997 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -795,7 +795,10 @@ def test_service_transaction_unconfirmed(self): def test_service_exlude_providers(self): srv = ServiceTest(network='testnet', cache_uri='') providers = [srv.providers[pi]['provider'] for pi in srv.providers] - srv2 = ServiceTest(network='testnet', exclude_providers=providers[1:], cache_uri='') + try: + srv2 = ServiceTest(network='testnet', exclude_providers=providers[1:], cache_uri='') + except ServiceError: + self.skipTest("Blockcount for provider %s was not successful" % providers[0]) self.assertEqual(len(srv2.providers), 1) From f165a238520752da02f11da6757ea81a4e899f93 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 20:35:05 +0200 Subject: [PATCH 190/207] Add some debug info to failing unittest --- tests/test_wallets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1730acb6..af91b6fe 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1817,6 +1817,7 @@ def test_wallet_add_dust_to_fee(self): to_key = wlt.get_key() wlt.utxos_update() t = wlt.send_to(to_key.address, 99992000, broadcast=True) + t.info() self.assertEqual(t.fee, 8000) del wlt @@ -2011,6 +2012,8 @@ def test_wallet_transaction_sign_with_wif(self): self.assertTrue(t.pushed) def test_wallet_transaction_restore_saved_tx(self): + if os.getenv('UNITTEST_DATABASE') == 'mysql': # fixme + self.skipTest() w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', db_uri=self.database_uri) wk = w.get_key() From d15dc3874d4069d1860ee9dc5fd10ab7162d6e89 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 20:57:06 +0200 Subject: [PATCH 191/207] Fix dust to fee unittest for windows --- tests/test_wallets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index af91b6fe..8858ad6a 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1816,8 +1816,7 @@ def test_wallet_add_dust_to_fee(self): db_uri=self.database_uri) to_key = wlt.get_key() wlt.utxos_update() - t = wlt.send_to(to_key.address, 99992000, broadcast=True) - t.info() + t = wlt.send_to(to_key.address, 99992000, fee=7500, broadcast=True) self.assertEqual(t.fee, 8000) del wlt @@ -2013,7 +2012,7 @@ def test_wallet_transaction_sign_with_wif(self): def test_wallet_transaction_restore_saved_tx(self): if os.getenv('UNITTEST_DATABASE') == 'mysql': # fixme - self.skipTest() + self.skipTest("Unittest not working for mysql at the moment") w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', db_uri=self.database_uri) wk = w.get_key() From 44b0ba37a9ac902f0a9ec9381740d20bd9c42d65 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 14 May 2024 21:46:03 +0200 Subject: [PATCH 192/207] Fix restore saved tx unittest --- tests/test_wallets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 8858ad6a..c9fecd8c 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -2011,13 +2011,12 @@ def test_wallet_transaction_sign_with_wif(self): self.assertTrue(t.pushed) def test_wallet_transaction_restore_saved_tx(self): - if os.getenv('UNITTEST_DATABASE') == 'mysql': # fixme - self.skipTest("Unittest not working for mysql at the moment") + if not USE_FASTECDSA: + self.skipTest("Need fastecdsa module with deterministic txid's to run this test") + w = wallet_create_or_open('test_wallet_transaction_restore', network='bitcoinlib_test', db_uri=self.database_uri) wk = w.get_key() - if not USE_FASTECDSA: - self.skipTest("Need fastecdsa module with deterministic txid's to run this test") utxos = [{ 'address': wk.address, 'script': '', @@ -2028,7 +2027,7 @@ def test_wallet_transaction_restore_saved_tx(self): }] w.utxos_update(utxos=utxos) to = w.get_key_change() - t = w.sweep(to.address) + t = w.sweep(to.address, fee=10000) tx_id = t.store() del w wallet_empty('test_wallet_transaction_restore', db_uri=self.database_uri) @@ -2037,8 +2036,9 @@ def test_wallet_transaction_restore_saved_tx(self): w.get_key() w.utxos_update(utxos=utxos) to = w.get_key_change() - t = w.sweep(to.address) - self.assertEqual(t.store(), tx_id) + t2 = w.sweep(to.address, fee=10000) + self.assertEqual(t.txid, t2.txid) + self.assertEqual(t2.store(), tx_id) def test_wallet_transaction_send_keyid(self): w = Wallet.create('wallet_send_key_id', witness_type='segwit', network='bitcoinlib_test', From 48e9b2f77c5c362ea16c453d1f39f851276fe6fa Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 15 May 2024 22:03:01 +0200 Subject: [PATCH 193/207] Fix some database errors --- bitcoinlib/db.py | 4 ++-- bitcoinlib/wallets.py | 9 ++++++++- tests/test_wallets.py | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bitcoinlib/db.py b/bitcoinlib/db.py index 6c863b02..99256f69 100644 --- a/bitcoinlib/db.py +++ b/bitcoinlib/db.py @@ -443,7 +443,7 @@ class DbTransactionInput(Base): transaction = relationship("DbTransaction", back_populates='inputs', doc="Related DbTransaction object") index_n = Column(Integer, primary_key=True, doc="Index number of transaction input") key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this input") - key = relationship("DbKey", back_populates="transaction_inputs", doc="Related DbKey object") + key = relationship("DbKey", doc="Related DbKey object") address = Column(String(255), doc="Address string of input, used if no key is associated. " "An cryptocurrency address is a hash of the public key or a redeemscript") @@ -484,7 +484,7 @@ class DbTransactionOutput(Base): doc="Link to transaction object") output_n = Column(BigInteger, primary_key=True, doc="Sequence number of transaction output") key_id = Column(Integer, ForeignKey('keys.id'), index=True, doc="ID of key used in this transaction output") - key = relationship("DbKey", back_populates="transaction_outputs", doc="List of DbKey object used in this output") + key = relationship("DbKey", doc="List of DbKey object used in this output") address = Column(String(255), doc="Address string of output, used if no key is associated. " "An cryptocurrency address is a hash of the public key or a redeemscript") diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 2129a94f..4de9f11e 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -3748,7 +3748,14 @@ def select_inputs(self, amount, variance=None, input_key_id=None, account_id=Non utxo_query = utxo_query.filter(DbKey.id.in_(input_key_id)) if skip_dust_amounts: utxo_query = utxo_query.filter(DbTransactionOutput.value >= dust_amount) - utxos = utxo_query.order_by(DbTransaction.confirmations.desc()).all() + utxo_query = utxo_query.order_by(DbTransaction.confirmations.desc()) + try: + utxos = utxo_query.all() + except Exception as e: + self.session.close() + logger.warning("Error when querying database, retry: %s" % str(e)) + utxos = utxo_query.all() + if not utxos: raise WalletError("Create transaction: No unspent transaction outputs found or no key available for UTXO's") diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c9fecd8c..daf30106 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -19,6 +19,7 @@ # import unittest +import time from random import shuffle try: @@ -2943,6 +2944,7 @@ def test_wallet_transaction_remove_unconfirmed(self): w.transactions_remove_unconfirmed(1) self.assertEqual(len(w.utxos()), 5) self.assertEqual(w.balance(), 104441651) + time.sleep(3) w.transactions_remove_unconfirmed(0) self.assertEqual(len(w.utxos()), 3) self.assertEqual(w.balance(), 102057170) From 7b3bf2a063a5776c88d6e7eb9cbf5f3503bd40e6 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 16 May 2024 07:13:16 +0200 Subject: [PATCH 194/207] Small update WalletKey docstring --- bitcoinlib/wallets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 4de9f11e..71704efd 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -546,14 +546,15 @@ def key(self): """ Get HDKey object for current WalletKey - :return HDKey: + :return HDKey, list of HDKey: """ self._hdkey_object = None if self.key_type == 'multisig': self._hdkey_object = [] for kc in self._dbkey.multisig_children: - self._hdkey_object.append(HDKey.from_wif(kc.child_key.wif, network=kc.child_key.network_name, compressed=self.compressed)) + self._hdkey_object.append(HDKey.from_wif(kc.child_key.wif, network=kc.child_key.network_name, + compressed=self.compressed)) if self._hdkey_object is None and self.wif: self._hdkey_object = HDKey.from_wif(self.wif, network=self.network_name, compressed=self.compressed) return self._hdkey_object From 46fe2d8fe42a146861a6a7c2f3dbbae018f0fe2f Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 16 May 2024 22:50:55 +0200 Subject: [PATCH 195/207] Add instructions for a readonly bitcoinlib wallet from bitcoin core --- bitcoinlib/services/services.py | 2 ++ examples/wallet_bitcoind_connected_wallets.py | 2 +- .../wallet_bitcoind_connected_wallets2.py | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/bitcoinlib/services/services.py b/bitcoinlib/services/services.py index 7c476f97..522bab08 100644 --- a/bitcoinlib/services/services.py +++ b/bitcoinlib/services/services.py @@ -78,6 +78,8 @@ def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, pr :type exclude_providers: list of str :param strict: Strict checks of valid signatures, scripts and transactions. Normally use strict=True for wallets, transaction validations etcetera. For blockchain parsing strict=False should be used, but be sure to check warnings in the log file. Default is True. :type strict: bool + :param wallet_name: Name of wallet if connecting to bitcoin node + :type wallet_name: str """ diff --git a/examples/wallet_bitcoind_connected_wallets.py b/examples/wallet_bitcoind_connected_wallets.py index f17b479b..7acb2116 100644 --- a/examples/wallet_bitcoind_connected_wallets.py +++ b/examples/wallet_bitcoind_connected_wallets.py @@ -5,7 +5,7 @@ # EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib # # Method 1 - Create wallet in Bitcoin Core and use the same wallet in Bitcoinlib using the bitcoin node to -# receive and send bitcoin transactions. +# receive and send bitcoin transactions. Only works for legacy wallets. # # © 2024 April - 1200 Web Development # diff --git a/examples/wallet_bitcoind_connected_wallets2.py b/examples/wallet_bitcoind_connected_wallets2.py index f37c60ee..8151df18 100644 --- a/examples/wallet_bitcoind_connected_wallets2.py +++ b/examples/wallet_bitcoind_connected_wallets2.py @@ -4,9 +4,9 @@ # # EXAMPLES - Using Bitcoin Core wallets with Bitcoinlib # -# Method 2 - ... +# Method 2 - Create wallet in Bitcoin Core, export public keys to bitcoinlib and easily manage wallet from bitcoinlib. # -# © 2024 April - 1200 Web Development +# © 2024 May - 1200 Web Development # from bitcoinlib.wallets import * @@ -16,14 +16,15 @@ # Settings and Initialization # -pkwif = 'cTAyLb37Sr4XQPzWCiwihJxdFpkLKeJBFeSnd5hwNiW8aqrbsZCd' +# Create wallet in Bitcoin Core and export descriptors +# $ bitcoin-cli createwallet wallet_bitcoincore2 +# $ bitcoin-cli -rpcwallet=wallet_bitcoincore2 listdescriptors -w = wallet_create_or_open("wallet_bitcoincore2", keys=pkwif, network='testnet', witness_type='segwit', - key_path=KEY_PATH_BITCOINCORE) +# Now copy the descriptor of the public master key, which looks like: wpkh([.../84h/1h/0h] +pkwif = 'tpubDDuQM8y9z4VQW5FS13BXGMxUwkUKEXc8KG5xzzbe6UsssrJDKJEygqbgMATnn6ZDwLXQ5PQipH989qWRTzFhPPZMiHxYYrG14X34vc24pD6' + +# You can create the wallet and manage it from bitcoinlib +w = wallet_create_or_open("wallet_bitcoincore2", keys=pkwif, witness_type='segwit') w.providers=['bitcoind'] -w.get_key() w.scan(scan_gap_limit=1) w.info() - -# TODO -# FIXME From 0d5b039b6560045cc040d0ee27734068c992439e Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 18 May 2024 19:16:21 +0200 Subject: [PATCH 196/207] Remove confusing script_code attribute from Input --- bitcoinlib/transactions.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index cec139fc..ffca4cea 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -271,7 +271,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.witnesses.append(witness) elif witnesses: self.witnesses = [bytes.fromhex(w) if isinstance(w, str) else w for w in witnesses] - self.script_code = b'' # If unlocking script is specified extract keys, signatures, type from script if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys): @@ -411,8 +410,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): self.public_hash = self.keys[0].hash160 if not self.keys and not self.public_hash: return - self.script_code = b'\x76\xa9\x14' + self.public_hash + b'\x88\xac' - self.locking_script = self.script_code + self.locking_script = b'\x76\xa9\x14' + self.public_hash + b'\x88\xac' addr_data = self.public_hash if self.signatures and self.keys: self.witnesses = [self.signatures[0].as_der_encoded() if hash_type else b'', self.keys[0].public_byte] @@ -453,25 +451,22 @@ def update_scripts(self, hash_type=SIGHASH_ALL): else: unlock_script = unlock_script_obj.serialize() if self.witness_type == 'segwit': - script_code = b'' + self.locking_script = b'' for k in self.keys: - script_code += varstr(k.public_byte) + b'\xad\xab' - if len(script_code) > 3: - script_code = script_code[:-2] + b'\xac' - self.script_code = script_code + self.locking_script += varstr(k.public_byte) + b'\xad\xab' + if len(self.locking_script) > 3: + self.locking_script = self.locking_script[:-2] + b'\xac' if signatures: self.witnesses = unlock_script elif self.witness_type == 'p2sh-segwit': self.unlocking_script = varstr(b'\0' + varstr(self.public_hash)) - self.script_code = self.unlocking_script if signatures: self.witnesses = unlock_script elif unlock_script != b'' and self.strict: self.unlocking_script = unlock_script elif self.script_type == 'signature': if self.keys: - self.script_code = varstr(self.keys[0].public_byte) + b'\xac' - self.locking_script = self.script_code + self.locking_script = varstr(self.keys[0].public_byte) + b'\xac' addr_data = hash160(self.keys[0].public_byte) if self.signatures and not self.unlocking_script: self.unlocking_script = varstr(self.signatures[0].as_der_encoded()) @@ -571,7 +566,6 @@ def as_dict(self): # 'locktime_cltv': self.locktime_cltv, 'locktime_csv': self.locktime_csv, 'public_hash': self.public_hash.hex(), - 'script_code': self.script_code.hex(), 'unlocking_script': self.unlocking_script.hex(), 'locking_script': self.locking_script.hex(), 'witness_type': self.witness_type, @@ -1565,7 +1559,7 @@ def signature_segwit(self, sign_id, hash_type=SIGHASH_ALL): sign_id) if not self.inputs[sign_id].redeemscript: - self.inputs[sign_id].redeemscript = self.inputs[sign_id].script_code + self.inputs[sign_id].redeemscript = self.inputs[sign_id].locking_script if (not self.inputs[sign_id].redeemscript or self.inputs[sign_id].redeemscript == b'\0') and \ self.inputs[sign_id].redeemscript != 'unknown' and not is_coinbase: From eeb44a41534e59ef6e00fc4915275409811272d0 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 18 May 2024 19:29:10 +0200 Subject: [PATCH 197/207] Increase delta in blockcount unittest --- tests/test_services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_services.py b/tests/test_services.py index 62a68997..fbbbf6db 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -609,7 +609,7 @@ def test_service_blockcount(self): n_blocks = None for provider in srv.results: if n_blocks is not None: - self.assertAlmostEqual(srv.results[provider], n_blocks, delta=25000 if nw == 'testnet' else 3, + self.assertAlmostEqual(srv.results[provider], n_blocks, delta=200, msg="Network %s, provider %s value %d != %d" % (nw, provider, srv.results[provider], n_blocks)) n_blocks = srv.results[provider] From a98dfdb5d3954535c90ec460b07c000236b7d6ce Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 19 May 2024 21:17:07 +0200 Subject: [PATCH 198/207] Add redeemscript to Input, cleanup Input init method --- bitcoinlib/scripts.py | 2 +- bitcoinlib/transactions.py | 60 +++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 5d0cf414..aea34b95 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -57,7 +57,7 @@ def _get_script_types(blueprint): bp.append(item) script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp] - if script_types: + if len(script_types) == 1: return script_types bp_len = [int(c.split('-')[1]) for c in blueprint if isinstance(c, str) and c[:4] == 'data'] diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index ffca4cea..b945edf4 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -146,10 +146,10 @@ class Input(object): """ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=b'', unlocking_script=b'', - locking_script=None, script_type=None, address='', sequence=0xffffffff, compressed=None, - sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, locktime_cltv=None, - locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, strict=True, - network=DEFAULT_NETWORK): + locking_script=None, redeemscript=None, script_type=None, address='', sequence=0xffffffff, + compressed=None, sigs_required=None, sort=False, index_n=0, value=0, double_spend=False, + locktime_cltv=None, locktime_csv=None, key_path='', witness_type=None, witnesses=None, encoding=None, + strict=True, network=DEFAULT_NETWORK): """ Create a new transaction input @@ -167,6 +167,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= :type unlocking_script: bytes, hexstring :param locking_script: Unlocking script for signing transaction :type locking_script: bytes, hexstring + :param redeemscript: Redeem script for p2sh transaction. Will be automatically created for standard scripts + :type redeemscript: bytes, hexstring :param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh :type script_type: str :param address: Address string or object for input @@ -210,8 +212,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= self.output_n_int = int.from_bytes(output_n, 'big') self.output_n = output_n self.unlocking_script = b'' if unlocking_script is None else to_bytes(unlocking_script) - self.locking_script = b'' if locking_script is None \ - else to_bytes(locking_script) + self.locking_script = b'' if locking_script is None else to_bytes(locking_script) self.script = None self.hash_type = SIGHASH_ALL if isinstance(sequence, numbers.Number): @@ -242,7 +243,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= else: self.address = address self.signatures = [] - self.redeemscript = b'' + self.redeemscript = b'' if not redeemscript else redeemscript self.script_type = script_type if self.prev_txid == b'\0' * 32: self.script_type = 'coinbase' @@ -282,15 +283,22 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= sigs_required = script.sigs_required if len(script.script_types) == 1 and not self.script_type: self.script_type = script.script_types[0] - elif script.script_types == ['signature_multisig', 'multisig']: - self.script_type = 'p2sh_multisig' - # TODO: Check if this if is necessary - if 'p2wpkh' in script.script_types: + # elif script.script_types == ['signature_multisig', 'multisig']: + # self.script_type = 'p2sh_multisig' + # If unlocking script type is an embedded p2sh script + if self.script_type == 'p2wpkh': self.script_type = 'p2sh_p2wpkh' self.witness_type = 'p2sh-segwit' - elif 'p2wsh' in script.script_types: + elif self.script_type == 'p2wsh': self.script_type = 'p2sh_p2wsh' self.witness_type = 'p2sh-segwit' + # TODO: Check if this if is necessary + # if 'p2wpkh' in script.script_types: + # self.script_type = 'p2sh_p2wpkh' + # self.witness_type = 'p2sh-segwit' + # elif 'p2wsh' in script.script_types: + # self.script_type = 'p2sh_p2wsh' + # self.witness_type = 'p2sh-segwit' if self.locking_script and not self.signatures: ls = Script.parse_bytes(self.locking_script, strict=strict) self.public_hash = self.public_hash if not ls.public_hash else ls.public_hash @@ -405,7 +413,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): addr_data = b'' unlock_script = b'' - if self.script_type in ['sig_pubkey', 'p2sh_p2wpkh', 'p2wpkh']: # fixme: p2wpkh == p2sh_p2wpkh + if self.script_type in ['sig_pubkey', 'p2sh_p2wpkh']: if not self.public_hash and self.keys: self.public_hash = self.keys[0].hash160 if not self.keys and not self.public_hash: @@ -422,11 +430,11 @@ def update_scripts(self, hash_type=SIGHASH_ALL): self.unlocking_script = b'' elif unlock_script != b'': self.unlocking_script = unlock_script - elif self.script_type in ['p2sh_multisig', 'p2sh_p2wsh', 'p2wsh']: # fixme: p2sh_p2wsh == p2wsh + elif self.script_type in ['p2sh_multisig', 'p2sh_p2wsh']: if not self.redeemscript and self.keys: self.redeemscript = Script(script_types=['multisig'], keys=self.keys, sigs_required=self.sigs_required).serialize() - if self.redeemscript: + if self.redeemscript and not self.public_hash: if self.witness_type == 'segwit' or self.witness_type == 'p2sh-segwit': self.public_hash = hashlib.sha256(self.redeemscript).digest() else: @@ -462,7 +470,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL): self.unlocking_script = varstr(b'\0' + varstr(self.public_hash)) if signatures: self.witnesses = unlock_script - elif unlock_script != b'' and self.strict: + elif unlock_script != b'': # and self.strict: self.unlocking_script = unlock_script elif self.script_type == 'signature': if self.keys: @@ -478,16 +486,16 @@ def update_scripts(self, hash_type=SIGHASH_ALL): if addr_data and not self.address: self.address = Address(hashed_data=addr_data, encoding=self.encoding, network=self.network, script_type=self.script_type, witness_type=self.witness_type).address - - if self.locktime_cltv: - self.locking_script = script_add_locktime_cltv(self.locktime_cltv, self.locking_script) - # if self.unlocking_script: - # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) - # if self.witness_type == 'segwit': - # self.witnesses.insert(0, script_add_locktime_cltv(self.locktime_cltv, b'')) - if self.locktime_csv: - self.locking_script = script_add_locktime_csv(self.locktime_csv, self.locking_script) - self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) + # FIXME: need to add locktime script to redeemscript + # if self.locktime_cltv: + # self.locking_script = script_add_locktime_cltv(self.locktime_cltv, self.locking_script) + # # if self.unlocking_script: + # # self.unlocking_script = script_add_locktime_cltv(self.locktime_cltv, self.unlocking_script) + # # if self.witness_type == 'segwit': + # # self.witnesses.insert(0, script_add_locktime_cltv(self.locktime_cltv, b'')) + # if self.locktime_csv: + # self.locking_script = script_add_locktime_csv(self.locktime_csv, self.locking_script) + # self.unlocking_script = script_add_locktime_csv(self.locktime_csv, self.unlocking_script) return True def verify(self, transaction_hash): From f39ccc00580631c4e6c324c529a2df748e17f863 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 23 May 2024 00:07:39 +0200 Subject: [PATCH 199/207] Cleanup Script and script_types --- bitcoinlib/config/config.py | 7 +++-- bitcoinlib/scripts.py | 51 +++++++++++++++++++++++++------------ bitcoinlib/transactions.py | 25 +++--------------- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 12f8e4bc..9dbd1fcd 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -75,11 +75,14 @@ 'sig_pubkey': ('unlocking', ['signature', 'key'], []), # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), + 'multisig_redeemscript': ('unlocking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), 'p2tr_unlock': ('unlocking', ['data'], [64]), 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), - 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), - 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + # 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), + # 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + 'p2sh_p2wpkh': ('unlocking', [op.op_0, 'data'], [20]), + 'p2sh_p2wsh': ('unlocking', [op.op_0, 'data'], [32]), 'signature': ('unlocking', ['signature'], []), 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index aea34b95..1ab9525d 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -41,7 +41,7 @@ def __str__(self): return self.msg -def _get_script_types(blueprint): +def _get_script_types(blueprint, is_locking=None): # Convert blueprint to more generic format bp = [] for item in blueprint: @@ -56,7 +56,14 @@ def _get_script_types(blueprint): else: bp.append(item) - script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp] + if is_locking is None: + locktype = ['locking', 'unlocking'] + elif is_locking: + locktype = ['locking'] + else: + locktype = ['unlocking'] + + script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp and values[0] in locktype] if len(script_types) == 1: return script_types @@ -65,7 +72,7 @@ def _get_script_types(blueprint): while len(bp): # Find all possible matches with blueprint matches = [(key, len(values[1]), values[2]) for key, values in SCRIPT_TYPES.items() if - values[1] == bp[:len(values[1])]] + values[1] == bp[:len(values[1])] and values[0] in locktype] if not matches: script_types.append('unknown') break @@ -80,9 +87,10 @@ def _get_script_types(blueprint): match_id = matches.index(match) break - # Add script type to list + # Add script type to list, if script_type = matches[match_id][0] - if script_type == 'multisig' and script_types[-1:] == ['signature_multisig']: + if (script_type == 'multisig' or script_type == 'multisig_redeemscript') \ + and script_types[-1:] == ['signature_multisig']: script_types.pop() script_type = 'p2sh_multisig' script_types.append(script_type) @@ -240,7 +248,7 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self._blueprint.append('data-%d' % len(c)) @classmethod - def parse(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -255,6 +263,8 @@ def parse(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -269,10 +279,10 @@ def parse(cls, script, message=None, env_data=None, strict=True, _level=0): elif isinstance(script, str): data_length = len(script) script = BytesIO(bytes.fromhex(script)) - return cls.parse_bytesio(script, message, env_data, data_length, strict, _level) + return cls.parse_bytesio(script, message, env_data, data_length, is_locking, strict, _level) @classmethod - def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, strict=True, _level=0): + def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -284,6 +294,8 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric :type env_data: dict :param data_length: Length of script data if known. Supply if you can to increase efficiency and lower change of incorrect parsing :type data_length: int + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -392,7 +404,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric script.seek(0) s._raw = script.read() - s.script_types = _get_script_types(blueprint) + s.script_types = _get_script_types(blueprint, is_locking=is_locking) if 'unknown' in s.script_types: s.script_types = ['unknown'] @@ -407,7 +419,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric if len(s.keys) != s.commands[-2] - 80: raise ScriptError("%d keys found but %d keys expected" % (len(s.keys), s.commands[-2] - 80)) - elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr'] and len(s.commands) > 1: + elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr', 'p2sh_p2wpkh', 'p2sh_p2wsh'] and len(s.commands) > 1: s.public_hash = s.commands[1] elif st == 'p2tr_unlock': s.public_hash = s.commands[0] @@ -422,7 +434,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric return s @classmethod - def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_hex(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -437,6 +449,8 @@ def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -445,10 +459,11 @@ def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) // 2 - return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, is_locking, strict, + _level) @classmethod - def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_bytes(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -460,6 +475,8 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -468,10 +485,10 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) :return Script: """ data_length = len(script) - return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, is_locking, strict, _level) @classmethod - def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_str(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse script in string format and return Script object. Extracts script commands, keys, signatures and other data. @@ -488,6 +505,8 @@ def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -508,7 +527,7 @@ def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): s_items.append(getattr(op, item.lower(), 'unknown-command-%s' % item)) else: s_items.append(bytes.fromhex(item)) - return cls(s_items, message, env_data, strict, _level) + return cls(s_items, message, env_data, is_locking=is_locking) def __repr__(self): s_items = self.view(blueprint=True, as_list=True) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index b945edf4..528be404 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -275,7 +275,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= # If unlocking script is specified extract keys, signatures, type from script if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys): - script = Script.parse_bytes(self.unlocking_script, strict=strict) + script = Script.parse_bytes(self.unlocking_script, is_locking=False, strict=strict) self.keys = script.keys self.signatures = script.signatures if len(self.signatures): @@ -283,24 +283,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= sigs_required = script.sigs_required if len(script.script_types) == 1 and not self.script_type: self.script_type = script.script_types[0] - # elif script.script_types == ['signature_multisig', 'multisig']: - # self.script_type = 'p2sh_multisig' - # If unlocking script type is an embedded p2sh script - if self.script_type == 'p2wpkh': - self.script_type = 'p2sh_p2wpkh' - self.witness_type = 'p2sh-segwit' - elif self.script_type == 'p2wsh': - self.script_type = 'p2sh_p2wsh' - self.witness_type = 'p2sh-segwit' - # TODO: Check if this if is necessary - # if 'p2wpkh' in script.script_types: - # self.script_type = 'p2sh_p2wpkh' - # self.witness_type = 'p2sh-segwit' - # elif 'p2wsh' in script.script_types: - # self.script_type = 'p2sh_p2wsh' - # self.witness_type = 'p2sh-segwit' if self.locking_script and not self.signatures: - ls = Script.parse_bytes(self.locking_script, strict=strict) + ls = Script.parse_bytes(self.locking_script, is_locking=True, strict=strict) self.public_hash = self.public_hash if not ls.public_hash else ls.public_hash if ls.script_types[0] in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' @@ -309,7 +293,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if self.script_type is None and self.witness_type is None and self.witnesses: self.witness_type = 'segwit' if self.witness_type is None or self.witness_type == 'legacy': - # if self.script_type in ['p2wpkh', 'p2wsh', 'p2sh_p2wpkh', 'p2sh_p2wsh']: if self.script_type in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' elif self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: @@ -673,7 +656,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri self.encoding = 'base58' self.spent = spent self.output_n = output_n - self.script = Script.parse_bytes(self.lock_script, strict=strict) + self.script = Script.parse_bytes(self.lock_script, strict=strict, is_locking=True) self.witver = witver if self._address_obj: @@ -968,7 +951,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None, witness = rawtx.read(item_size) inputs[n].witnesses.append(witness) if not is_taproot: - s = Script.parse_bytes(witness, strict=strict) + s = Script.parse_bytes(witness, strict=strict, is_locking=False) if s.script_types == ['p2tr_unlock']: # FIXME: Support Taproot unlocking scripts _logger.warning("Taproot is not supported at the moment, rest of parsing input transaction " From 18083addc4ff17f16c74f1677adda51b22c6f177 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Fri, 24 May 2024 12:32:19 +0200 Subject: [PATCH 200/207] Add trace method to Script evaluate method --- bitcoinlib/scripts.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 1ab9525d..0a9318aa 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -87,7 +87,7 @@ def _get_script_types(blueprint, is_locking=None): match_id = matches.index(match) break - # Add script type to list, if + # Add script type to list, if script is p2sh embedded multisig set type to p2sh_multisig script_type = matches[match_id][0] if (script_type == 'multisig' or script_type == 'multisig_redeemscript') \ and script_types[-1:] == ['signature_multisig']: @@ -655,7 +655,7 @@ def view(self, blueprint=False, as_list=False, op_code_numbers=False, show_1_byt return s_items if as_list else ' '.join(str(i) for i in s_items) - def evaluate(self, message=None, env_data=None): + def evaluate(self, message=None, env_data=None, trace=False): """ Evaluate script, run all commands and check if it is valid @@ -690,6 +690,13 @@ def evaluate(self, message=None, env_data=None): commands = self.commands[:] while len(commands): command = commands.pop(0) + if trace: + print("----------") + print("Stack:") + [print(f"- {i.hex()}") for i in self.stack] + cmd = opcodenames[command] if isinstance(command, int) else command.hex() + print(f"Command: {cmd}") + print("\n") if isinstance(command, int): if command == op.op_0: # OP_0 self.stack.append(encode_num(0)) From 4fffc736edf0113fc2209234937a8c0f3f09f6e7 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Tue, 28 May 2024 17:25:13 +0200 Subject: [PATCH 201/207] Distinct redeemscript from rest of script --- bitcoinlib/scripts.py | 21 ++++++++++++++++++--- tests/test_script.py | 13 ++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 0a9318aa..4d6415b7 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -53,6 +53,8 @@ def _get_script_types(blueprint, is_locking=None): bp[-1] = 'key' elif item == 'signature' and len(bp) and bp[-1] == 'signature': bp[-1] = 'signature' + elif isinstance(item, list): + bp.append('redeemscript') else: bp.append(item) @@ -130,6 +132,8 @@ def get_data_type(data): return 'key_object' elif isinstance(data, Signature): return 'signature_object' + elif isinstance(data, list): + return 'redeemscript' elif data.startswith(b'\x30') and 69 <= len(data) <= 74: return 'signature' elif ((data.startswith(b'\x02') or data.startswith(b'\x03')) and len(data) == 33) or \ @@ -365,8 +369,8 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, is_lo else: s2 = Script.parse_bytes(data, _level=_level+1, strict=strict) commands.pop() - commands += s2.commands - blueprint += s2.blueprint + commands += [s2.commands] + blueprint += [s2.blueprint] keys += s2.keys signatures += s2.signatures redeemscript = s2.redeemscript @@ -399,6 +403,10 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, is_lo chb = script.read(1) ch = int.from_bytes(chb, 'big') + if len(commands) == 1 and isinstance(commands[0], list): + commands = commands[0] + if len(blueprint) == 1 and isinstance(blueprint[0], list): + blueprint = blueprint[0] s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, env_data=env_data, hash_type=hash_type) script.seek(0) @@ -639,6 +647,8 @@ def view(self, blueprint=False, as_list=False, op_code_numbers=False, show_1_byt s_items.append(command) else: s_items.append(opcodenames.get(command, 'unknown-op-%s' % command)) + elif isinstance(command, list): + s_items.append('redeemscript') else: if blueprint: if self.blueprint and len(self.blueprint) >= i: @@ -687,7 +697,12 @@ def evaluate(self, message=None, env_data=None, trace=False): self.env_data = self.env_data if env_data is None else env_data self.stack = Stack() - commands = self.commands[:] + commands = [] + for c in self.commands: + if isinstance(c, list): + commands += c + else: + commands.append(c) while len(commands): command = commands.pop(0) if trace: diff --git a/tests/test_script.py b/tests/test_script.py index cd857722..7a24e49e 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -624,10 +624,10 @@ def test_script_verify_transaction_input_p2sh_multisig(self): script = unlock_script + lock_script s = Script.parse_bytes(script) - self.assertEqual(s.blueprint, [0, 'signature', 'signature', 82, 'key', 'key', 'key', 83, 174, 169, + self.assertEqual(s.blueprint, [0, 'signature', 'signature', [82, 'key', 'key', 'key', 83, 174], 169, 'data-20', 135]) self.assertEqual(s.script_types, ['p2sh_multisig', 'p2sh']) - self.assertEqual(str(s), "OP_0 signature signature OP_2 key key key OP_3 OP_CHECKMULTISIG OP_HASH160 " + self.assertEqual(str(s), "OP_0 signature signature redeemscript OP_HASH160 " "data-20 OP_EQUAL") transaction_hash = bytes.fromhex('5a805853bf82bcdd865deb09c73ccdd61d2331ac19d8c2911f17c7d954aec059') self.assertTrue(s.evaluate(message=transaction_hash)) @@ -673,13 +673,12 @@ def test_script_verify_transaction_input_p2sh_multisig_huge(self): script = unlock_script + lock_script s = Script.parse_bytes(script) self.assertEqual(s.blueprint, [0, 'signature', 'signature', 'signature', 'signature', 'signature', - 'signature', 'signature', 'signature', 88, 'key', 'key', 'key', 'key', + 'signature', 'signature', 'signature', [88, 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', 'key', - 95, 174, 169, 'data-20', 135]) + 95, 174], 169, 'data-20', 135]) self.assertEqual(s.script_types, ['p2sh_multisig', 'p2sh']) self.assertEqual(str(s), "OP_0 signature signature signature signature signature signature signature " - "signature OP_8 key key key key key key key key key key key key key key key OP_15 " - "OP_CHECKMULTISIG OP_HASH160 data-20 OP_EQUAL") + "signature redeemscript OP_HASH160 data-20 OP_EQUAL") transaction_hash = bytes.fromhex('8d190df3d02369999cad3eb222ac18b3315ff2bdc449b8fb30eb14db45730fe3') self.assertEqual(s.redeemscript, redeemscript) self.assertTrue(s.evaluate(message=transaction_hash)) @@ -884,7 +883,7 @@ def test_script_large_redeemscript_packing(self): redeemscript_size = '4dff01' + redeemscript s = Script.parse_hex(redeemscript_size) - self.assertEqual((str(s)), redeemscript_str) + self.assertEqual((str(s)), "redeemscript OP_15 OP_CHECKMULTISIG") redeemscript_error = '4d0101' + redeemscript self.assertRaisesRegex(ScriptError, "Malformed script, not enough data found", Script.parse_hex, From 3c4bd3b0d24bfbf1c3c40a366b88efd66a54980c Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 29 May 2024 15:59:30 +0200 Subject: [PATCH 202/207] op_checkmultisig should leave 1 on stack when succesfull --- bitcoinlib/scripts.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index 4d6415b7..720218c8 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -732,8 +732,13 @@ def evaluate(self, message=None, env_data=None, trace=False): method = getattr(self.stack, method_name) if method_name == 'op_checksig' or method_name == 'op_checksigverify': res = method(self.message) - elif method_name == 'op_checkmultisig' or method_name == 'op_checkmultisigverify': + elif method_name == 'op_checkmultisig': + method(self.message, self.env_data) + res = self.stack.op_verify() + self.stack.append(self.env_data['redeemscript']) + elif method_name == 'op_checkmultisigverify': res = method(self.message, self.env_data) + self.stack.append(self.env_data['redeemscript']) elif method_name == 'op_checklocktimeverify': res = self.stack.op_checklocktimeverify( self.env_data['sequence'], self.env_data.get('locktime')) @@ -1179,10 +1184,7 @@ def op_checkmultisig(self, message, data=None): break if sigcount == len(signatures): - if data and 'redeemscript' in data: - self.append(data['redeemscript']) - else: - self.append(b'\1') + self.append(b'\1') else: self.append(b'') return True From 7fc61a323bb1301976de8ae0e7772352234f1bda Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Wed, 29 May 2024 20:41:25 +0200 Subject: [PATCH 203/207] Fix issue when trying to add existing key to wallet --- bitcoinlib/wallets.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bitcoinlib/wallets.py b/bitcoinlib/wallets.py index 71704efd..42739d1f 100644 --- a/bitcoinlib/wallets.py +++ b/bitcoinlib/wallets.py @@ -390,14 +390,14 @@ def from_key(name, wallet_id, session, key, account_id=0, network=None, change=0 path = "M" address = k.address(encoding=encoding, script_type=script_type) - if commit: - keyexists = session.query(DbKey).\ - filter(DbKey.wallet_id == wallet_id, - DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() - if keyexists: - _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) - return WalletKey(keyexists.id, session, k) + keyexists = session.query(DbKey).\ + filter(DbKey.wallet_id == wallet_id, + DbKey.wif == k.wif(witness_type=witness_type, multisig=multisig, is_private=True)).first() + if keyexists: + _logger.warning("Key already exists in this wallet. Key ID: %d" % keyexists.id) + return WalletKey(keyexists.id, session, k) + if commit: wk = session.query(DbKey).filter( DbKey.wallet_id == wallet_id, or_(DbKey.public == k.public_byte, From 7d1b1d887eb3298c6aed3cf9320b7cfc5c27ef06 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Thu, 30 May 2024 08:29:20 +0200 Subject: [PATCH 204/207] Add script locking type unittest --- tests/test_script.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_script.py b/tests/test_script.py index 7a24e49e..aac056ca 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -920,6 +920,12 @@ def test_script_str(self): self.assertListEqual(s.commands, clist) self.assertEqual(s.view(), script_str_2) + def test_script_locking_type(self): + script_str = (b'"\x00 \x04\x7f\x8d]S\x04\xb8\xa1x\xbf\xfb\xd7\xc1\xc0\xc7\xc2To\xc9O\xc3\xb2\x91\n\xdb\x9db' + b'\x19\x85{]\x9f') + self.assertEqual(Script.parse(script_str, is_locking=True).script_types, ['p2wsh']) + self.assertEqual(Script.parse(script_str, is_locking=False).script_types, ['p2sh_p2wsh']) + class TestScriptMPInumbers(unittest.TestCase): def test_encode_decode_numbers(self): From f84552ead5358bd690eaf75a4a2a0957c82a1152 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sat, 1 Jun 2024 18:49:26 +0200 Subject: [PATCH 205/207] Use segwit as default for Input class --- bitcoinlib/transactions.py | 22 +++++++++++----- tests/test_transactions.py | 52 ++++++++++++++++++++------------------ tests/test_wallets.py | 4 +-- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index 528be404..c910dd88 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -236,12 +236,21 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if not isinstance(signatures, list): signatures = [signatures] self.sort = sort + + self.address = '' + self.address_obj = None if isinstance(address, Address): + self.address_obj = address self.address = address.address self.encoding = address.encoding self.network = address.network - else: + self.witness_type = address.witness_type + elif address: self.address = address + self.address_obj = Address.parse(address) + if self.address_obj: + encoding = self.address_obj.encoding + witness_type = self.address_obj.witness_type if self.address_obj.witness_type else witness_type self.signatures = [] self.redeemscript = b'' if not redeemscript else redeemscript self.script_type = script_type @@ -293,13 +302,14 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if self.script_type is None and self.witness_type is None and self.witnesses: self.witness_type = 'segwit' if self.witness_type is None or self.witness_type == 'legacy': - if self.script_type in ['p2wpkh', 'p2wsh']: - self.witness_type = 'segwit' - elif self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: + if self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: self.witness_type = 'p2sh-segwit' self.encoding = 'base58' - else: - self.witness_type = 'legacy' + elif not self.witness_type: + if not self.witnesses: + self.witness_type = 'legacy' + else: + self.witness_type = 'segwit' elif self.witness_type == 'segwit' and self.script_type == 'sig_pubkey' and encoding is None: self.encoding = 'bech32' if not self.script_type: diff --git a/tests/test_transactions.py b/tests/test_transactions.py index fb8f6c4f..e5901265 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -236,7 +236,7 @@ def test_transactions_sign_1(self): def test_transactions_sign_2(self): pk = Key('KwbbBb6iz1hGq6dNF9UsHc7cWaXJZfoQGFWeozexqnWA4M7aSwh4') # Private key for import inp = Input(prev_txid='fdaa42051b1fc9226797b2ef9700a7148ee8be9466fc8408379814cb0b1d88e3', - output_n=1, keys=pk.public()) + output_n=1, keys=pk.public(), witness_type='legacy') out = Output(95000, address='1K5j3KpsSt2FyumzLmoVjmFWVcpFhXHvNF') t = Transaction([inp], [out]) t.sign(pk) @@ -261,8 +261,8 @@ def test_transactions_sign_multiple_inputs(self): utxo_hash = '0177ac29fa8b2960051321c730c6f15017503aa5b9c1dd2d61e7286e366fbaba' pk1 = HDKey(wif1) pk2 = HDKey(wif2) - input1 = Input(prev_txid=utxo_hash, output_n=0, keys=pk1.public_byte, index_n=0) - input2 = Input(prev_txid=utxo_hash, output_n=1, keys=pk2.public_byte, index_n=1) + input1 = Input(prev_txid=utxo_hash, output_n=0, keys=pk1.public_byte, index_n=0, witness_type='legacy') + input2 = Input(prev_txid=utxo_hash, output_n=1, keys=pk2.public_byte, index_n=1, witness_type='legacy') # Create a transaction with 2 inputs, and add 2 outputs below osm_address = '1J3pt9koWJZTo2jarg98RL89iJqff9Kobp' @@ -306,14 +306,16 @@ def test_transactions_estimate_size_p2pkh(self): t = Transaction(witness_type='legacy') t.add_output(2710000, '1Khyc5eUddbhYZ8bEZi9wiN8TrmQ8uND4j') t.add_output(2720000, '1D1gLEHsvjunpJxqjkWcPZqU4QzzRrHDdL') - t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) + t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0, + witness_type='legacy') self.assertEqual(t.estimate_size(), 227) def test_transactions_estimate_size_nulldata(self): t = Transaction(witness_type='legacy') lock_script = b'j' + varstr(b'Please leave a message after the beep') t.add_output(0, lock_script=lock_script) - t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0) + t.add_input('82b48b128232256d1d5ce0c6ae7f7897f2b464d44456c25d7cf2be51626530d9', 0, + witness_type='legacy') self.assertEqual(t.estimate_size(number_of_change_outputs=1), 241) def test_transaction_very_large(self): @@ -1081,7 +1083,7 @@ def test_transaction_sign_uncompressed(self): prev_tx = "5b5903a9e5f5a1fee68fbd597085969a36789dc5b5e397dad76a57c3fb7c232a" output_n = 0 t = Transaction() - t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False) + t.add_input(prev_txid=prev_tx, output_n=output_n, compressed=False, witness_type='legacy') t.add_output(99900000, '1EHmhQH4HjJF7e4tyX61PVzzVevRJfsPMg') t.sign(ki.private_byte) self.assertTrue(t.verify()) @@ -1178,7 +1180,7 @@ def test_transaction_equal(self): t2 = Transaction([Input('a8d4eb4ba80e5cc87fc38c0a7df44461e995fb021ed33bfd5ecf0d12137fb85e', 0, '033c152137b251654f971bc4b2335646186e1298bfcbb0b7608ec33609ac08cc6f', '1e989d20c6f25bd36df33ef0206398b0708069ac7d1d7cdb0cd756dcb05f4dcc3c6ad2ca53cd3b5b82fc6969' - 'e43f4825bded505e351e7b4a3492b5c6f0054c94')], + 'e43f4825bded505e351e7b4a3492b5c6f0054c94', witness_type='legacy')], [Output(3077, '1Bj8rNK1tRsic6TRgJgVc6vF5FMAZVicaN', '75a94834a58225d5aa1d0403f3c72f7d8b01b0dd', script_type='p2pkh')], ) @@ -1279,7 +1281,7 @@ def test_transaction_bumpfee(self): value = 100000 # Test 1 - bumpfee, extra_fee and remove output - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) outputs = [ Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] @@ -1294,7 +1296,7 @@ def test_transaction_bumpfee(self): self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) # Test 2 - bumpfee, extra_fee, round dust and remove output - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) outputs = [ Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] @@ -1309,7 +1311,7 @@ def test_transaction_bumpfee(self): self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) # Test 3 - bumpfee, fee and remove output - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) outputs = [ Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] @@ -1324,7 +1326,7 @@ def test_transaction_bumpfee(self): self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) # Test 4 - bumpfee, fee, round dust and remove output - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=value) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=value) outputs = [ Output(90000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(5000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', change=True)] @@ -1339,7 +1341,7 @@ def test_transaction_bumpfee(self): self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) # Test 5 - bumpfee, 2 change outputs, no parameters - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=200000) outputs = [ Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', @@ -1356,7 +1358,7 @@ def test_transaction_bumpfee(self): self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee) # Test 6 - bumpfee, fee, 2 change outputs - inp = Input(prev_txid, output_n, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', value=200000) + inp = Input(prev_txid, output_n, address='zyJgheqe9HEXmmn7VP45dawA5P6BAYY4NG', value=200000) outputs = [ Output(180000, address='blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag', network='bitcoinlib_test'), Output(10000, address='blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', network='bitcoinlib_test', @@ -1422,7 +1424,7 @@ def test_transaction_multisig_p2sh_sign(self): t.add_output(55600, '18tiB1yNTzJMCg6bQS1Eh29dvJngq8QTfx') t.add_input('02b082113e35d5386285094c2829e7e2963fa0b5369fb7f4b79c4c90877dcd3d', 0, keys=[self.keylist[0], self.keylist[1], self.keylist[2]], script_type='p2sh_multisig', - sigs_required=2, compressed=False, sort=False) + sigs_required=2, compressed=False, sort=False, witness_type='legacy') pk1 = Key(self.keylist[0]).private_byte pk2 = Key(self.keylist[2]).private_byte t.sign([pk1, pk2]) @@ -1439,7 +1441,7 @@ def test_transaction_multisig_p2sh_sign_separate(self): pubk2 = Key(self.keylist[2]).public() t.add_input('02b082113e35d5386285094c2829e7e2963fa0b5369fb7f4b79c4c90877dcd3d', 0, keys=[pubk0, self.keylist[0], pubk2], script_type='p2sh_multisig', - sigs_required=2, compressed=False, sort=False) + sigs_required=2, compressed=False, sort=False, witness_type='legacy') pk1 = Key(self.keylist[0]).private_byte pk2 = Key(self.keylist[2]).private_byte t.sign([pk1]) @@ -1476,7 +1478,8 @@ def test_transaction_multisig_signature_redeemscript_mixup(self): # Create 2-of-2 multisig transaction with 1 input and 1 output t = Transaction(network='testnet') t.add_input('a2c226037d73022ea35af9609c717d98785906ff8b71818cd4095a12872795e7', 1, - [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2) + [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2, + witness_type='legacy') t.add_output(900000, '2NEgmZU64NjiZsxPULekrFcqdS7YwvYh24r') # Sign with private key and verify t.sign(pk1) @@ -1491,7 +1494,7 @@ def test_transaction_multisig_sign_3_of_5(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1503,10 +1506,10 @@ def test_transaction_multisig_sign_3_of_5(self): self.assertTrue(t.verify()) def test_transaction_multisig_sign_2_of_5_not_enough(self): - t = Transaction(network='testnet') + t = Transaction(network='testnet', witness_type='legacy') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1520,7 +1523,7 @@ def test_transaction_multisig_sign_duplicate(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1534,7 +1537,7 @@ def test_transaction_multisig_sign_extra_sig(self): t = Transaction(network='testnet') t.add_input(self.utxo_prev_tx, self.utxo_output_n, [self.pk1.public_byte, self.pk2.public_byte, self.pk3.public_byte, self.pk4.public_byte, - self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3) + self.pk5.public_byte], script_type='p2sh_multisig', sigs_required=3, witness_type='legacy') t.add_output(100000, 'mi1Lxs5boL6nDM3teraP3moVfLXJXWrWSK') t.add_output(self.utxo_tbtcleft - 110000, '2Mt1veesS36nYspXhkMXYKGHRAbtEYF6b8W') @@ -1558,7 +1561,7 @@ def test_transaction_multisig_estimate_size(self): t = Transaction(network=network, witness_type='legacy') t.add_input(prev_txid, 0, [pk1.private_byte, pk2.public_byte, pk3.public_byte], script_type='p2sh_multisig', - sigs_required=2) + sigs_required=2, witness_type='legacy') t.add_output(10000, '22zkxRGNsjHJpqU8tSS7cahSZVXrz9pJKSs') self.assertEqual(t.estimate_size(), 339) @@ -1570,7 +1573,7 @@ def test_transaction_multisig_litecoin(self): t = Transaction(network=network) t.add_input(self.utxo_prev_tx, self.utxo_output_n, [pk1.public_byte, pk2.public_byte, pk3.public_byte], - script_type='p2sh_multisig', sigs_required=2) + script_type='p2sh_multisig', sigs_required=2, witness_type='legacy') t.add_output(100000, 'LTK1nK5TyGALmSup5SzhgkX1cnVQrC4cLd') t.sign(pk1) self.assertFalse(t.verify()) @@ -1729,7 +1732,8 @@ def test_transaction_save_load_sign(self): t = Transaction(network='testnet') t.add_input('a2c226037d73022ea35af9609c717d98785906ff8b71818cd4095a12872795e7', 1, - [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2) + [pk1.public_byte, pk2.public_byte], script_type='p2sh_multisig', sigs_required=2, + witness_type='legacy') t.add_output(900000, '2NEgmZU64NjiZsxPULekrFcqdS7YwvYh24r') self.assertFalse(t.verify()) t.save() diff --git a/tests/test_wallets.py b/tests/test_wallets.py index daf30106..f7166465 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1767,9 +1767,9 @@ def test_wallet_two_utxos_one_key(self): utxos = wlt.utxos() inp1 = Input(prev_txid=utxos[0]['txid'], output_n=utxos[0]['output_n'], keys=key.key_public, - network='bitcoinlib_test') + network='bitcoinlib_test', witness_type='legacy') inp2 = Input(prev_txid=utxos[1]['txid'], output_n=utxos[1]['output_n'], keys=key.key_public, - network='bitcoinlib_test') + network='bitcoinlib_test', witness_type='legacy') out = Output(10000000, address=key.address, network='bitcoinlib_test') t = Transaction(inputs=[inp1, inp2], outputs=[out], network='testnet') From 5d32d2d436fb17c3015bd2a530e9a40f12e9b005 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Sun, 2 Jun 2024 22:39:33 +0200 Subject: [PATCH 206/207] Add and fix witness_type in Address and Output class --- bitcoinlib/keys.py | 4 ++++ bitcoinlib/transactions.py | 28 ++++++++++++++----------- tests/test_keys.py | 14 ++++++++++++- tests/test_transactions.py | 42 ++++++++++++++++++++++++++++++-------- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index 10edd939..fc359251 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -852,6 +852,10 @@ def __init__(self, data='', hashed_data='', prefix=None, script_type=None, elif self.script_type == 'p2tr': witness_type = 'taproot' self.witver = 1 if self.witver == 0 else self.witver + elif self.encoding == 'base58': + witness_type = 'legacy' + else: + witness_type = 'segwit' self.witness_type = witness_type self.depth = depth self.change = change diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index c910dd88..a2144212 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -589,7 +589,7 @@ class Output(object): def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_script=b'', spent=False, output_n=0, script_type=None, witver=0, encoding=None, spending_txid='', spending_index_n=None, - strict=True, change=None, network=DEFAULT_NETWORK): + strict=True, change=None, witness_type=None, network=DEFAULT_NETWORK): """ Create a new transaction output @@ -628,6 +628,8 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri :type strict: bool :param change: Is this a change output back to own wallet or not? Used for replace-by-fee. :type change: bool + :param witness_type: Specify witness type: 'segwit' or 'legacy'. Determine from script, address or encoding if not specified. + :type witness_type: str :param network: Network, leave empty for default :type network: str, Network """ @@ -652,33 +654,33 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri public_key = address.public_byte if not script_type: script_type = script_type_default(address.witness_type, address.multisig, True) - self.public_hash = address.hash160 + # self.public_hash = address.hash160 + # self.witness_type = address.witness_type else: self._address = address self._address_obj = None self.public_key = to_bytes(public_key) self.compressed = True - self.k = None self.versionbyte = self.network.prefix_address self.script_type = script_type self.encoding = encoding - if not self._address and self.encoding is None: - self.encoding = 'base58' self.spent = spent self.output_n = output_n self.script = Script.parse_bytes(self.lock_script, strict=strict, is_locking=True) self.witver = witver + self.witness_type = witness_type if self._address_obj: self.script_type = self._address_obj.script_type if script_type is None else script_type + # if not script_type: + # script_type = script_type_default(address.witness_type, address.multisig, True) self.public_hash = self._address_obj.hash_bytes self.network = self._address_obj.network self.encoding = self._address_obj.encoding + self.witness_type = self._address_obj.witness_type if self.script: self.script_type = self.script_type if not self.script.script_types else self.script.script_types[0] - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr']: - self.encoding = 'bech32' self.public_hash = self.script.public_hash if self.script.keys: self.public_key = self.script.keys[0].public_byte @@ -686,8 +688,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri self.witver = self.script.commands[0] - 80 if self.public_key and not self.public_hash: - k = Key(self.public_key, is_private=False, network=network) - self.public_hash = k.hash160 + self.public_hash = hash160(self.public_key) elif self._address and (not self.public_hash or not self.script_type or not self.encoding): address_dict = deserialize_address(self._address, self.encoding, self.network.name) if address_dict['script_type'] and not script_type: @@ -703,10 +704,13 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri raise TransactionError("Network for output address %s is different from transaction network. %s not " "in %s" % (self._address, self.network.name, network_guesses)) self.public_hash = address_dict['public_key_hash_bytes'] + self.witness_type = address_dict['witness_type'] if not self.encoding: - self.encoding = 'base58' - if self.script_type in ['p2wpkh', 'p2wsh', 'p2tr']: - self.encoding = 'bech32' + self.encoding = 'bech32' + if self.script_type in ['p2pkh', 'p2sh', 'p2pk'] or self.witness_type == 'legacy': + self.encoding = 'base58' + else: + self.witness_type = 'segwit' if self.script_type is None: self.script_type = 'p2pkh' diff --git a/tests/test_keys.py b/tests/test_keys.py index b9c5712e..1fc1fff5 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -792,7 +792,7 @@ def test_hdkey_derive_from_public_and_private_random(self): self.assertEqual(pub_with_pubparent, pub_with_privparent) -class TestKeysAddress(unittest.TestCase): +class TestAddress(unittest.TestCase): """ Tests for Address class. Address format, conversion and representation @@ -898,6 +898,18 @@ def test_keys_address_p2tr_bcrt(self): encoding='bech32').address self.assertEqual(addr, 'bcrt1pq77c6jeemv8wxlsh5h5pfdq6323naua8yapte3juw9hyec83mr8sw2eggg') + def test_keys_address_witness_types(self): + data = b'\x03\xb0\x12\x86\x15bt\xc9\x0f\xa7\xd0\xf6\xe6\x17\xc9\xc6\xafS\xa0u/ou\x8d\xa5\x1d\x1c\xc9h4nl\xb8' + a = Address(data) + self.assertEqual(a.address, 'bc1q36cn4tunsaptdskkf29lerzym0uznqw26pxffm') + self.assertEqual(a.witness_type, 'segwit') + a = Address(data, witness_type='segwit') + self.assertEqual(a.address, 'bc1q36cn4tunsaptdskkf29lerzym0uznqw26pxffm') + self.assertEqual(a.witness_type, 'segwit') + a = Address(data, witness_type='legacy') + self.assertEqual(a.address, '1E1VGLvZ2YpgcSgr3DYm7ZTHbovKw9xLw6') + self.assertEqual(a.witness_type, 'legacy') + class TestKeysSignatures(unittest.TestCase): diff --git a/tests/test_transactions.py b/tests/test_transactions.py index e5901265..3be30dc4 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -108,29 +108,37 @@ def test_transaction_input_with_pkh(self): class TestTransactionOutputs(unittest.TestCase): - def test_transaction_output_add_address(self): + def test_transaction_output_address(self): to = Output(1000, '1QhnmvncrbZFkjt5R8hs8yHDM7xXX3feg') self.assertEqual(b'v\xa9\x14\x04{\x9d\xc2=\xda\xa9\x17\x1e\xa5\x11\xe1\x93t\xabUo\xaa\xbbD\x88\xac', to.lock_script) self.assertEqual(repr(to), '') - def test_transaction_output_add_address_p2sh(self): + def test_transaction_output_address_p2sh(self): to = Output(1000, '2N5WPJ2qPzVpy5LeE576JCwZfWg1ikjUxdK', network='testnet') self.assertEqual(b'\xa9\x14\x86\x7f\x84`u\x87\xf7\xc2\x05G@\xc6\xca\xe0\x92\x98\xcc\xbc\xd5(\x87', to.lock_script) - def test_transaction_output_add_public_key(self): - to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470' - '243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6') + def test_transaction_output_public_key_legacy(self): + to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522' + 'CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6', + witness_type='legacy') self.assertEqual(b"v\xa9\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee\x88\xac", to.lock_script) - def test_transaction_output_add_public_key_hash(self): - to = Output(1000, public_hash='010966776006953d5567439e5e39f86a0d273bee') + def test_transaction_output_public_key(self): + to = Output(1000000000, public_key='0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522' + 'CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6') + self.assertEqual(b"\x00\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee", + to.lock_script) + self.assertEqual('segwit', to.witness_type) + + def test_transaction_output_public_key_hash(self): + to = Output(1000, public_hash='010966776006953d5567439e5e39f86a0d273bee', witness_type='legacy') self.assertEqual(b"v\xa9\x14\x01\tfw`\x06\x95=UgC\x9e^9\xf8j\r';\xee\x88\xac", to.lock_script) - def test_transaction_output_add_script(self): + def test_transaction_output_script(self): to = Output(1000, lock_script='76a91423e102597c4a99516f851406f935a6e634dbccec88ac') self.assertEqual('14GiCdJHj3bznWpcocjcu9ByCmDPEhEoP8', to.address) @@ -140,6 +148,22 @@ def test_transaction_output_value(self): self.assertRaisesRegex(ValueError, "Value uses different network \(bitcoin\) then supplied network: testnet", Output, '1 BTC', address=HDKey(network='testnet').address(), network='testnet') + def test_transaction_output_witness_types(self): + k = HDKey(witness_type='segwit') + o = Output(10000, k) + self.assertEqual(o.witness_type, 'segwit') + self.assertEqual(o.script_type, 'p2wpkh') + + k = HDKey(witness_type='legacy') + o = Output(10000, k) + self.assertEqual(o.witness_type, 'legacy') + self.assertEqual(o.script_type, 'p2pkh') + + k = HDKey() + o = Output(10000, k) + self.assertEqual(o.witness_type, 'segwit') + self.assertEqual(o.script_type, 'p2wpkh') + class TestTransactions(unittest.TestCase): def setUp(self): @@ -226,7 +250,7 @@ def test_transactions_sign_1(self): keys=pk.public(), network='testnet', witness_type='legacy') # key for address mkzpsGwaUU7rYzrDZZVXFne7dXEeo6Zpw2 pubkey = Key('0391634874ffca219ff5633f814f7f013f7385c66c65c8c7d81e7076a5926f1a75', network='testnet') - out = Output(880000, public_hash=pubkey.hash160, network='testnet') + out = Output(880000, public_hash=pubkey.hash160, network='testnet', witness_type='legacy') t = Transaction([inp], [out], network='testnet') t.sign(pk) self.assertTrue(t.verify(), msg="Can not verify transaction '%s'") From 33dd52d1502a5085a946989eb7e8c8762007a402 Mon Sep 17 00:00:00 2001 From: Cryp Toon Date: Mon, 3 Jun 2024 14:20:42 +0200 Subject: [PATCH 207/207] Pass hash_type to sign methods and add unsupported warning --- bitcoinlib/config/config.py | 2 +- bitcoinlib/keys.py | 18 +++++++++++------- bitcoinlib/transactions.py | 7 +++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 9dbd1fcd..4b169638 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -95,7 +95,7 @@ SIGHASH_ALL = 1 SIGHASH_NONE = 2 SIGHASH_SINGLE = 3 -SIGHASH_ANYONECANPAY = 80 +SIGHASH_ANYONECANPAY = 0x80 SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31) # To enable sequence time locks SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22) # If set use timestamp based lock otherwise use block height diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index fc359251..d0ea8eaf 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -2386,7 +2386,7 @@ def parse_bytes(signature, public_key=None): hash_type=hash_type) @staticmethod - def create(txid, private, use_rfc6979=True, k=None): + def create(txid, private, use_rfc6979=True, k=None, hash_type=SIGHASH_ALL): """ Sign a transaction hash and create a signature with provided private key. @@ -2408,7 +2408,9 @@ def create(txid, private, use_rfc6979=True, k=None): :type use_rfc6979: bool :param k: Provide own k. Only use for testing or if you know what you are doing. Providing wrong value for k can result in leaking your private key! :type k: int - + :param hash_type: Specific hash type, default is SIGHASH_ALL + :type hash_type: int + :return Signature: """ if isinstance(txid, bytes): @@ -2445,7 +2447,7 @@ def create(txid, private, use_rfc6979=True, k=None): ) if int(s) > secp256k1_n / 2: s = secp256k1_n - int(s) - return Signature(r, s, txid, secret, public_key=pub_key, k=k) + return Signature(r, s, txid, secret, public_key=pub_key, k=k, hash_type=hash_type) else: sk = ecdsa.SigningKey.from_string(private.private_byte, curve=ecdsa.SECP256k1) txid_bytes = to_bytes(txid) @@ -2455,7 +2457,7 @@ def create(txid, private, use_rfc6979=True, k=None): s = int(signature[64:], 16) if s > secp256k1_n / 2: s = secp256k1_n - s - return Signature(r, s, txid, secret, public_key=pub_key, k=k) + return Signature(r, s, txid, secret, public_key=pub_key, k=k, hash_type=hash_type) def __init__(self, r, s, txid=None, secret=None, signature=None, der_signature=None, public_key=None, k=None, hash_type=SIGHASH_ALL): @@ -2680,7 +2682,7 @@ def verify(self, txid=None, public_key=None): return True -def sign(txid, private, use_rfc6979=True, k=None): +def sign(txid, private, use_rfc6979=True, k=None, hash_type=SIGHASH_ALL): """ Sign transaction hash or message with secret private key. Creates a signature object. @@ -2700,10 +2702,12 @@ def sign(txid, private, use_rfc6979=True, k=None): :type use_rfc6979: bool :param k: Provide own k. Only use for testing or if you know what you are doing. Providing wrong value for k can result in leaking your private key! :type k: int - + :param hash_type: Specific hash type, default is SIGHASH_ALL + :type hash_type: int + :return Signature: """ - return Signature.create(txid, private, use_rfc6979, k) + return Signature.create(txid, private, use_rfc6979, k, hash_type=hash_type) def verify(txid, signature, public_key=None): diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index a2144212..199238f4 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -1716,6 +1716,9 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A :return None: """ + if hash_type != SIGHASH_ALL: + raise TransactionError("Hash type othen than SIGHASH_ALL are not supported at the moment") + if index_n is None: tids = range(len(self.inputs)) else: @@ -1743,7 +1746,7 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A n_total_sigs = len(self.inputs[tid].keys) sig_domain = [''] * n_total_sigs - txid = self.signature_hash(tid, witness_type=self.inputs[tid].witness_type) + txid = self.signature_hash(tid, hash_type, self.inputs[tid].witness_type) for key in tid_keys: # Check if signature signs known key and is not already in list if key.public_byte not in pub_key_list: @@ -1758,7 +1761,7 @@ def sign(self, keys=None, index_n=None, multisig_key_n=None, hash_type=SIGHASH_A if not key.private_byte: raise TransactionError("Please provide a valid private key to sign the transaction") - sig = sign(txid, key) + sig = sign(txid, key, hash_type=hash_type) newsig_pos = pub_key_list.index(key.public_byte) sig_domain[newsig_pos] = sig n_signs += 1