diff --git a/jmbitcoin/jmbitcoin/secp256k1_transaction.py b/jmbitcoin/jmbitcoin/secp256k1_transaction.py index 705302c..4b5f65b 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_transaction.py +++ b/jmbitcoin/jmbitcoin/secp256k1_transaction.py @@ -3,6 +3,10 @@ # note, only used for non-cryptographic randomness: import random import json +# needed for single sha256 evaluation, which is used +# in bitcoin (p2wsh) but not exposed in python-bitcointx: +import hashlib + from jmbitcoin.secp256k1_main import * from jmbase import bintohex, utxo_to_utxostr from bitcointx.core import (CMutableTransaction, Hash160, CTxInWitness, @@ -154,10 +158,12 @@ def pubkey_to_p2sh_p2wpkh_script(pub): return pubkey_to_p2wpkh_script(pub).to_p2sh_scriptPubKey() def redeem_script_to_p2wsh_script(redeem_script): - return P2WSH_PRE + bin_sha256(binascii.unhexlify(redeem_script)) - -def redeem_script_to_p2wsh_address(redeem_script, vbyte, witver=0): - return script_to_address(redeem_script_to_p2wsh_script(redeem_script), vbyte, witver) + """ Given redeem script of type CScript (or bytes) + returns the corresponding segwit v0 scriptPubKey as + for the case pay-to-witness-scripthash. + """ + return standard_witness_v0_scriptpubkey( + hashlib.sha256(redeem_script).digest()) def mk_freeze_script(pub, locktime): """ @@ -169,27 +175,30 @@ def mk_freeze_script(pub, locktime): if not isinstance(pub, bytes): raise TypeError("pubkey must be in bytes") usehex = False - if not is_valid_pubkey(pub, usehex, require_compressed=True): + if not is_valid_pubkey(pub, require_compressed=True): raise ValueError("not a valid public key") - scr = [locktime, btc.OP_CHECKLOCKTIMEVERIFY, btc.OP_DROP, pub, - btc.OP_CHECKSIG] - return binascii.hexlify(serialize_script(scr)).decode() + return CScript([locktime, OP_CHECKLOCKTIMEVERIFY, OP_DROP, pub, + OP_CHECKSIG]) def mk_burn_script(data): + """ For a given bytestring (data), + returns a scriptPubKey which is an OP_RETURN + of that data. + """ if not isinstance(data, bytes): raise TypeError("data must be in bytes") - data = binascii.hexlify(data).decode() - scr = [btc.OP_RETURN, data] - return serialize_script(scr) + return CScript([btc.OP_RETURN, data]) def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False): """ Given a transaction tx of type CMutableTransaction, an input index i, and a raw privkey in bytes, updates the CMutableTransaction to contain the newly appended signature. - Only three scriptPubKey types supported: p2pkh, p2wpkh, p2sh-p2wpkh. + Only four scriptPubKey types supported: p2pkh, p2wpkh, p2sh-p2wpkh, p2wsh. Note that signing multisig must be done outside this function, using the wrapped library. + If native is not the default (False), and if native != "p2wpkh", + then native must be a CScript object containing the redeemscript needed to sign. Returns: (signature, "signing succeeded") or: (None, errormsg) in case of failure """ @@ -226,11 +235,17 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False): # see line 1256 of bitcointx.core.scripteval.py: flags.add(SCRIPT_VERIFY_P2SH) - input_scriptPubKey = pubkey_to_p2wpkh_script(pub) - # only created for convenience access to scriptCode: - input_address = P2WPKHCoinAddress.from_scriptPubKey(input_scriptPubKey) - # function name is misleading here; redeemScript only applies to p2sh. - scriptCode = input_address.to_redeemScript() + if native and native != "p2wpkh": + scriptCode = native + input_scriptPubKey = redeem_script_to_p2wsh_script(native) + else: + # this covers both p2wpkh and p2sh-p2wpkh case: + input_scriptPubKey = pubkey_to_p2wpkh_script(pub) + # only created for convenience access to scriptCode: + input_address = P2WPKHCoinAddress.from_scriptPubKey( + input_scriptPubKey) + # function name is misleading here; redeemScript only applies to p2sh. + scriptCode = input_address.to_redeemScript() sighash = SignatureHash(scriptCode, tx, i, hashcode, amount=amount, sigversion=SIGVERSION_WITNESS_V0) @@ -243,7 +258,10 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False): else: tx.vin[i].scriptSig = CScript([input_scriptPubKey]) - witness = [sig, pub] + if native and native != "p2wpkh": + witness = [sig, scriptCode] + else: + witness = [sig, pub] ctxwitness = CTxInWitness(CScriptWitness(witness)) tx.wit.vtxinwit[i] = ctxwitness # Verify the signature worked. @@ -268,7 +286,9 @@ def apply_freeze_signature(tx, i, redeem_script, sig): def mktx(ins, outs, version=1, locktime=0): """ Given a list of input tuples (txid(bytes), n(int)), and a list of outputs which are dicts with - keys "address" (value should be *str* not CCoinAddress), + keys "address" (value should be *str* not CCoinAddress) ( + or alternately "script" (for nonstandard outputs, value + should be CScript)), "value" (value should be integer satoshis), outputs a CMutableTransaction object. Tx version and locktime are optionally set, for non-default @@ -289,10 +309,13 @@ def mktx(ins, outs, version=1, locktime=0): inp = CMutableTxIn(prevout=outpoint, nSequence=sequence) vin.append(inp) for o in outs: - # note the to_scriptPubKey method is only available for standard - # address types - out = CMutableTxOut(o["value"], - CCoinAddress(o["address"]).to_scriptPubKey()) + if "script" in o: + sPK = o["script"] + else: + # note the to_scriptPubKey method is only available for standard + # address types + sPK = CCoinAddress(o["address"]).to_scriptPubKey() + out = CMutableTxOut(o["value"], sPK) vout.append(out) return CMutableTransaction(vin, vout, nLockTime=locktime, nVersion=version) diff --git a/jmbitcoin/test/test_tx_signing.py b/jmbitcoin/test/test_tx_signing.py index d7f3d6f..e2ccf21 100644 --- a/jmbitcoin/test/test_tx_signing.py +++ b/jmbitcoin/test/test_tx_signing.py @@ -49,7 +49,7 @@ def test_sign_standard_txs(addrtype): # Calculate the signature hash for the transaction. This is then signed by the # private key that controls the UTXO being spent here at this txin_index. if addrtype == "p2wpkh": - sig, msg = btc.sign(tx, 0, priv, amount=amount, native=True) + sig, msg = btc.sign(tx, 0, priv, amount=amount, native="p2wpkh") elif addrtype == "p2sh-p2wpkh": sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False) elif addrtype == "p2pkh": diff --git a/jmclient/jmclient/cryptoengine.py b/jmclient/jmclient/cryptoengine.py index 17f1148..9262d3b 100644 --- a/jmclient/jmclient/cryptoengine.py +++ b/jmclient/jmclient/cryptoengine.py @@ -314,7 +314,7 @@ class BTC_P2WPKH(BTCEngine): hashcode=btc.SIGHASH_ALL, **kwargs): assert amount is not None return btc.sign(tx, index, privkey, - hashcode=hashcode, amount=amount, native=True) + hashcode=hashcode, amount=amount, native="p2wpkh") class BTC_Timelocked_P2WSH(BTCEngine): @@ -362,16 +362,10 @@ class BTC_Timelocked_P2WSH(BTCEngine): def sign_transaction(cls, tx, index, privkey_locktime, amount, hashcode=btc.SIGHASH_ALL, **kwargs): assert amount is not None - - privkey, locktime = privkey_locktime - privkey = hexlify(privkey).decode() - pubkey = btc.privkey_to_pubkey(privkey) - pubkey = unhexlify(pubkey) - redeem_script = cls.pubkey_to_script_code((pubkey, locktime)) - tx = btc.serialize(tx) - sig = btc.get_p2sh_signature(tx, index, redeem_script, privkey, - amount) - return btc.apply_freeze_signature(tx, index, redeem_script, sig) + priv, locktime = privkey_locktime + pub = cls.privkey_to_pubkey(priv) + redeem_script = cls.pubkey_to_script_code((pub, locktime)) + return btc.sign(tx, index, priv, amount=amount, native=redeem_script) class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH): diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index 875b343..f579356 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -11,7 +11,7 @@ from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \ FidelityBondMixin from jmbitcoin import make_shuffled_tx, amount_to_str, mk_burn_script,\ - PartiallySignedTransaction, CMutableTxOut, hrt + PartiallySignedTransaction, CMutableTxOut, hrt, Hash160 from jmbase.support import EXIT_SUCCESS log = get_log() @@ -94,10 +94,10 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, path = wallet_service.wallet.get_path(mixdepth, address_type, index) privkey, engine = wallet_service.wallet._get_key_from_path(path) pubkey = engine.privkey_to_pubkey(privkey) - pubkeyhash = bin_hash160(pubkey) + pubkeyhash = Hash160(pubkey) #size of burn output is slightly different from regular outputs - burn_script = mk_burn_script(pubkeyhash) #in hex + burn_script = mk_burn_script(pubkeyhash) fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2) outs = [{"script": burn_script, "value": total_inputs_val - fee_est}] diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 5185113..2797e3c 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -1193,7 +1193,7 @@ class PSBTWalletMixin(object): # this happens when an input is provided but it's not in # this wallet; in this case, we cannot set the redeem script. continue - privkey, _ = self._get_priv_from_path(path) + privkey, _ = self._get_key_from_path(path) txinput.redeem_script = btc.pubkey_to_p2wpkh_script( btc.privkey_to_pubkey(privkey)) return new_psbt @@ -1218,7 +1218,7 @@ class PSBTWalletMixin(object): privkeys = [] for k, v in self._utxos._utxo.items(): for k2, v2 in v.items(): - privkeys.append(self._get_priv_from_path(v2[0])) + privkeys.append(self._get_key_from_path(v2[0])) jmckeys = list(btc.JMCKey(x[0][:-1]) for x in privkeys) new_keystore = btc.KeyStore.from_iterable(jmckeys) @@ -1241,7 +1241,7 @@ class PSBTWalletMixin(object): # this happens when an input is provided but it's not in # this wallet; in this case, we cannot set the redeem script. continue - privkey, _ = self._get_priv_from_path(path) + privkey, _ = self._get_key_from_path(path) txinput.redeem_script = btc.pubkey_to_p2wpkh_script( btc.privkey_to_pubkey(privkey)) # no else branch; any other form of scriptPubKey will just be diff --git a/jmclient/test/test_taker.py b/jmclient/test/test_taker.py index 9d6471b..a316078 100644 --- a/jmclient/test/test_taker.py +++ b/jmclient/test/test_taker.py @@ -412,7 +412,7 @@ def test_on_sig(setup_taker, dummyaddr, schedule): utxos = [(struct.pack(b"B", x) * 32, 1) for x in range(5)] #create 2 privkey + utxos that are to be ours privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,6)]] - scripts = [BTC_P2PKH.privkey_to_script(privs[x]) for x in range(5)] + scripts = [BTC_P2PKH.key_to_script(privs[x]) for x in range(5)] addrs = [BTC_P2PKH.privkey_to_address(privs[x]) for x in range(5)] fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x], 'script': scripts[x], 'confirms': 20} for x in range(5)] diff --git a/jmclient/test/test_tx_creation.py b/jmclient/test/test_tx_creation.py index 75f4d8a..4c0aafe 100644 --- a/jmclient/test/test_tx_creation.py +++ b/jmclient/test/test_tx_creation.py @@ -6,7 +6,6 @@ does not use this feature.''' import struct -from binascii import unhexlify from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated import jmbitcoin as bitcoin @@ -132,7 +131,7 @@ def test_spend_p2wpkh(setup_tx_creation): for i, priv in enumerate(privs): # sign each of 3 inputs; note that bitcoin.sign # automatically validates each signature it creates. - sig, msg = bitcoin.sign(tx, i, priv, amount=amount, native=True) + sig, msg = bitcoin.sign(tx, i, priv, amount=amount, native="p2wpkh") if not sig: assert False, msg txid = jm_single().bc_interface.pushtx(tx.serialize()) @@ -150,13 +149,14 @@ def test_spend_freeze_script(setup_tx_creation): for timeoffset, required_success in timeoffset_success_tests: #generate keypair - priv = "aa"*32 + "01" - pub = unhexlify(bitcoin.privkey_to_pubkey(priv)) + priv = b"\xaa"*32 + b"\x01" + pub = bitcoin.privkey_to_pubkey(priv) addr_locktime = mediantime + timeoffset redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime) script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script) - regtest_vbyte = 100 - addr = bitcoin.script_to_address(script_pub_key, vbyte=regtest_vbyte) + # cannot convert to address within wallet service, as not known + # to wallet; use engine directly: + addr = wallet_service._ENGINE.script_to_address(script_pub_key) #fund frozen funds address amount = 100000000 @@ -165,20 +165,18 @@ def test_spend_freeze_script(setup_tx_creation): assert funding_txid #spend frozen funds - frozen_in = funding_txid + ":0" + frozen_in = (funding_txid, 0) output_addr = wallet_service.get_internal_addr(1) miner_fee = 5000 outs = [{'value': amount - miner_fee, 'address': output_addr}] tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1) i = 0 - sig = bitcoin.get_p2sh_signature(tx, i, redeem_script, priv, amount) - - assert bitcoin.verify_tx_input(tx, i, script_pub_key, sig, pub, - scriptCode=redeem_script, amount=amount) - tx = bitcoin.apply_freeze_signature(tx, i, redeem_script, sig) - push_success = jm_single().bc_interface.pushtx(tx) - + sig, success = bitcoin.sign(tx, i, priv, amount=amount, + native=redeem_script) + assert success + push_success = jm_single().bc_interface.pushtx(tx.serialize()) assert push_success == required_success + @pytest.fixture(scope="module") def setup_tx_creation(): load_test_config() diff --git a/jmclient/test/test_wallet.py b/jmclient/test/test_wallet.py index c19a8cf..15fed46 100644 --- a/jmclient/test/test_wallet.py +++ b/jmclient/test/test_wallet.py @@ -416,12 +416,12 @@ def test_timelocked_output_signing(setup_wallet): utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script)) timestamp = wallet._time_number_to_timestamp(timenumber) - tx = btc.deserialize(btc.mktx(['{}:{}'.format( - hexlify(utxo[0]).decode('ascii'), utxo[1])], - [btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)], - locktime=timestamp+1)) - tx = wallet.sign_tx(tx, {0: (script, 10**8)}) - txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) + tx = btc.mktx([utxo], [{"address": str(btc.CCoinAddress.from_scriptPubKey( + btc.standard_scripthash_scriptpubkey(btc.Hash160(b"\x00")))), + "value":10**8 - 9000}], locktime=timestamp+1) + success, msg = wallet.sign_tx(tx, {0: (script, 10**8)}) + assert success, msg + txout = jm_single().bc_interface.pushtx(tx.serialize()) assert txout def test_get_bbm(setup_wallet):