Browse Source

Updates to account for code changes in #544

Note in particular that:
bitcoin.mktx in this PR now does support script
entries in outputs to account for nonstandard
destinations (as is needed for burn).
bitcoin.sign now supports p2wsh (as is needed
for timelocks).
master
Adam Gibson 6 years ago
parent
commit
53ef79bf37
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 55
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  2. 2
      jmbitcoin/test/test_tx_signing.py
  3. 16
      jmclient/jmclient/cryptoengine.py
  4. 6
      jmclient/jmclient/taker_utils.py
  5. 6
      jmclient/jmclient/wallet.py
  6. 2
      jmclient/test/test_taker.py
  7. 26
      jmclient/test/test_tx_creation.py
  8. 12
      jmclient/test/test_wallet.py

55
jmbitcoin/jmbitcoin/secp256k1_transaction.py

@ -3,6 +3,10 @@
# note, only used for non-cryptographic randomness: # note, only used for non-cryptographic randomness:
import random import random
import json 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 jmbitcoin.secp256k1_main import *
from jmbase import bintohex, utxo_to_utxostr from jmbase import bintohex, utxo_to_utxostr
from bitcointx.core import (CMutableTransaction, Hash160, CTxInWitness, 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() return pubkey_to_p2wpkh_script(pub).to_p2sh_scriptPubKey()
def redeem_script_to_p2wsh_script(redeem_script): def redeem_script_to_p2wsh_script(redeem_script):
return P2WSH_PRE + bin_sha256(binascii.unhexlify(redeem_script)) """ Given redeem script of type CScript (or bytes)
returns the corresponding segwit v0 scriptPubKey as
def redeem_script_to_p2wsh_address(redeem_script, vbyte, witver=0): for the case pay-to-witness-scripthash.
return script_to_address(redeem_script_to_p2wsh_script(redeem_script), vbyte, witver) """
return standard_witness_v0_scriptpubkey(
hashlib.sha256(redeem_script).digest())
def mk_freeze_script(pub, locktime): def mk_freeze_script(pub, locktime):
""" """
@ -169,27 +175,30 @@ def mk_freeze_script(pub, locktime):
if not isinstance(pub, bytes): if not isinstance(pub, bytes):
raise TypeError("pubkey must be in bytes") raise TypeError("pubkey must be in bytes")
usehex = False 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") raise ValueError("not a valid public key")
scr = [locktime, btc.OP_CHECKLOCKTIMEVERIFY, btc.OP_DROP, pub, return CScript([locktime, OP_CHECKLOCKTIMEVERIFY, OP_DROP, pub,
btc.OP_CHECKSIG] OP_CHECKSIG])
return binascii.hexlify(serialize_script(scr)).decode()
def mk_burn_script(data): 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): if not isinstance(data, bytes):
raise TypeError("data must be in bytes") raise TypeError("data must be in bytes")
data = binascii.hexlify(data).decode() return CScript([btc.OP_RETURN, data])
scr = [btc.OP_RETURN, data]
return serialize_script(scr)
def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False): def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False):
""" """
Given a transaction tx of type CMutableTransaction, an input index i, Given a transaction tx of type CMutableTransaction, an input index i,
and a raw privkey in bytes, updates the CMutableTransaction to contain and a raw privkey in bytes, updates the CMutableTransaction to contain
the newly appended signature. 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 Note that signing multisig must be done outside this function, using
the wrapped library. 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") Returns: (signature, "signing succeeded")
or: (None, errormsg) in case of failure or: (None, errormsg) in case of failure
""" """
@ -226,9 +235,15 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False):
# see line 1256 of bitcointx.core.scripteval.py: # see line 1256 of bitcointx.core.scripteval.py:
flags.add(SCRIPT_VERIFY_P2SH) flags.add(SCRIPT_VERIFY_P2SH)
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) input_scriptPubKey = pubkey_to_p2wpkh_script(pub)
# only created for convenience access to scriptCode: # only created for convenience access to scriptCode:
input_address = P2WPKHCoinAddress.from_scriptPubKey(input_scriptPubKey) input_address = P2WPKHCoinAddress.from_scriptPubKey(
input_scriptPubKey)
# function name is misleading here; redeemScript only applies to p2sh. # function name is misleading here; redeemScript only applies to p2sh.
scriptCode = input_address.to_redeemScript() scriptCode = input_address.to_redeemScript()
@ -243,6 +258,9 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, amount=None, native=False):
else: else:
tx.vin[i].scriptSig = CScript([input_scriptPubKey]) tx.vin[i].scriptSig = CScript([input_scriptPubKey])
if native and native != "p2wpkh":
witness = [sig, scriptCode]
else:
witness = [sig, pub] witness = [sig, pub]
ctxwitness = CTxInWitness(CScriptWitness(witness)) ctxwitness = CTxInWitness(CScriptWitness(witness))
tx.wit.vtxinwit[i] = ctxwitness tx.wit.vtxinwit[i] = ctxwitness
@ -268,7 +286,9 @@ def apply_freeze_signature(tx, i, redeem_script, sig):
def mktx(ins, outs, version=1, locktime=0): def mktx(ins, outs, version=1, locktime=0):
""" Given a list of input tuples (txid(bytes), n(int)), """ Given a list of input tuples (txid(bytes), n(int)),
and a list of outputs which are dicts with 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 "value" (value should be integer satoshis), outputs a
CMutableTransaction object. CMutableTransaction object.
Tx version and locktime are optionally set, for non-default 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) inp = CMutableTxIn(prevout=outpoint, nSequence=sequence)
vin.append(inp) vin.append(inp)
for o in outs: for o in outs:
if "script" in o:
sPK = o["script"]
else:
# note the to_scriptPubKey method is only available for standard # note the to_scriptPubKey method is only available for standard
# address types # address types
out = CMutableTxOut(o["value"], sPK = CCoinAddress(o["address"]).to_scriptPubKey()
CCoinAddress(o["address"]).to_scriptPubKey()) out = CMutableTxOut(o["value"], sPK)
vout.append(out) vout.append(out)
return CMutableTransaction(vin, vout, nLockTime=locktime, nVersion=version) return CMutableTransaction(vin, vout, nLockTime=locktime, nVersion=version)

