From 00a2fa386e459924a6a652a9645ee655202ecc97 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Fri, 27 Sep 2024 16:25:27 +0000 Subject: [PATCH 1/7] Introduce composer.py --- .../counterpartycore/lib/backend/bitcoind.py | 15 ++ .../counterpartycore/lib/composer.py | 250 ++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 counterparty-core/counterpartycore/lib/composer.py diff --git a/counterparty-core/counterpartycore/lib/backend/bitcoind.py b/counterparty-core/counterpartycore/lib/backend/bitcoind.py index cf4be0e896..35ce378cd2 100644 --- a/counterparty-core/counterpartycore/lib/backend/bitcoind.py +++ b/counterparty-core/counterpartycore/lib/backend/bitcoind.py @@ -182,6 +182,17 @@ def fee_per_kb( return int(max(feeperkb["feerate"] * config.UNIT, config.DEFAULT_FEE_PER_KB_ESTIMATE_SMART)) +def satoshis_per_vbyte( + conf_target: int = config.ESTIMATE_FEE_CONF_TARGET, mode: str = config.ESTIMATE_FEE_MODE +): + feeperkb = rpc("estimatesmartfee", [conf_target, mode]) + + if "errors" in feeperkb and feeperkb["errors"][0] == "Insufficient data or no feerate found": + return config.DEFAULT_FEE_PER_KB_ESTIMATE_SMART + + return (feeperkb["feerate"] * config.UNIT) / 1024 + + def get_btc_supply(normalize=False): f"""returns the total supply of {config.BTC} (based on what Bitcoin Core says the current block height is)""" # noqa: B021 block_count = getblockcount() @@ -277,6 +288,10 @@ def get_tx_out_amount(tx_hash, vout): return raw_tx["vout"][vout]["value"] +def get_tx_out_value(tx_hash, vout): + return get_tx_out_amount(tx_hash, vout) + + class BlockFetcher: def __init__(self, first_block) -> None: self.current_block = first_block diff --git a/counterparty-core/counterpartycore/lib/composer.py b/counterparty-core/counterpartycore/lib/composer.py new file mode 100644 index 0000000000..9aea21c83b --- /dev/null +++ b/counterparty-core/counterpartycore/lib/composer.py @@ -0,0 +1,250 @@ +import binascii + +from bitcoin.core.script import ( + OP_CHECKMULTISIG, + OP_RETURN, + CScript, + CTransaction, + CTxIn, + CTxOut, +) +from bitcoin.wallet import CBitcoinAddress, P2WPKHBitcoinAddress + +from counterpartycore.lib import arc4, backend, config, exceptions, script, transaction, util +from counterpartycore.lib.transaction_helper.common_serializer import make_fully_valid +from counterpartycore.lib.transaction_helper.transaction_outputs import chunks + +MAX_INPUTS_SET = 100 + + +def get_script(address): + if script.is_multisig(address): + signatures_required, pubkeys, signatures_possible = script.extract_array(address) + return CScript([signatures_required] + pubkeys + [signatures_possible, OP_CHECKMULTISIG]) + elif script.is_bech32(address): + return P2WPKHBitcoinAddress(address).to_scriptPubKey() + else: + return CBitcoinAddress(address).to_scriptPubKey() + + +def get_default_value(address): + if script.is_multisig(address): + return config.MULTISIG_DUST_SIZE + else: + return config.REGULAR_DUST_SIZE + + +def perpare_non_data_outputs(destinations): + outputs = [] + for address, value in destinations: + output_value = value or get_default_value(address) + outputs.append(CTxOut(output_value, get_script(address))) + return outputs + + +def determine_encoding(data, desired_encoding="auto"): + encoding = desired_encoding + if desired_encoding == "auto": + if len(data) + len(config.PREFIX) <= config.OP_RETURN_MAX_SIZE: + encoding = "opreturn" + else: + encoding = "multisig" + if encoding not in ("multisig", "opreturn"): + raise exceptions.TransactionError(f"Not supported encoding: {encoding}") + return encoding + + +def encrypt_data(data, arc4_key): + key = arc4.init_arc4(binascii.unhexlify(arc4_key)) + return key.encrypt(data) + + +def prepare_opreturn_output(data, arc4_key=None): + if len(data) + len(config.PREFIX) > config.OP_RETURN_MAX_SIZE: + raise exceptions.TransactionError("One `OP_RETURN` output per transaction.") + opreturn_data = config.PREFIX + data + if arc4_key: + opreturn_data = encrypt_data.encrypt(opreturn_data, arc4_key) + return [CTxOut(0, CScript([OP_RETURN, opreturn_data]))] + + +def data_to_pubkey_pairs(data, arc4_key=None): + # Two pubkeys, minus length byte, minus prefix, minus two nonces, + # minus two sign bytes. + chunk_size = (33 * 2) - 1 - len(config.PREFIX) - 2 - 2 + data_array = list(chunks(data, chunk_size)) + pubkey_pairs = [] + for data_chunk in data_array: + # Get data (fake) public key. + pad_length = (33 * 2) - 1 - 2 - 2 - len(data_chunk) + assert pad_length >= 0 + output_data = bytes([len(data_chunk)]) + data_chunk + (pad_length * b"\x00") # noqa: PLW2901 + if arc4_key: + output_data = encrypt_data(output_data, arc4_key) + data_pubkey_1 = make_fully_valid(output_data[:31]) + data_pubkey_2 = make_fully_valid(output_data[31:]) + pubkey_pairs.append((data_pubkey_1, data_pubkey_2)) + return pubkey_pairs + + +def prepare_multisig_output(data, pubkey, arc4_key=None): + try: + dust_return_pubkey = binascii.unhexlify(pubkey) + except binascii.Error: + raise script.InputError(f"Invalid pubkey key: {pubkey}") # noqa: B904 + if not script.is_fully_valid(dust_return_pubkey): + raise exceptions.ComposeError(f"invalid public key: {pubkey}") + pubkey_pairs = data_to_pubkey_pairs(data, arc4_key) + outputs = [] + for pubkey_pair in pubkey_pairs: + output_script = CScript( + [1, pubkey_pair[0], pubkey_pair[1], dust_return_pubkey, 3, OP_CHECKMULTISIG] + ) + outputs.append(CTxOut(config.MULTISIG_DUST_SIZE, output_script)) + return outputs + + +def prepare_data_outputs(encoding, data, source, pubkey, arc4_key=None): + data_encoding = determine_encoding(data, encoding) + if data_encoding == "multisig": + return prepare_multisig_output(data, source, pubkey, arc4_key) + if data_encoding == "opreturn": + return prepare_opreturn_output(data, arc4_key) + raise exceptions.TransactionError(f"Not supported encoding: {encoding}") + + +def prepare_outputs(source, destinations, data, pubkey, encoding, arc4_key=None): + outputs = perpare_non_data_outputs(destinations) + if data: + outputs += prepare_data_outputs(encoding, data, source, pubkey, arc4_key) + return outputs + + +def prepare_unspent_list(inputs_set: str): + unspent_list = [] + utxos_list = inputs_set.split(",") + if len(utxos_list) > MAX_INPUTS_SET: + raise exceptions.ComposeError( + f"too many UTXOs in inputs_set (max. {MAX_INPUTS_SET}): {len(utxos_list)}" + ) + for str_input in utxos_list: + if not util.is_utxo_format(str_input): + raise exceptions.ComposeError(f"invalid UTXO: {str_input}") + try: + value = backend.bitcoind.get_tx_out_value( + str_input.split(":")[0], int(str_input.split(":")[1]) + ) + except Exception as e: + raise exceptions.ComposeError(f"invalid UTXO: {str_input}") from e + unspent_list.append( + { + "txid": str_input.split(":")[0], + "vout": int(str_input.split(":")[1]), + "value": value, + } + ) + return sorted(unspent_list, key=lambda x: x["value"], reverse=True) + + +def select_utxos(unspent_list, target_amount): + total_amount = 0 + selected_utxos = [] + for utxo in unspent_list: + total_amount += utxo["value"] + selected_utxos.append(utxo) + if total_amount >= target_amount: + break + return selected_utxos + + +def utxos_to_txins(utxos: list): + inputs = [] + for utxo in utxos: + inputs.append(CTxIn(CScript([utxo["txid"], utxo["vout"]]))) + return inputs + + +def get_virtual_size(weight): + return (weight + 3) // 4 + + +def get_needed_fee(tx, satoshis_per_vbyte=None): + weight = tx.calc_weight() + virtual_size = get_virtual_size(weight) + if satoshis_per_vbyte: + return satoshis_per_vbyte * virtual_size + else: + return backend.bitcoind.satoshis_per_vbyte() * virtual_size + + +def get_minimum_change(source): + if script.is_multisig(source): + return config.MULTISIG_DUST_SIZE + else: + return config.REGULAR_DUST_SIZE + + +def prepare_transaction(source, outputs, unspent_list, desired_fee): + outputs_total = sum(output["value"] for output in outputs) + target_amount = outputs_total + desired_fee + selected_utxos = select_utxos(unspent_list, target_amount) + input_total = sum(input["value"] for input in selected_utxos) + inputs = utxos_to_txins(selected_utxos) + change = input_total - target_amount + change_outputs = [] + if change > get_minimum_change(source): + change_outputs.append(CTxOut(change, get_script(source))) + else: + change = 0 + return inputs, change_outputs, input_total + + +def construct_transaction(source, outputs, unspent_list, desired_fee): + inputs, change_outputs, _input_total = prepare_transaction( + source, outputs, unspent_list, desired_fee + ) + tx = CTransaction(inputs, outputs + change_outputs) + return tx + + +def get_estimated_fee(source, outputs, unspent_list, satoshis_per_vbyte=None): + # calculate fee for a transaction with desired_fee = 0 + tx = construct_transaction(source, outputs, unspent_list, 0) + return get_needed_fee(tx, satoshis_per_vbyte) + + +def compose_transaction( + db, name, params, pubkey, inputs_set, encoding="auto", exact_fee=None, satoshis_per_vbyte=None +): + source, destinations, data = transaction.compose_data(db, name, params) + unspent_list = prepare_unspent_list(inputs_set) + + # prepare non obfuscted outputs + clear_outputs = prepare_outputs(source, destinations, data, pubkey, encoding) + + if exact_fee: + desired_fee = exact_fee + else: + # use non obfuscated outputs to calculate estimated fee... + desired_fee = get_estimated_fee(source, clear_outputs, unspent_list, satoshis_per_vbyte) + + # prepare transaction with desired fee and no-obfuscated outputs + inputs, change_outputs, btc_in = prepare_transaction( + source, clear_outputs, unspent_list, desired_fee + ) + # now we have inputs we can prepare obfuscated outputs + outputs = prepare_outputs( + source, destinations, data, pubkey, encoding, arc4_key=inputs[0]["txid"] + ) + tx = CTransaction(inputs, outputs + change_outputs) + btc_out = sum(output.nValue for output in outputs) + btc_change = sum(change_output.nValue for change_output in change_outputs) + + return { + "btc_in": btc_in, + "btc_out": btc_out, + "btc_change": btc_change, + "btc_fee": btc_in - btc_out - btc_change, + "unsigned_tx_hex": tx.serialize().hex(), + "data": config.PREFIX + data if data else None, + } From b4f2300a79ba454a3dea173e30345f06a5367e34 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Mon, 30 Sep 2024 11:40:53 +0000 Subject: [PATCH 2/7] Add unit tests --- .../counterpartycore/lib/backend/bitcoind.py | 2 +- .../counterpartycore/lib/composer.py | 115 ++-- .../counterpartycore/test/conftest.py | 7 + .../fixtures/contract_vectors/composer.py | 551 ++++++++++++++++++ .../counterpartycore/test/fixtures/vectors.py | 6 +- .../counterpartycore/test/util_test.py | 1 + 6 files changed, 633 insertions(+), 49 deletions(-) create mode 100644 counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py diff --git a/counterparty-core/counterpartycore/lib/backend/bitcoind.py b/counterparty-core/counterpartycore/lib/backend/bitcoind.py index 35ce378cd2..aa3cd66139 100644 --- a/counterparty-core/counterpartycore/lib/backend/bitcoind.py +++ b/counterparty-core/counterpartycore/lib/backend/bitcoind.py @@ -288,7 +288,7 @@ def get_tx_out_amount(tx_hash, vout): return raw_tx["vout"][vout]["value"] -def get_tx_out_value(tx_hash, vout): +def get_utxo_value(tx_hash, vout): return get_tx_out_amount(tx_hash, vout) diff --git a/counterparty-core/counterpartycore/lib/composer.py b/counterparty-core/counterpartycore/lib/composer.py index 9aea21c83b..a459912772 100644 --- a/counterparty-core/counterpartycore/lib/composer.py +++ b/counterparty-core/counterpartycore/lib/composer.py @@ -1,14 +1,21 @@ import binascii +from bitcoin.core import ( + CTransaction, + CTxIn, + CTxOut, +) from bitcoin.core.script import ( OP_CHECKMULTISIG, OP_RETURN, CScript, - CTransaction, - CTxIn, - CTxOut, ) -from bitcoin.wallet import CBitcoinAddress, P2WPKHBitcoinAddress +from bitcoin.wallet import ( + CBitcoinAddress, + CBitcoinAddressError, + P2PKHBitcoinAddress, + P2WPKHBitcoinAddress, +) from counterpartycore.lib import arc4, backend, config, exceptions, script, transaction, util from counterpartycore.lib.transaction_helper.common_serializer import make_fully_valid @@ -17,10 +24,26 @@ MAX_INPUTS_SET = 100 -def get_script(address): +def search_pubkey(address, provides_pubkeys=None): + if provides_pubkeys is None: + raise exceptions.ComposeError("no pubkeys provided") + for pubkey in provides_pubkeys: + try: + if not pubkey: + raise exceptions.ComposeError(f"invalid pubkey: {pubkey}") + if str(P2PKHBitcoinAddress.from_pubkey(bytes.fromhex(pubkey))) == address: + return pubkey + except (ValueError, CBitcoinAddressError) as e: + raise exceptions.ComposeError(f"invalid pubkey: {pubkey}") from e + raise exceptions.ComposeError(f"`{address}` pubkey not found in provided pubkeys") + + +def get_script(address, pubkeys=None): if script.is_multisig(address): - signatures_required, pubkeys, signatures_possible = script.extract_array(address) - return CScript([signatures_required] + pubkeys + [signatures_possible, OP_CHECKMULTISIG]) + signatures_required, addresses, signatures_possible = script.extract_array(address) + pubkeys = [search_pubkey(address, pubkeys) for address in addresses] + pubkeys = [bytes.fromhex(pubkey) for pubkey in pubkeys] + return CScript([signatures_required] + pubkeys + [signatures_possible] + [OP_CHECKMULTISIG]) elif script.is_bech32(address): return P2WPKHBitcoinAddress(address).to_scriptPubKey() else: @@ -29,16 +52,16 @@ def get_script(address): def get_default_value(address): if script.is_multisig(address): - return config.MULTISIG_DUST_SIZE + return config.DEFAULT_MULTISIG_DUST_SIZE else: - return config.REGULAR_DUST_SIZE + return config.DEFAULT_REGULAR_DUST_SIZE -def perpare_non_data_outputs(destinations): +def perpare_non_data_outputs(destinations, pubkeys=None): outputs = [] for address, value in destinations: output_value = value or get_default_value(address) - outputs.append(CTxOut(output_value, get_script(address))) + outputs.append(CTxOut(output_value, get_script(address, pubkeys))) return outputs @@ -61,10 +84,10 @@ def encrypt_data(data, arc4_key): def prepare_opreturn_output(data, arc4_key=None): if len(data) + len(config.PREFIX) > config.OP_RETURN_MAX_SIZE: - raise exceptions.TransactionError("One `OP_RETURN` output per transaction.") + raise exceptions.TransactionError("One `OP_RETURN` output per transaction") opreturn_data = config.PREFIX + data if arc4_key: - opreturn_data = encrypt_data.encrypt(opreturn_data, arc4_key) + opreturn_data = encrypt_data(opreturn_data, arc4_key) return [CTxOut(0, CScript([OP_RETURN, opreturn_data]))] @@ -87,36 +110,32 @@ def data_to_pubkey_pairs(data, arc4_key=None): return pubkey_pairs -def prepare_multisig_output(data, pubkey, arc4_key=None): - try: - dust_return_pubkey = binascii.unhexlify(pubkey) - except binascii.Error: - raise script.InputError(f"Invalid pubkey key: {pubkey}") # noqa: B904 - if not script.is_fully_valid(dust_return_pubkey): - raise exceptions.ComposeError(f"invalid public key: {pubkey}") +def prepare_multisig_output(data, source, pubkeys, arc4_key=None): + source_pubkey = search_pubkey(source, pubkeys) + dust_return_pubkey = binascii.unhexlify(source_pubkey) pubkey_pairs = data_to_pubkey_pairs(data, arc4_key) outputs = [] for pubkey_pair in pubkey_pairs: output_script = CScript( [1, pubkey_pair[0], pubkey_pair[1], dust_return_pubkey, 3, OP_CHECKMULTISIG] ) - outputs.append(CTxOut(config.MULTISIG_DUST_SIZE, output_script)) + outputs.append(CTxOut(config.DEFAULT_MULTISIG_DUST_SIZE, output_script)) return outputs -def prepare_data_outputs(encoding, data, source, pubkey, arc4_key=None): +def prepare_data_outputs(encoding, data, source, pubkeys, arc4_key=None): data_encoding = determine_encoding(data, encoding) if data_encoding == "multisig": - return prepare_multisig_output(data, source, pubkey, arc4_key) + return prepare_multisig_output(data, source, pubkeys, arc4_key) if data_encoding == "opreturn": return prepare_opreturn_output(data, arc4_key) raise exceptions.TransactionError(f"Not supported encoding: {encoding}") -def prepare_outputs(source, destinations, data, pubkey, encoding, arc4_key=None): +def prepare_outputs(source, destinations, data, pubkeys, encoding, arc4_key=None): outputs = perpare_non_data_outputs(destinations) if data: - outputs += prepare_data_outputs(encoding, data, source, pubkey, arc4_key) + outputs += prepare_data_outputs(encoding, data, source, pubkeys, arc4_key) return outputs @@ -127,19 +146,19 @@ def prepare_unspent_list(inputs_set: str): raise exceptions.ComposeError( f"too many UTXOs in inputs_set (max. {MAX_INPUTS_SET}): {len(utxos_list)}" ) - for str_input in utxos_list: - if not util.is_utxo_format(str_input): - raise exceptions.ComposeError(f"invalid UTXO: {str_input}") + for utxo in utxos_list: + if not util.is_utxo_format(utxo): + raise exceptions.ComposeError(f"invalid UTXO: {utxo}") + txid, vout = utxo.split(":") + vout = int(vout) try: - value = backend.bitcoind.get_tx_out_value( - str_input.split(":")[0], int(str_input.split(":")[1]) - ) + value = backend.bitcoind.get_utxo_value(txid, vout) except Exception as e: - raise exceptions.ComposeError(f"invalid UTXO: {str_input}") from e + raise exceptions.ComposeError(f"invalid UTXO: {utxo}") from e unspent_list.append( { - "txid": str_input.split(":")[0], - "vout": int(str_input.split(":")[1]), + "txid": txid, + "vout": vout, "value": value, } ) @@ -154,13 +173,15 @@ def select_utxos(unspent_list, target_amount): selected_utxos.append(utxo) if total_amount >= target_amount: break + if total_amount < target_amount: + raise exceptions.ComposeError(f"Insufficient funds for the target amount: {target_amount}") return selected_utxos def utxos_to_txins(utxos: list): inputs = [] for utxo in utxos: - inputs.append(CTxIn(CScript([utxo["txid"], utxo["vout"]]))) + inputs.append(CTxIn(CScript([bytes.fromhex(utxo["txid"]), utxo["vout"]]))) return inputs @@ -184,7 +205,7 @@ def get_minimum_change(source): return config.REGULAR_DUST_SIZE -def prepare_transaction(source, outputs, unspent_list, desired_fee): +def prepare_transaction(source, outputs, pubkeys, unspent_list, desired_fee): outputs_total = sum(output["value"] for output in outputs) target_amount = outputs_total + desired_fee selected_utxos = select_utxos(unspent_list, target_amount) @@ -193,48 +214,50 @@ def prepare_transaction(source, outputs, unspent_list, desired_fee): change = input_total - target_amount change_outputs = [] if change > get_minimum_change(source): - change_outputs.append(CTxOut(change, get_script(source))) + change_outputs.append(CTxOut(change, get_script(source, pubkeys))) else: change = 0 return inputs, change_outputs, input_total -def construct_transaction(source, outputs, unspent_list, desired_fee): +def construct_transaction(source, outputs, pubkeys, unspent_list, desired_fee): inputs, change_outputs, _input_total = prepare_transaction( - source, outputs, unspent_list, desired_fee + source, outputs, pubkeys, unspent_list, desired_fee ) tx = CTransaction(inputs, outputs + change_outputs) return tx -def get_estimated_fee(source, outputs, unspent_list, satoshis_per_vbyte=None): +def get_estimated_fee(source, outputs, pubkeys, unspent_list, satoshis_per_vbyte=None): # calculate fee for a transaction with desired_fee = 0 - tx = construct_transaction(source, outputs, unspent_list, 0) + tx = construct_transaction(source, outputs, pubkeys, unspent_list, 0) return get_needed_fee(tx, satoshis_per_vbyte) def compose_transaction( - db, name, params, pubkey, inputs_set, encoding="auto", exact_fee=None, satoshis_per_vbyte=None + db, name, params, pubkeys, inputs_set, encoding="auto", exact_fee=None, satoshis_per_vbyte=None ): source, destinations, data = transaction.compose_data(db, name, params) unspent_list = prepare_unspent_list(inputs_set) # prepare non obfuscted outputs - clear_outputs = prepare_outputs(source, destinations, data, pubkey, encoding) + clear_outputs = prepare_outputs(source, destinations, data, pubkeys, encoding) if exact_fee: desired_fee = exact_fee else: # use non obfuscated outputs to calculate estimated fee... - desired_fee = get_estimated_fee(source, clear_outputs, unspent_list, satoshis_per_vbyte) + desired_fee = get_estimated_fee( + source, clear_outputs, pubkeys, unspent_list, satoshis_per_vbyte + ) # prepare transaction with desired fee and no-obfuscated outputs inputs, change_outputs, btc_in = prepare_transaction( - source, clear_outputs, unspent_list, desired_fee + source, clear_outputs, pubkeys, unspent_list, desired_fee ) # now we have inputs we can prepare obfuscated outputs outputs = prepare_outputs( - source, destinations, data, pubkey, encoding, arc4_key=inputs[0]["txid"] + source, destinations, data, pubkeys, encoding, arc4_key=inputs[0]["txid"] ) tx = CTransaction(inputs, outputs + change_outputs) btc_out = sum(output.nValue for output in outputs) diff --git a/counterparty-core/counterpartycore/test/conftest.py b/counterparty-core/counterpartycore/test/conftest.py index 0a853bdcdd..3b8e706395 100644 --- a/counterparty-core/counterpartycore/test/conftest.py +++ b/counterparty-core/counterpartycore/test/conftest.py @@ -595,6 +595,9 @@ def get_utxo_address_and_value(value): def get_transaction_fee(db, transaction_type, block_index): return 10 + def mocked_get_utxo_value(txid, vout): + return 999 + monkeypatch.setattr("counterpartycore.lib.transaction.arc4.init_arc4", init_arc4) monkeypatch.setattr( "counterpartycore.lib.backend.addrindexrs.get_unspent_txouts", get_unspent_txouts @@ -637,3 +640,7 @@ def get_transaction_fee(db, transaction_type, block_index): ) monkeypatch.setattr("counterpartycore.lib.gas.get_transaction_fee", get_transaction_fee) + + monkeypatch.setattr( + "counterpartycore.lib.backend.bitcoind.get_utxo_value", mocked_get_utxo_value + ) diff --git a/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py new file mode 100644 index 0000000000..2208443aa6 --- /dev/null +++ b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py @@ -0,0 +1,551 @@ +from bitcoin import SelectParams +from bitcoin.core import CTxOut +from bitcoin.core.script import OP_CHECKMULTISIG, OP_RETURN, CScript +from bitcoin.wallet import CBitcoinAddress, P2WPKHBitcoinAddress + +from counterpartycore.lib import config, exceptions + +from ..params import ( + ADDR, + DEFAULT_PARAMS, + MULTISIGADDR, + P2WPKH_ADDR, +) + +SelectParams("testnet") + +PROVIDED_PUBKEYS = [DEFAULT_PARAMS["pubkey"][ADDR[0]], DEFAULT_PARAMS["pubkey"][ADDR[1]]] +ARC4_KEY = "0000000000000000000000000000000000000000000000000000000000000000" +UTXO_1 = "344dcc8909ca3a137630726d0071dfd2df4f7c855bac150c7d3a8367835c90bc:1" +UTXO_2 = "4f0433ba841038e2e16328445930dd7bca35309b14b0da4451c8f94c631368b8:1" +UTXO_3 = "1fc2e5a57f584b2f2edd05676e75c33d03eed1d3098cc0550ea33474e3ec9db1:1" + +COMPOSER_VECTOR = { + "composer": { + "get_script": [ + { + "comment": "P2PKH address", + "in": (ADDR[0],), + "out": CBitcoinAddress(ADDR[0]).to_scriptPubKey(), + }, + { + "comment": "P2WPKH address", + "in": (P2WPKH_ADDR[0],), + "out": P2WPKHBitcoinAddress(P2WPKH_ADDR[0]).to_scriptPubKey(), + }, + { + "comment": "multisig address", + "in": (MULTISIGADDR[0], PROVIDED_PUBKEYS), + "out": CScript( + [ + 1, + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[1]]), + 2, + OP_CHECKMULTISIG, + ] + ), + }, + ], + "perpare_non_data_outputs": [ + { + "in": ([(ADDR[0], 0)],), + "out": [CTxOut(546, CBitcoinAddress(ADDR[0]).to_scriptPubKey())], + }, + { + "in": ([(MULTISIGADDR[0], 0)], PROVIDED_PUBKEYS), + "out": [ + CTxOut( + 1000, + CScript( + [ + 1, + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[1]]), + 2, + OP_CHECKMULTISIG, + ] + ), + ) + ], + }, + { + "in": ([(ADDR[0], 2024)],), + "out": [CTxOut(2024, CBitcoinAddress(ADDR[0]).to_scriptPubKey())], + }, + ], + "determine_encoding": [ + { + "in": (b"Hello, World!",), + "out": "opreturn", + }, + { + "in": (b"Hello, World!" * 100,), + "out": "multisig", + }, + { + "in": (b"Hello, World!", "p2sh"), + "error": (exceptions.TransactionError, "Not supported encoding: p2sh"), + }, + { + "in": (b"Hello, World!", "toto"), + "error": (exceptions.TransactionError, "Not supported encoding: toto"), + }, + ], + "encrypt_data": [ + { + "in": (b"Hello, World!", ARC4_KEY), + "out": b"\x96}\xe5-\xcc\x1b}m\xe5tr\x03v", + }, + ], + "prepare_opreturn_output": [ + { + "in": (b"Hello, World!", ARC4_KEY), + "out": [ + CTxOut( + 0, + CScript( + [ + OP_RETURN, + b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1", + ] + ), + ) + ], + }, + { + "in": (b"Hello, World!",), + "out": [CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"]))], + }, + ], + "data_to_pubkey_pairs": [ + { + "in": (b"Hello, World!" * 10,), + "out": [ + ( + b"\x025Hello, World!Hello, World!Hell\x9b", + b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", + ), + ( + b"\x025ello, World!Hello, World!Hello<", + b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", + ), + ( + b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", + b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", + ), + ], + }, + { + "in": (b"Hello, World!" * 10, ARC4_KEY), + "out": [ + ( + b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", + b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', + ), + ( + b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", + b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", + ), + ( + b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", + b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', + ), + ], + }, + ], + "prepare_multisig_output": [ + { + "comment": "No encryption", + "in": (b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), + "out": [ + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025Hello, World!Hello, World!Hell\x9b", + b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025ello, World!Hello, World!Hello<", + b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", + b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + ], + }, + { + "comment": "Encrypted", + "in": (b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS, ARC4_KEY), + "out": [ + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", + b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", + b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", + b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + ], + }, + ], + "prepare_data_outputs": [ + { + "in": ("opreturn", b"Hello, World!", ADDR[0], None), + "out": [CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"]))], + }, + { + "in": ("opreturn", b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), + "error": (exceptions.TransactionError, "One `OP_RETURN` output per transaction"), + }, + { + "in": ("multisig", b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), + "out": [ + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025Hello, World!Hello, World!Hell\x9b", + b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025ello, World!Hello, World!Hello<", + b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", + b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + ], + }, + { + "in": ("p2sh", b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), + "error": (exceptions.TransactionError, "Not supported encoding: p2sh"), + }, + ], + "prepare_outputs": [ + { + "in": (ADDR[0], [(ADDR[0], 9999)], b"Hello, World!", None, "opreturn"), + "out": [ + CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), + CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"])), + ], + }, + { + "in": (ADDR[0], [(ADDR[0], 9999)], b"Hello, World!", None, "opreturn", ARC4_KEY), + "out": [ + CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), + CTxOut( + 0, + CScript( + [ + OP_RETURN, + b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1", + ] + ), + ), + ], + }, + { + "in": ( + ADDR[0], + [(ADDR[0], 9999)], + b"Hello, World!" * 10, + PROVIDED_PUBKEYS, + "multisig", + ), + "out": [ + CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025Hello, World!Hello, World!Hell\x9b", + b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x025ello, World!Hello, World!Hello<", + b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", + b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + ], + }, + { + "in": ( + ADDR[0], + [(ADDR[0], 9999)], + b"Hello, World!" * 10, + PROVIDED_PUBKEYS, + "multisig", + ARC4_KEY, + ), + "out": [ + CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", + b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", + b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + CTxOut( + config.DEFAULT_MULTISIG_DUST_SIZE, + CScript( + [ + 1, + b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", + b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', + bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + 3, + OP_CHECKMULTISIG, + ] + ), + ), + ], + }, + ], + "prepare_unspent_list": [ + { + "in": (f"{UTXO_1},{UTXO_2},{UTXO_3}",), + "out": [ + {"txid": UTXO_1.split(":")[0], "vout": int(UTXO_1.split(":")[1]), "value": 999}, + {"txid": UTXO_2.split(":")[0], "vout": int(UTXO_2.split(":")[1]), "value": 999}, + {"txid": UTXO_3.split(":")[0], "vout": int(UTXO_3.split(":")[1]), "value": 999}, + ], + } + ], + "select_utxos": [ + { + "in": ( + [ + { + "txid": UTXO_1.split(":")[0], + "vout": int(UTXO_1.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_2.split(":")[0], + "vout": int(UTXO_2.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_3.split(":")[0], + "vout": int(UTXO_3.split(":")[1]), + "value": 999, + }, + ], + 500, + ), + "out": [ + {"txid": UTXO_1.split(":")[0], "vout": int(UTXO_1.split(":")[1]), "value": 999}, + ], + }, + { + "in": ( + [ + { + "txid": UTXO_1.split(":")[0], + "vout": int(UTXO_1.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_2.split(":")[0], + "vout": int(UTXO_2.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_3.split(":")[0], + "vout": int(UTXO_3.split(":")[1]), + "value": 999, + }, + ], + 1000, + ), + "out": [ + {"txid": UTXO_1.split(":")[0], "vout": int(UTXO_1.split(":")[1]), "value": 999}, + {"txid": UTXO_2.split(":")[0], "vout": int(UTXO_2.split(":")[1]), "value": 999}, + ], + }, + { + "in": ( + [ + { + "txid": UTXO_1.split(":")[0], + "vout": int(UTXO_1.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_2.split(":")[0], + "vout": int(UTXO_2.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_3.split(":")[0], + "vout": int(UTXO_3.split(":")[1]), + "value": 999, + }, + ], + 2000, + ), + "out": [ + {"txid": UTXO_1.split(":")[0], "vout": int(UTXO_1.split(":")[1]), "value": 999}, + {"txid": UTXO_2.split(":")[0], "vout": int(UTXO_2.split(":")[1]), "value": 999}, + {"txid": UTXO_3.split(":")[0], "vout": int(UTXO_3.split(":")[1]), "value": 999}, + ], + }, + { + "in": ( + [ + { + "txid": UTXO_1.split(":")[0], + "vout": int(UTXO_1.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_2.split(":")[0], + "vout": int(UTXO_2.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_3.split(":")[0], + "vout": int(UTXO_3.split(":")[1]), + "value": 999, + }, + ], + 3000, + ), + "error": ( + exceptions.ComposeError, + "Insufficient funds for the target amount: 3000", + ), + }, + ], + } +} diff --git a/counterparty-core/counterpartycore/test/fixtures/vectors.py b/counterparty-core/counterpartycore/test/fixtures/vectors.py index 87bc73d499..8f023bbc22 100644 --- a/counterparty-core/counterpartycore/test/fixtures/vectors.py +++ b/counterparty-core/counterpartycore/test/fixtures/vectors.py @@ -17,6 +17,7 @@ from counterpartycore.lib.messages import issuance from counterpartycore.lib.util import RPCError +from .contract_vectors.composer import COMPOSER_VECTOR from .contract_vectors.dispenser import DISPENSER_VECTOR from .contract_vectors.fairmint import FAIRMINT_VECTOR from .contract_vectors.fairminter import FAIRMINTER_VECTOR @@ -36,9 +37,9 @@ DEFAULT_PARAMS as DP, ) -UNITTEST_VECTOR_ = TRANSACTION_VECTOR +UNITTEST_VECTOR = COMPOSER_VECTOR -UNITTEST_VECTOR = ( +UNITTEST_VECTOR_ = ( FAIRMINTER_VECTOR | FAIRMINT_VECTOR | LEDGER_VECTOR @@ -47,6 +48,7 @@ | DISPENSER_VECTOR | GAS_VECTOR | TRANSACTION_VECTOR + | COMPOSER_VECTOR | { "bet": { "validate": [ diff --git a/counterparty-core/counterpartycore/test/util_test.py b/counterparty-core/counterpartycore/test/util_test.py index 5523f5c634..ce10ad16d8 100644 --- a/counterparty-core/counterpartycore/test/util_test.py +++ b/counterparty-core/counterpartycore/test/util_test.py @@ -820,6 +820,7 @@ def exec_tested_method(tx_name, method, tested_method, inputs, server_db): "message_type", "address", ] + or (tx_name == "composer" and method != "compose_transaction") or ( tx_name in [ From 106405cedf205be01de263e523cfcc6a72adc325 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Thu, 3 Oct 2024 13:21:06 +0000 Subject: [PATCH 3/7] fix test --- counterparty-core/counterpartycore/test/util_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/counterparty-core/counterpartycore/test/util_test.py b/counterparty-core/counterpartycore/test/util_test.py index ce10ad16d8..bbd27a672e 100644 --- a/counterparty-core/counterpartycore/test/util_test.py +++ b/counterparty-core/counterpartycore/test/util_test.py @@ -34,6 +34,7 @@ backend, blocks, check, + composer, # noqa config, database, deserialize, From 32a168dc604f99b754029e397245b9406c98bc98 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Thu, 3 Oct 2024 13:24:20 +0000 Subject: [PATCH 4/7] tweaks --- counterparty-core/counterpartycore/lib/composer.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/counterparty-core/counterpartycore/lib/composer.py b/counterparty-core/counterpartycore/lib/composer.py index a459912772..88e85727db 100644 --- a/counterparty-core/counterpartycore/lib/composer.py +++ b/counterparty-core/counterpartycore/lib/composer.py @@ -44,17 +44,15 @@ def get_script(address, pubkeys=None): pubkeys = [search_pubkey(address, pubkeys) for address in addresses] pubkeys = [bytes.fromhex(pubkey) for pubkey in pubkeys] return CScript([signatures_required] + pubkeys + [signatures_possible] + [OP_CHECKMULTISIG]) - elif script.is_bech32(address): + if script.is_bech32(address): return P2WPKHBitcoinAddress(address).to_scriptPubKey() - else: - return CBitcoinAddress(address).to_scriptPubKey() + return CBitcoinAddress(address).to_scriptPubKey() def get_default_value(address): if script.is_multisig(address): return config.DEFAULT_MULTISIG_DUST_SIZE - else: - return config.DEFAULT_REGULAR_DUST_SIZE + return config.DEFAULT_REGULAR_DUST_SIZE def perpare_non_data_outputs(destinations, pubkeys=None): @@ -194,15 +192,13 @@ def get_needed_fee(tx, satoshis_per_vbyte=None): virtual_size = get_virtual_size(weight) if satoshis_per_vbyte: return satoshis_per_vbyte * virtual_size - else: - return backend.bitcoind.satoshis_per_vbyte() * virtual_size + return backend.bitcoind.satoshis_per_vbyte() * virtual_size def get_minimum_change(source): if script.is_multisig(source): return config.MULTISIG_DUST_SIZE - else: - return config.REGULAR_DUST_SIZE + return config.REGULAR_DUST_SIZE def prepare_transaction(source, outputs, pubkeys, unspent_list, desired_fee): From 5a8ea0af2175a02a3357a7c8a207fd1f548839ea Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Thu, 3 Oct 2024 14:41:07 +0000 Subject: [PATCH 5/7] Migrate to 'bitcoinutils' which supports taproot addresses --- .../counterpartycore/lib/composer.py | 59 ++-- .../fixtures/contract_vectors/composer.py | 273 +++++++++--------- .../counterpartycore/test/util_test.py | 9 +- counterparty-core/requirements.txt | 1 + 4 files changed, 172 insertions(+), 170 deletions(-) diff --git a/counterparty-core/counterpartycore/lib/composer.py b/counterparty-core/counterpartycore/lib/composer.py index 88e85727db..35e05c7798 100644 --- a/counterparty-core/counterpartycore/lib/composer.py +++ b/counterparty-core/counterpartycore/lib/composer.py @@ -1,21 +1,8 @@ import binascii -from bitcoin.core import ( - CTransaction, - CTxIn, - CTxOut, -) -from bitcoin.core.script import ( - OP_CHECKMULTISIG, - OP_RETURN, - CScript, -) -from bitcoin.wallet import ( - CBitcoinAddress, - CBitcoinAddressError, - P2PKHBitcoinAddress, - P2WPKHBitcoinAddress, -) +from bitcoinutils.keys import P2pkhAddress, P2wpkhAddress, PublicKey +from bitcoinutils.script import Script, b_to_h +from bitcoinutils.transactions import Transaction, TxInput, TxOutput from counterpartycore.lib import arc4, backend, config, exceptions, script, transaction, util from counterpartycore.lib.transaction_helper.common_serializer import make_fully_valid @@ -31,9 +18,15 @@ def search_pubkey(address, provides_pubkeys=None): try: if not pubkey: raise exceptions.ComposeError(f"invalid pubkey: {pubkey}") - if str(P2PKHBitcoinAddress.from_pubkey(bytes.fromhex(pubkey))) == address: + check_address = PublicKey.from_hex(pubkey).get_address(compressed=True).to_string() + print(check_address) + if check_address == address: return pubkey - except (ValueError, CBitcoinAddressError) as e: + check_address = PublicKey.from_hex(pubkey).get_address(compressed=False).to_string() + print(check_address) + if check_address == address: + return pubkey + except ValueError as e: raise exceptions.ComposeError(f"invalid pubkey: {pubkey}") from e raise exceptions.ComposeError(f"`{address}` pubkey not found in provided pubkeys") @@ -42,11 +35,12 @@ def get_script(address, pubkeys=None): if script.is_multisig(address): signatures_required, addresses, signatures_possible = script.extract_array(address) pubkeys = [search_pubkey(address, pubkeys) for address in addresses] - pubkeys = [bytes.fromhex(pubkey) for pubkey in pubkeys] - return CScript([signatures_required] + pubkeys + [signatures_possible] + [OP_CHECKMULTISIG]) + return Script( + [signatures_required] + pubkeys + [signatures_possible] + ["OP_CHECKMULTISIG"] + ) if script.is_bech32(address): - return P2WPKHBitcoinAddress(address).to_scriptPubKey() - return CBitcoinAddress(address).to_scriptPubKey() + return P2wpkhAddress(address).to_script_pub_key() + return P2pkhAddress(address).to_script_pub_key() def get_default_value(address): @@ -59,7 +53,7 @@ def perpare_non_data_outputs(destinations, pubkeys=None): outputs = [] for address, value in destinations: output_value = value or get_default_value(address) - outputs.append(CTxOut(output_value, get_script(address, pubkeys))) + outputs.append(TxOutput(output_value, get_script(address, pubkeys))) return outputs @@ -86,7 +80,7 @@ def prepare_opreturn_output(data, arc4_key=None): opreturn_data = config.PREFIX + data if arc4_key: opreturn_data = encrypt_data(opreturn_data, arc4_key) - return [CTxOut(0, CScript([OP_RETURN, opreturn_data]))] + return [TxOutput(0, Script(["OP_RETURN", b_to_h(opreturn_data)]))] def data_to_pubkey_pairs(data, arc4_key=None): @@ -104,20 +98,19 @@ def data_to_pubkey_pairs(data, arc4_key=None): output_data = encrypt_data(output_data, arc4_key) data_pubkey_1 = make_fully_valid(output_data[:31]) data_pubkey_2 = make_fully_valid(output_data[31:]) - pubkey_pairs.append((data_pubkey_1, data_pubkey_2)) + pubkey_pairs.append((b_to_h(data_pubkey_1), b_to_h(data_pubkey_2))) return pubkey_pairs def prepare_multisig_output(data, source, pubkeys, arc4_key=None): source_pubkey = search_pubkey(source, pubkeys) - dust_return_pubkey = binascii.unhexlify(source_pubkey) pubkey_pairs = data_to_pubkey_pairs(data, arc4_key) outputs = [] for pubkey_pair in pubkey_pairs: - output_script = CScript( - [1, pubkey_pair[0], pubkey_pair[1], dust_return_pubkey, 3, OP_CHECKMULTISIG] + output_script = Script( + [1, pubkey_pair[0], pubkey_pair[1], source_pubkey, 3, "OP_CHECKMULTISIG"] ) - outputs.append(CTxOut(config.DEFAULT_MULTISIG_DUST_SIZE, output_script)) + outputs.append(TxOutput(config.DEFAULT_MULTISIG_DUST_SIZE, output_script)) return outputs @@ -179,7 +172,7 @@ def select_utxos(unspent_list, target_amount): def utxos_to_txins(utxos: list): inputs = [] for utxo in utxos: - inputs.append(CTxIn(CScript([bytes.fromhex(utxo["txid"]), utxo["vout"]]))) + inputs.append(TxInput(utxo["txid"], utxo["vout"])) return inputs @@ -210,7 +203,7 @@ def prepare_transaction(source, outputs, pubkeys, unspent_list, desired_fee): change = input_total - target_amount change_outputs = [] if change > get_minimum_change(source): - change_outputs.append(CTxOut(change, get_script(source, pubkeys))) + change_outputs.append(TxOutput(change, get_script(source, pubkeys))) else: change = 0 return inputs, change_outputs, input_total @@ -220,7 +213,7 @@ def construct_transaction(source, outputs, pubkeys, unspent_list, desired_fee): inputs, change_outputs, _input_total = prepare_transaction( source, outputs, pubkeys, unspent_list, desired_fee ) - tx = CTransaction(inputs, outputs + change_outputs) + tx = Transaction(inputs, outputs + change_outputs) return tx @@ -255,7 +248,7 @@ def compose_transaction( outputs = prepare_outputs( source, destinations, data, pubkeys, encoding, arc4_key=inputs[0]["txid"] ) - tx = CTransaction(inputs, outputs + change_outputs) + tx = Transaction(inputs, outputs + change_outputs) btc_out = sum(output.nValue for output in outputs) btc_change = sum(change_output.nValue for change_output in change_outputs) diff --git a/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py index 2208443aa6..6845d78fa2 100644 --- a/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py +++ b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py @@ -1,7 +1,6 @@ -from bitcoin import SelectParams -from bitcoin.core import CTxOut -from bitcoin.core.script import OP_CHECKMULTISIG, OP_RETURN, CScript -from bitcoin.wallet import CBitcoinAddress, P2WPKHBitcoinAddress +from bitcoinutils.keys import P2pkhAddress, P2wpkhAddress +from bitcoinutils.script import Script, b_to_h +from bitcoinutils.transactions import TxOutput from counterpartycore.lib import config, exceptions @@ -12,8 +11,6 @@ P2WPKH_ADDR, ) -SelectParams("testnet") - PROVIDED_PUBKEYS = [DEFAULT_PARAMS["pubkey"][ADDR[0]], DEFAULT_PARAMS["pubkey"][ADDR[1]]] ARC4_KEY = "0000000000000000000000000000000000000000000000000000000000000000" UTXO_1 = "344dcc8909ca3a137630726d0071dfd2df4f7c855bac150c7d3a8367835c90bc:1" @@ -26,23 +23,23 @@ { "comment": "P2PKH address", "in": (ADDR[0],), - "out": CBitcoinAddress(ADDR[0]).to_scriptPubKey(), + "out": P2pkhAddress(ADDR[0]).to_script_pub_key(), }, { "comment": "P2WPKH address", "in": (P2WPKH_ADDR[0],), - "out": P2WPKHBitcoinAddress(P2WPKH_ADDR[0]).to_scriptPubKey(), + "out": P2wpkhAddress(P2WPKH_ADDR[0]).to_script_pub_key(), }, { "comment": "multisig address", "in": (MULTISIGADDR[0], PROVIDED_PUBKEYS), - "out": CScript( + "out": Script( [ 1, - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[1]]), + DEFAULT_PARAMS["pubkey"][ADDR[0]], + DEFAULT_PARAMS["pubkey"][ADDR[1]], 2, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), }, @@ -50,20 +47,20 @@ "perpare_non_data_outputs": [ { "in": ([(ADDR[0], 0)],), - "out": [CTxOut(546, CBitcoinAddress(ADDR[0]).to_scriptPubKey())], + "out": [TxOutput(546, P2pkhAddress(ADDR[0]).to_script_pub_key())], }, { "in": ([(MULTISIGADDR[0], 0)], PROVIDED_PUBKEYS), "out": [ - CTxOut( + TxOutput( 1000, - CScript( + Script( [ 1, - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[1]]), + DEFAULT_PARAMS["pubkey"][ADDR[0]], + DEFAULT_PARAMS["pubkey"][ADDR[1]], 2, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ) @@ -71,7 +68,7 @@ }, { "in": ([(ADDR[0], 2024)],), - "out": [CTxOut(2024, CBitcoinAddress(ADDR[0]).to_scriptPubKey())], + "out": [TxOutput(2024, P2pkhAddress(ADDR[0]).to_script_pub_key())], }, ], "determine_encoding": [ @@ -102,12 +99,14 @@ { "in": (b"Hello, World!", ARC4_KEY), "out": [ - CTxOut( + TxOutput( 0, - CScript( + Script( [ - OP_RETURN, - b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1", + "OP_RETURN", + b_to_h( + b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1" + ), ] ), ) @@ -115,7 +114,7 @@ }, { "in": (b"Hello, World!",), - "out": [CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"]))], + "out": [TxOutput(0, Script(["OP_RETURN", b_to_h(b"TESTXXXXHello, World!")]))], }, ], "data_to_pubkey_pairs": [ @@ -123,16 +122,16 @@ "in": (b"Hello, World!" * 10,), "out": [ ( - b"\x025Hello, World!Hello, World!Hell\x9b", - b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", ), ( - b"\x025ello, World!Hello, World!Hello<", - b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", ), ( - b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", - b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", ), ], }, @@ -140,16 +139,16 @@ "in": (b"Hello, World!" * 10, ARC4_KEY), "out": [ ( - b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", - b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', + "03eb50ec2dcf58711add696c0b334fda08ab76108fd09b84e5294f6c7a42fa3333", + "02f184ec22681ff3dde4091fc9a75fd0024ee446f4499a2a9ec04b4f8b05a071da", ), ( - b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", - b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", + "02eb7de52dcc1b7d6de57472037626f701ab755383a7a399fb210a05574bfa300d", + "02b2889b1a7501fb988d2416c9a41cdc7576f958fc0cf3079ec04b4f8b05a071a2", ), ( - b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", - b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', + "03c674e52e8f170a55f86a7a461f0bfe01a8365ff49fbe87f3642b4d3227965f81", + "029ea8cc75076d9fb9c5417aa5cb30fc22198b34982dbb629ec04b4f8b05a07134", ), ], }, @@ -159,42 +158,42 @@ "comment": "No encryption", "in": (b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), "out": [ - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025Hello, World!Hello, World!Hell\x9b", - b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025ello, World!Hello, World!Hello<", - b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", - b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), @@ -204,42 +203,42 @@ "comment": "Encrypted", "in": (b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS, ARC4_KEY), "out": [ - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", - b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "03eb50ec2dcf58711add696c0b334fda08ab76108fd09b84e5294f6c7a42fa3333", + "02f184ec22681ff3dde4091fc9a75fd0024ee446f4499a2a9ec04b4f8b05a071da", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", - b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "02eb7de52dcc1b7d6de57472037626f701ab755383a7a399fb210a05574bfa300d", + "02b2889b1a7501fb988d2416c9a41cdc7576f958fc0cf3079ec04b4f8b05a071a2", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", - b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "03c674e52e8f170a55f86a7a461f0bfe01a8365ff49fbe87f3642b4d3227965f81", + "029ea8cc75076d9fb9c5417aa5cb30fc22198b34982dbb629ec04b4f8b05a07134", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), @@ -249,7 +248,7 @@ "prepare_data_outputs": [ { "in": ("opreturn", b"Hello, World!", ADDR[0], None), - "out": [CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"]))], + "out": [TxOutput(0, Script(["OP_RETURN", b_to_h(b"TESTXXXXHello, World!")]))], }, { "in": ("opreturn", b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), @@ -258,42 +257,42 @@ { "in": ("multisig", b"Hello, World!" * 10, ADDR[0], PROVIDED_PUBKEYS), "out": [ - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025Hello, World!Hello, World!Hell\x9b", - b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025ello, World!Hello, World!Hello<", - b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", - b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), @@ -308,20 +307,22 @@ { "in": (ADDR[0], [(ADDR[0], 9999)], b"Hello, World!", None, "opreturn"), "out": [ - CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), - CTxOut(0, CScript([OP_RETURN, b"TESTXXXXHello, World!"])), + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput(0, Script(["OP_RETURN", b_to_h(b"TESTXXXXHello, World!")])), ], }, { "in": (ADDR[0], [(ADDR[0], 9999)], b"Hello, World!", None, "opreturn", ARC4_KEY), "out": [ - CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), - CTxOut( + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput( 0, - CScript( + Script( [ - OP_RETURN, - b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1", + "OP_RETURN", + b_to_h( + b"\x8a]\xda\x15\xfbo\x05b\xc2cr\x0b8B\xb2:\xa8h\x13\xc7\xd1" + ), ] ), ), @@ -336,43 +337,43 @@ "multisig", ), "out": [ - CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), - CTxOut( + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025Hello, World!Hello, World!Hell\x9b", - b"\x03o, World!Hello, World!H\x00\x00\x00\x00\x00\x00\x00\x00\x94", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x025ello, World!Hello, World!Hello<", - b"\x03, World!Hello, World!He\x00\x00\x00\x00\x00\x00\x00\x00\t", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x02\x18llo, World!Hello, World!\x00\x00\x00\x00\x00\x00G", - b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), @@ -388,43 +389,43 @@ ARC4_KEY, ), "out": [ - CTxOut(9999, CBitcoinAddress(ADDR[0]).to_scriptPubKey()), - CTxOut( + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x03\xebP\xec-\xcfXq\x1a\xddil\x0b3O\xda\x08\xabv\x10\x8f\xd0\x9b\x84\xe5)OlzB\xfa33", - b'\x02\xf1\x84\xec"h\x1f\xf3\xdd\xe4\t\x1f\xc9\xa7_\xd0\x02N\xe4F\xf4I\x9a*\x9e\xc0KO\x8b\x05\xa0q\xda', - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "03eb50ec2dcf58711add696c0b334fda08ab76108fd09b84e5294f6c7a42fa3333", + "02f184ec22681ff3dde4091fc9a75fd0024ee446f4499a2a9ec04b4f8b05a071da", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x02\xeb}\xe5-\xcc\x1b}m\xe5tr\x03v&\xf7\x01\xabuS\x83\xa7\xa3\x99\xfb!\n\x05WK\xfa0\r", - b"\x02\xb2\x88\x9b\x1au\x01\xfb\x98\x8d$\x16\xc9\xa4\x1c\xdcuv\xf9X\xfc\x0c\xf3\x07\x9e\xc0KO\x8b\x05\xa0q\xa2", - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "02eb7de52dcc1b7d6de57472037626f701ab755383a7a399fb210a05574bfa300d", + "02b2889b1a7501fb988d2416c9a41cdc7576f958fc0cf3079ec04b4f8b05a071a2", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), - CTxOut( + TxOutput( config.DEFAULT_MULTISIG_DUST_SIZE, - CScript( + Script( [ 1, - b"\x03\xc6t\xe5.\x8f\x17\nU\xf8jzF\x1f\x0b\xfe\x01\xa86_\xf4\x9f\xbe\x87\xf3d+M2'\x96_\x81", - b'\x02\x9e\xa8\xccu\x07m\x9f\xb9\xc5Az\xa5\xcb0\xfc"\x19\x8b4\x98-\xbbb\x9e\xc0KO\x8b\x05\xa0q4', - bytes.fromhex(DEFAULT_PARAMS["pubkey"][ADDR[0]]), + "03c674e52e8f170a55f86a7a461f0bfe01a8365ff49fbe87f3642b4d3227965f81", + "029ea8cc75076d9fb9c5417aa5cb30fc22198b34982dbb629ec04b4f8b05a07134", + DEFAULT_PARAMS["pubkey"][ADDR[0]], 3, - OP_CHECKMULTISIG, + "OP_CHECKMULTISIG", ] ), ), diff --git a/counterparty-core/counterpartycore/test/util_test.py b/counterparty-core/counterpartycore/test/util_test.py index bbd27a672e..5edbb48c7f 100644 --- a/counterparty-core/counterpartycore/test/util_test.py +++ b/counterparty-core/counterpartycore/test/util_test.py @@ -22,6 +22,7 @@ import bitcoin as bitcoinlib import pycoin import pytest +from bitcoinutils.transactions import TxOutput from pycoin.coins.bitcoin import Tx # noqa: F401 CURR_DIR = os.path.dirname( @@ -904,7 +905,13 @@ def check_outputs( if outputs is not None: try: - assert outputs == test_outputs + if isinstance(outputs, TxOutput): + assert outputs.to_bytes() == test_outputs.to_bytes() + elif isinstance(outputs, list) and isinstance(outputs[0], TxOutput): + for i, output in enumerate(outputs): + assert output.to_bytes() == test_outputs[i].to_bytes() + else: + assert outputs == test_outputs except AssertionError: if pytest_config.getoption("verbose") >= 2: msg = ( diff --git a/counterparty-core/requirements.txt b/counterparty-core/requirements.txt index 80452ba258..5011d37452 100644 --- a/counterparty-core/requirements.txt +++ b/counterparty-core/requirements.txt @@ -32,4 +32,5 @@ python-gnupg==0.5.2 pyzmq==26.0.3 JSON-log-formatter==1.0 yoyo-migrations==8.2.0 +bitcoin-utils==0.7.1 counterparty-rs==10.4.2 From 45fa9fc420f1db3ed7d2264bab134507ecdc670a Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Thu, 3 Oct 2024 14:54:35 +0000 Subject: [PATCH 6/7] migrate get vsize --- counterparty-core/counterpartycore/lib/composer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/counterparty-core/counterpartycore/lib/composer.py b/counterparty-core/counterpartycore/lib/composer.py index 35e05c7798..9b0a263b95 100644 --- a/counterparty-core/counterpartycore/lib/composer.py +++ b/counterparty-core/counterpartycore/lib/composer.py @@ -176,13 +176,8 @@ def utxos_to_txins(utxos: list): return inputs -def get_virtual_size(weight): - return (weight + 3) // 4 - - def get_needed_fee(tx, satoshis_per_vbyte=None): - weight = tx.calc_weight() - virtual_size = get_virtual_size(weight) + virtual_size = tx.get_vsize() if satoshis_per_vbyte: return satoshis_per_vbyte * virtual_size return backend.bitcoind.satoshis_per_vbyte() * virtual_size From 59ab8abe57b88b6862eafe2dcc1a9be500be8a37 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Thu, 3 Oct 2024 15:22:34 +0000 Subject: [PATCH 7/7] more unit tests --- .../counterpartycore/test/conftest.py | 6 + .../fixtures/contract_vectors/composer.py | 141 +++++++++++++++++- .../counterpartycore/test/util_test.py | 6 +- 3 files changed, 149 insertions(+), 4 deletions(-) diff --git a/counterparty-core/counterpartycore/test/conftest.py b/counterparty-core/counterpartycore/test/conftest.py index 34d8d08c94..3dc3c05ce3 100644 --- a/counterparty-core/counterpartycore/test/conftest.py +++ b/counterparty-core/counterpartycore/test/conftest.py @@ -598,6 +598,9 @@ def get_transaction_fee(db, transaction_type, block_index): def mocked_get_utxo_value(txid, vout): return 999 + def satoshis_per_vbyte(): + return 3 + def determine_encoding( data, desired_encoding="auto", op_return_max_size=config.OP_RETURN_MAX_SIZE ): @@ -661,3 +664,6 @@ def determine_encoding( "counterpartycore.lib.backend.bitcoind.get_utxo_value", mocked_get_utxo_value ) monkeypatch.setattr("counterpartycore.lib.transaction.determine_encoding", determine_encoding) + monkeypatch.setattr( + "counterpartycore.lib.backend.bitcoind.satoshis_per_vbyte", satoshis_per_vbyte + ) diff --git a/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py index 6845d78fa2..f945060631 100644 --- a/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py +++ b/counterparty-core/counterpartycore/test/fixtures/contract_vectors/composer.py @@ -1,6 +1,6 @@ from bitcoinutils.keys import P2pkhAddress, P2wpkhAddress from bitcoinutils.script import Script, b_to_h -from bitcoinutils.transactions import TxOutput +from bitcoinutils.transactions import Transaction, TxInput, TxOutput from counterpartycore.lib import config, exceptions @@ -548,5 +548,144 @@ ), }, ], + "utxos_to_txins": [ + { + "in": ( + [ + { + "txid": UTXO_1.split(":")[0], + "vout": int(UTXO_1.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_2.split(":")[0], + "vout": int(UTXO_2.split(":")[1]), + "value": 999, + }, + { + "txid": UTXO_3.split(":")[0], + "vout": int(UTXO_3.split(":")[1]), + "value": 999, + }, + ], + ), + "out": [ + TxInput(UTXO_1.split(":")[0], int(UTXO_1.split(":")[1])), + TxInput(UTXO_2.split(":")[0], int(UTXO_2.split(":")[1])), + TxInput(UTXO_3.split(":")[0], int(UTXO_3.split(":")[1])), + ], + } + ], + "get_needed_fee": [ + { + "in": ( + Transaction( + [ + TxInput(UTXO_1.split(":")[0], int(UTXO_1.split(":")[1])), + TxInput(UTXO_2.split(":")[0], int(UTXO_2.split(":")[1])), + TxInput(UTXO_3.split(":")[0], int(UTXO_3.split(":")[1])), + ], + [ + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + ], + ), + ), + "out": 1527, + }, + { + "in": ( + Transaction( + [ + TxInput(UTXO_1.split(":")[0], int(UTXO_1.split(":")[1])), + TxInput(UTXO_2.split(":")[0], int(UTXO_2.split(":")[1])), + TxInput(UTXO_3.split(":")[0], int(UTXO_3.split(":")[1])), + ], + [ + TxOutput(9999, P2pkhAddress(ADDR[0]).to_script_pub_key()), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "023548656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c9b", + "036f2c20576f726c642148656c6c6f2c20576f726c642148000000000000000094", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "0235656c6c6f2c20576f726c642148656c6c6f2c20576f726c642148656c6c6f3c", + "032c20576f726c642148656c6c6f2c20576f726c64214865000000000000000009", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + TxOutput( + config.DEFAULT_MULTISIG_DUST_SIZE, + Script( + [ + 1, + "02186c6c6f2c20576f726c642148656c6c6f2c20576f726c642100000000000047", + "03000000000000000000000000000000000000000000000000000000000000000c", + DEFAULT_PARAMS["pubkey"][ADDR[0]], + 3, + "OP_CHECKMULTISIG", + ] + ), + ), + ], + ), + 6, + ), + "out": 3054, + }, + ], } } diff --git a/counterparty-core/counterpartycore/test/util_test.py b/counterparty-core/counterpartycore/test/util_test.py index 5edbb48c7f..888a24449a 100644 --- a/counterparty-core/counterpartycore/test/util_test.py +++ b/counterparty-core/counterpartycore/test/util_test.py @@ -22,7 +22,7 @@ import bitcoin as bitcoinlib import pycoin import pytest -from bitcoinutils.transactions import TxOutput +from bitcoinutils.transactions import TxInput, TxOutput from pycoin.coins.bitcoin import Tx # noqa: F401 CURR_DIR = os.path.dirname( @@ -905,9 +905,9 @@ def check_outputs( if outputs is not None: try: - if isinstance(outputs, TxOutput): + if isinstance(outputs, (TxOutput, TxInput)): assert outputs.to_bytes() == test_outputs.to_bytes() - elif isinstance(outputs, list) and isinstance(outputs[0], TxOutput): + elif isinstance(outputs, list) and isinstance(outputs[0], (TxOutput, TxInput)): for i, output in enumerate(outputs): assert output.to_bytes() == test_outputs[i].to_bytes() else: