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. 69
      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

69
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)

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
# 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":

16
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):

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, \
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}]

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 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

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)]
#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)]

26
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()

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))
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):

Loading…
Cancel
Save