2
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 # 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. # private key that controls the UTXO being spent here at this txin_index.
if addrtype == "p2wpkh": 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": elif addrtype == "p2sh-p2wpkh":
sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False) sig, msg = btc.sign(tx, 0, priv, amount=amount, native=False)
elif addrtype == "p2pkh": elif addrtype == "p2pkh":

16
jmclient/jmclient/cryptoengine.py

@ -314,7 +314,7 @@ class BTC_P2WPKH(BTCEngine):
hashcode=btc.SIGHASH_ALL, **kwargs): hashcode=btc.SIGHASH_ALL, **kwargs):
assert amount is not None assert amount is not None
return btc.sign(tx, index, privkey, return btc.sign(tx, index, privkey,
hashcode=hashcode, amount=amount, native=True) hashcode=hashcode, amount=amount, native="p2wpkh")
class BTC_Timelocked_P2WSH(BTCEngine): class BTC_Timelocked_P2WSH(BTCEngine):
@ -362,16 +362,10 @@ class BTC_Timelocked_P2WSH(BTCEngine):
def sign_transaction(cls, tx, index, privkey_locktime, amount, def sign_transaction(cls, tx, index, privkey_locktime, amount,
hashcode=btc.SIGHASH_ALL, **kwargs): hashcode=btc.SIGHASH_ALL, **kwargs):
assert amount is not None assert amount is not None
priv, locktime = privkey_locktime
privkey, locktime = privkey_locktime pub = cls.privkey_to_pubkey(priv)
privkey = hexlify(privkey).decode() redeem_script = cls.pubkey_to_script_code((pub, locktime))
pubkey = btc.privkey_to_pubkey(privkey) return btc.sign(tx, index, priv, amount=amount, native=redeem_script)
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)
class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH): class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH):

6
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, \ from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \
FidelityBondMixin FidelityBondMixin
from jmbitcoin import make_shuffled_tx, amount_to_str, mk_burn_script,\ 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 from jmbase.support import EXIT_SUCCESS
log = get_log() 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) path = wallet_service.wallet.get_path(mixdepth, address_type, index)
privkey, engine = wallet_service.wallet._get_key_from_path(path) privkey, engine = wallet_service.wallet._get_key_from_path(path)
pubkey = engine.privkey_to_pubkey(privkey) pubkey = engine.privkey_to_pubkey(privkey)
pubkeyhash = bin_hash160(pubkey) pubkeyhash = Hash160(pubkey)
#size of burn output is slightly different from regular outputs #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) 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}] outs = [{"script": burn_script, "value": total_inputs_val - fee_est}]

6
jmclient/jmclient/wallet.py

@ -1193,7 +1193,7 @@ class PSBTWalletMixin(object):
# this happens when an input is provided but it's not in # this happens when an input is provided but it's not in
# this wallet; in this case, we cannot set the redeem script. # this wallet; in this case, we cannot set the redeem script.
continue continue
privkey, _ = self._get_priv_from_path(path) privkey, _ = self._get_key_from_path(path)
txinput.redeem_script = btc.pubkey_to_p2wpkh_script( txinput.redeem_script = btc.pubkey_to_p2wpkh_script(
btc.privkey_to_pubkey(privkey)) btc.privkey_to_pubkey(privkey))
return new_psbt return new_psbt
@ -1218,7 +1218,7 @@ class PSBTWalletMixin(object):
privkeys = [] privkeys = []
for k, v in self._utxos._utxo.items(): for k, v in self._utxos._utxo.items():
for k2, v2 in v.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) jmckeys = list(btc.JMCKey(x[0][:-1]) for x in privkeys)
new_keystore = btc.KeyStore.from_iterable(jmckeys) 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 happens when an input is provided but it's not in
# this wallet; in this case, we cannot set the redeem script. # this wallet; in this case, we cannot set the redeem script.
continue continue
privkey, _ = self._get_priv_from_path(path) privkey, _ = self._get_key_from_path(path)
txinput.redeem_script = btc.pubkey_to_p2wpkh_script( txinput.redeem_script = btc.pubkey_to_p2wpkh_script(
btc.privkey_to_pubkey(privkey)) btc.privkey_to_pubkey(privkey))
# no else branch; any other form of scriptPubKey will just be # no else branch; any other form of scriptPubKey will just be

