From 3eb888bc26b2900e7c38ec97dc65a04b53206363 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Tue, 1 Oct 2024 18:02:04 +0000 Subject: [PATCH 1/3] test p2sh encoding with regtest --- .../counterpartycore/lib/transaction.py | 1 + .../test/regtest/regtestnode.py | 1 + .../counterpartycore/test/regtest/testp2sh.py | 179 ++++++++++++++++++ counterparty-core/tools/xcpcli.py | 7 +- 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 counterparty-core/counterpartycore/test/regtest/testp2sh.py diff --git a/counterparty-core/counterpartycore/lib/transaction.py b/counterparty-core/counterpartycore/lib/transaction.py index 0376da8d07..0814fd6949 100644 --- a/counterparty-core/counterpartycore/lib/transaction.py +++ b/counterparty-core/counterpartycore/lib/transaction.py @@ -102,6 +102,7 @@ def check_transaction_sanity(db, source, tx_info, unsigned_tx_hex, encoding, inp if encoding == "p2sh": # make_canonical can't determine the address, so we blindly change the desired to the parsed desired_source = parsed_source + # desired_destination = parsed_destination except exceptions.BTCOnlyError: # Skip BTC‐only transactions. return diff --git a/counterparty-core/counterpartycore/test/regtest/regtestnode.py b/counterparty-core/counterpartycore/test/regtest/regtestnode.py index f37bd95761..bf9d59da4c 100644 --- a/counterparty-core/counterpartycore/test/regtest/regtestnode.py +++ b/counterparty-core/counterpartycore/test/regtest/regtestnode.py @@ -196,6 +196,7 @@ def start(self): "-zmqpubsequence=tcp://0.0.0.0:29332", "-zmqpubrawblock=tcp://0.0.0.0:29333", "-fallbackfee=0.0002", + "-acceptnonstdtxn", f"-datadir={self.datadir}", _bg=True, _out=sys.stdout, diff --git a/counterparty-core/counterpartycore/test/regtest/testp2sh.py b/counterparty-core/counterpartycore/test/regtest/testp2sh.py new file mode 100644 index 0000000000..2e29d7a3aa --- /dev/null +++ b/counterparty-core/counterpartycore/test/regtest/testp2sh.py @@ -0,0 +1,179 @@ +import hashlib +import json +import sys +import time +import urllib.parse + +import sh +from bitcoin import SelectParams +from bitcoin.core import ( + CMutableTransaction, + CScriptWitness, + CTxIn, + CTxInWitness, + CTxWitness, + Hash160, + b2lx, +) +from bitcoin.core.script import OP_0, SIGHASH_ALL, SIGVERSION_WITNESS_V0, CScript, SignatureHash +from bitcoin.wallet import CBitcoinSecret, P2WPKHBitcoinAddress + +SelectParams("regtest") + +SERVER = "http://localhost:24000/v2/" + + +def api_call(route, params=None): + params = params or {} + params_in_url = [] + for key, value in params.items(): + if f"<{key}>" in route: + route = route.replace(f"<{key}>", value) + params_in_url.append(key) + for key in params_in_url: + del params[key] + query_string = urllib.parse.urlencode(params) + url = f"{SERVER}{route}?{query_string}" + return json.loads(sh.curl(url).strip()) + + +def bake_bitcoin_clients(): + bitcoin_cli = sh.bitcoin_cli.bake( + "-rpcuser=rpc", + "-rpcpassword=rpc", + "-rpcconnect=localhost", + "-regtest", + ) + return bitcoin_cli + + +def get_tx_out_amount(tx_hash, vout): + raw_tx = json.loads(bitcoin_cli("getrawtransaction", tx_hash, 1).strip()) + return raw_tx["vout"][vout]["value"] + + +def get_new_address(seed: str): + secret_key = CBitcoinSecret.from_secret_bytes(hashlib.sha256(bytes(seed, "utf-8")).digest()) + public_key = secret_key.pub + scriptPubKey = CScript([OP_0, Hash160(public_key)]) + address = P2WPKHBitcoinAddress.from_scriptPubKey(scriptPubKey) + print( + { + "address": str(address), + "secret_key": str(secret_key), + "public_key": public_key.hex(), + } + ) + return address, secret_key + + +def sign_rawtransaction(rawtransaction, address, secret_key, amount=None): + tx = CMutableTransaction.deserialize(bytes.fromhex(rawtransaction)) + txin_index = 0 + redeem_script = address.to_redeemScript() + prev_txid = b2lx(tx.vin[txin_index].prevout.hash) + if not amount: + amount = get_tx_out_amount(prev_txid, tx.vin[txin_index].prevout.n) + amount = int(amount * 10e8) + # amount = int(10 * 10e8) + print("AMount", amount) + sighash = SignatureHash( + redeem_script, tx, txin_index, SIGHASH_ALL, amount=amount, sigversion=SIGVERSION_WITNESS_V0 + ) + signature = secret_key.sign(sighash) + bytes([SIGHASH_ALL]) + witness = [signature, secret_key.pub] + ctxinwitnesses = [CTxInWitness(CScriptWitness(witness))] + # clean scriptSig + vins = [CTxIn(tx.vin[0].prevout, CScript())] + signed_tx = CMutableTransaction(vins, tx.vout) + signed_tx.wit = CTxWitness(ctxinwitnesses) + return signed_tx.serialize().hex() + + +def send_funds_to_address(address): + source_with_xcp = api_call("assets/XCP/balances", {"limit": 1})["result"][0]["address"] + sh.python3( + "tools/xcpcli.py", + "send_send", + "--address", + source_with_xcp, + "--asset", + "XCP", + "--quantity", + int(10 * 10e8), + "--destination", + str(address), + _out=sys.stdout, + _err=sys.stdout, + ) + sh.python3( + "tools/xcpcli.py", + "send_send", + "--address", + source_with_xcp, + "--asset", + "BTC", + "--quantity", + int(10 * 10e8), + "--destination", + str(address), + _out=sys.stdout, + _err=sys.stdout, + ) + return source_with_xcp + + +bitcoin_cli = bake_bitcoin_clients() + +# generate a new address +address, secret_key = get_new_address("correct horse battery staple") +# send XCP and BTC to this address +destination_address = send_funds_to_address(address) +# mine a block +bitcoin_cli("generatetoaddress", 1, destination_address) +time.sleep(10) + +# generate unsigned pretx +unsigned_pretx_hex = api_call( + f"addresses/{str(address)}/compose/send", + { + "asset": "XCP", + "quantity": 5000, + "destination": destination_address, + "encoding": "p2sh", + "fee_per_kb": 1000, + "pubkeys": secret_key.pub.hex(), + }, +)["result"]["unsigned_pretx_hex"] +print("unsigned_pretx_hex:", unsigned_pretx_hex) + +# sign pretx +signed_pretx_hex = sign_rawtransaction(unsigned_pretx_hex, address, secret_key, int(10 * 10e8)) +print("signed_pretx_hex:", signed_pretx_hex) + +# broadcast pretx and get pretx_txid +pretx_txid = bitcoin_cli("sendrawtransaction", signed_pretx_hex).strip() +print("pretx_txid", pretx_txid) +# mine a block +bitcoin_cli("generatetoaddress", 1, destination_address) +time.sleep(10) + +# generate final tx +unsigned_finaltx_hex = api_call( + f"addresses/{str(address)}/compose/send", + { + "asset": "XCP", + "quantity": 5000, + "destination": destination_address, + "encoding": "p2sh", + "fee_per_kb": 1000, + "pubkeys": secret_key.pub.hex(), + "p2sh_pretx_txid": pretx_txid, + }, +)["result"]["rawtransaction"] +print("unsigned_finaltx_hex:", unsigned_finaltx_hex) + +# sign and broadcast final tx +signed_finaltx_hex = sign_rawtransaction(unsigned_finaltx_hex, address, secret_key) +print("signed_finaltx_hex:", signed_finaltx_hex) +txid = bitcoin_cli("sendrawtransaction", signed_finaltx_hex).strip() diff --git a/counterparty-core/tools/xcpcli.py b/counterparty-core/tools/xcpcli.py index ec40c8e01a..60e30301e4 100644 --- a/counterparty-core/tools/xcpcli.py +++ b/counterparty-core/tools/xcpcli.py @@ -184,9 +184,10 @@ def sign_and_send_transaction(result): return check_bitcoin_cli_is_installed() bitcoin_cli, bitcoin_wallet = bake_bitcoin_clients() - signed_transaction_json = bitcoin_wallet( - "signrawtransactionwithwallet", result["result"]["rawtransaction"] - ).strip() + rawtransaction = result["result"].get("rawtransaction") or result["result"].get( + "unsigned_pretx_hex" + ) + signed_transaction_json = bitcoin_wallet("signrawtransactionwithwallet", rawtransaction).strip() signed_transaction = json.loads(signed_transaction_json)["hex"] tx_hash = bitcoin_wallet("sendrawtransaction", signed_transaction, 0).strip() cprint(f"Transaction sent: {tx_hash}", "green") From 23a35de0f96d674f01703d2159dc1e39ce959e3f Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Wed, 2 Oct 2024 08:36:50 +0000 Subject: [PATCH 2/3] fix p2sh signature --- .../counterpartycore/lib/transaction.py | 2 +- .../counterpartycore/test/regtest/testp2sh.py | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/counterparty-core/counterpartycore/lib/transaction.py b/counterparty-core/counterpartycore/lib/transaction.py index 0814fd6949..7eed22937e 100644 --- a/counterparty-core/counterpartycore/lib/transaction.py +++ b/counterparty-core/counterpartycore/lib/transaction.py @@ -102,7 +102,7 @@ def check_transaction_sanity(db, source, tx_info, unsigned_tx_hex, encoding, inp if encoding == "p2sh": # make_canonical can't determine the address, so we blindly change the desired to the parsed desired_source = parsed_source - # desired_destination = parsed_destination + desired_destination = parsed_destination except exceptions.BTCOnlyError: # Skip BTC‐only transactions. return diff --git a/counterparty-core/counterpartycore/test/regtest/testp2sh.py b/counterparty-core/counterpartycore/test/regtest/testp2sh.py index 2e29d7a3aa..30f0f5ecbe 100644 --- a/counterparty-core/counterpartycore/test/regtest/testp2sh.py +++ b/counterparty-core/counterpartycore/test/regtest/testp2sh.py @@ -67,7 +67,7 @@ def get_new_address(seed: str): return address, secret_key -def sign_rawtransaction(rawtransaction, address, secret_key, amount=None): +def sign_p2wpkh_rawtransaction(rawtransaction, address, secret_key, amount=None): tx = CMutableTransaction.deserialize(bytes.fromhex(rawtransaction)) txin_index = 0 redeem_script = address.to_redeemScript() @@ -75,8 +75,6 @@ def sign_rawtransaction(rawtransaction, address, secret_key, amount=None): if not amount: amount = get_tx_out_amount(prev_txid, tx.vin[txin_index].prevout.n) amount = int(amount * 10e8) - # amount = int(10 * 10e8) - print("AMount", amount) sighash = SignatureHash( redeem_script, tx, txin_index, SIGHASH_ALL, amount=amount, sigversion=SIGVERSION_WITNESS_V0 ) @@ -90,6 +88,18 @@ def sign_rawtransaction(rawtransaction, address, secret_key, amount=None): return signed_tx.serialize().hex() +def sign_p2sh_rawtransaction(rawtransaction, secret_key): + tx = CMutableTransaction.deserialize(bytes.fromhex(rawtransaction)) + txin_index = 0 + txin_redeemScript = tx.vin[txin_index].scriptSig + sighash = SignatureHash(txin_redeemScript, tx, txin_index, SIGHASH_ALL) + sig = secret_key.sign(sighash) + bytes([SIGHASH_ALL]) + # set scriptSig + vins = [CTxIn(tx.vin[0].prevout, CScript([sig, txin_redeemScript]))] + signed_tx = CMutableTransaction(vins, tx.vout) + return signed_tx.serialize().hex() + + def send_funds_to_address(address): source_with_xcp = api_call("assets/XCP/balances", {"limit": 1})["result"][0]["address"] sh.python3( @@ -148,7 +158,9 @@ def send_funds_to_address(address): print("unsigned_pretx_hex:", unsigned_pretx_hex) # sign pretx -signed_pretx_hex = sign_rawtransaction(unsigned_pretx_hex, address, secret_key, int(10 * 10e8)) +signed_pretx_hex = sign_p2wpkh_rawtransaction( + unsigned_pretx_hex, address, secret_key, int(10 * 10e8) +) print("signed_pretx_hex:", signed_pretx_hex) # broadcast pretx and get pretx_txid @@ -174,6 +186,7 @@ def send_funds_to_address(address): print("unsigned_finaltx_hex:", unsigned_finaltx_hex) # sign and broadcast final tx -signed_finaltx_hex = sign_rawtransaction(unsigned_finaltx_hex, address, secret_key) +signed_finaltx_hex = sign_p2sh_rawtransaction(unsigned_finaltx_hex, secret_key) print("signed_finaltx_hex:", signed_finaltx_hex) txid = bitcoin_cli("sendrawtransaction", signed_finaltx_hex).strip() +bitcoin_cli("generatetoaddress", 1, destination_address) From ea9fb36b0d983b0eb1b4cf3240aaa049dee00847 Mon Sep 17 00:00:00 2001 From: Ouziel Slama Date: Wed, 2 Oct 2024 10:24:12 +0000 Subject: [PATCH 3/3] test with broadcast instead send --- .../counterpartycore/lib/transaction.py | 1 - .../counterpartycore/test/regtest/testp2sh.py | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/counterparty-core/counterpartycore/lib/transaction.py b/counterparty-core/counterpartycore/lib/transaction.py index 7eed22937e..0376da8d07 100644 --- a/counterparty-core/counterpartycore/lib/transaction.py +++ b/counterparty-core/counterpartycore/lib/transaction.py @@ -102,7 +102,6 @@ def check_transaction_sanity(db, source, tx_info, unsigned_tx_hex, encoding, inp if encoding == "p2sh": # make_canonical can't determine the address, so we blindly change the desired to the parsed desired_source = parsed_source - desired_destination = parsed_destination except exceptions.BTCOnlyError: # Skip BTC‐only transactions. return diff --git a/counterparty-core/counterpartycore/test/regtest/testp2sh.py b/counterparty-core/counterpartycore/test/regtest/testp2sh.py index 30f0f5ecbe..5d3aeb430e 100644 --- a/counterparty-core/counterpartycore/test/regtest/testp2sh.py +++ b/counterparty-core/counterpartycore/test/regtest/testp2sh.py @@ -145,11 +145,12 @@ def send_funds_to_address(address): # generate unsigned pretx unsigned_pretx_hex = api_call( - f"addresses/{str(address)}/compose/send", + f"addresses/{str(address)}/compose/broadcast", { - "asset": "XCP", - "quantity": 5000, - "destination": destination_address, + "value": 1, + "fee_fraction": 0.5, + "timestamp": 4003903985, + "text": "un broadcast avec une transaction p2sh", "encoding": "p2sh", "fee_per_kb": 1000, "pubkeys": secret_key.pub.hex(), @@ -172,11 +173,12 @@ def send_funds_to_address(address): # generate final tx unsigned_finaltx_hex = api_call( - f"addresses/{str(address)}/compose/send", + f"addresses/{str(address)}/compose/broadcast", { - "asset": "XCP", - "quantity": 5000, - "destination": destination_address, + "value": 1, + "fee_fraction": 0.5, + "timestamp": 4003903985, + "text": "un broadcast avec une transaction p2sh", "encoding": "p2sh", "fee_per_kb": 1000, "pubkeys": secret_key.pub.hex(),