2
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)] utxos = [(struct.pack(b"B", x) * 32, 1) for x in range(5)]
#create 2 privkey + utxos that are to be ours #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)]] 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)] addrs = [BTC_P2PKH.privkey_to_address(privs[x]) for x in range(5)]
fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x], fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x],
'script': scripts[x], 'confirms': 20} for x in range(5)] 'script': scripts[x], 'confirms': 20} for x in range(5)]

26
jmclient/test/test_tx_creation.py

@ -6,7 +6,6 @@
does not use this feature.''' does not use this feature.'''
import struct import struct
from binascii import unhexlify
from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
@ -132,7 +131,7 @@ def test_spend_p2wpkh(setup_tx_creation):
for i, priv in enumerate(privs): for i, priv in enumerate(privs):
# sign each of 3 inputs; note that bitcoin.sign # sign each of 3 inputs; note that bitcoin.sign
# automatically validates each signature it creates. # 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: if not sig:
assert False, msg assert False, msg
txid = jm_single().bc_interface.pushtx(tx.serialize()) 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: for timeoffset, required_success in timeoffset_success_tests:
#generate keypair #generate keypair
priv = "aa"*32 + "01" priv = b"\xaa"*32 + b"\x01"
pub = unhexlify(bitcoin.privkey_to_pubkey(priv)) pub = bitcoin.privkey_to_pubkey(priv)
addr_locktime = mediantime + timeoffset addr_locktime = mediantime + timeoffset
redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime) redeem_script = bitcoin.mk_freeze_script(pub, addr_locktime)
script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script) script_pub_key = bitcoin.redeem_script_to_p2wsh_script(redeem_script)
regtest_vbyte = 100 # cannot convert to address within wallet service, as not known
addr = bitcoin.script_to_address(script_pub_key, vbyte=regtest_vbyte) # to wallet; use engine directly:
addr = wallet_service._ENGINE.script_to_address(script_pub_key)
#fund frozen funds address #fund frozen funds address
amount = 100000000 amount = 100000000
@ -165,20 +165,18 @@ def test_spend_freeze_script(setup_tx_creation):
assert funding_txid assert funding_txid
#spend frozen funds #spend frozen funds
frozen_in = funding_txid + ":0" frozen_in = (funding_txid, 0)
output_addr = wallet_service.get_internal_addr(1) output_addr = wallet_service.get_internal_addr(1)
miner_fee = 5000 miner_fee = 5000
outs = [{'value': amount - miner_fee, 'address': output_addr}] outs = [{'value': amount - miner_fee, 'address': output_addr}]
tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1) tx = bitcoin.mktx([frozen_in], outs, locktime=addr_locktime+1)
i = 0 i = 0
sig = bitcoin.get_p2sh_signature(tx, i, redeem_script, priv, amount) sig, success = bitcoin.sign(tx, i, priv, amount=amount,
native=redeem_script)
assert bitcoin.verify_tx_input(tx, i, script_pub_key, sig, pub, assert success
scriptCode=redeem_script, amount=amount) push_success = jm_single().bc_interface.pushtx(tx.serialize())
tx = bitcoin.apply_freeze_signature(tx, i, redeem_script, sig)
push_success = jm_single().bc_interface.pushtx(tx)
assert push_success == required_success assert push_success == required_success
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_tx_creation(): def setup_tx_creation():
load_test_config() load_test_config()

12
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)) utxo = fund_wallet_addr(wallet, wallet.script_to_addr(script))
timestamp = wallet._time_number_to_timestamp(timenumber) timestamp = wallet._time_number_to_timestamp(timenumber)
tx = btc.deserialize(btc.mktx(['{}:{}'.format( tx = btc.mktx([utxo], [{"address": str(btc.CCoinAddress.from_scriptPubKey(
hexlify(utxo[0]).decode('ascii'), utxo[1])], btc.standard_scripthash_scriptpubkey(btc.Hash160(b"\x00")))),
[btc.p2sh_scriptaddr(b"\x00",magicbyte=196) + ':' + str(10**8 - 9000)], "value":10**8 - 9000}], locktime=timestamp+1)
locktime=timestamp+1)) success, msg = wallet.sign_tx(tx, {0: (script, 10**8)})
tx = wallet.sign_tx(tx, {0: (script, 10**8)}) assert success, msg
txout = jm_single().bc_interface.pushtx(btc.serialize(tx)) txout = jm_single().bc_interface.pushtx(tx.serialize())
assert txout assert txout
def test_get_bbm(setup_wallet): def test_get_bbm(setup_wallet):

Loading…
Cancel
Save