Skip to content

Commit

Permalink
Updates to account for code changes in #544
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
AdamISZ committed Jul 4, 2020
1 parent 4935275 commit 8718c0e
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 62 deletions.
69 changes: 46 additions & 23 deletions jmbitcoin/jmbitcoin/secp256k1_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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
"""
Expand Down Expand Up @@ -226,11 +235,17 @@ def return_err(e):
# 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)
Expand All @@ -243,7 +258,10 @@ def return_err(e):
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.
Expand All @@ -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
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion jmbitcoin/test/test_tx_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
16 changes: 5 additions & 11 deletions jmclient/jmclient/cryptoengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def sign_transaction(cls, tx, index, privkey, amount,
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):

Expand Down Expand Up @@ -362,16 +362,10 @@ def privkey_to_wif(cls, privkey_locktime):
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):

Expand Down
6 changes: 3 additions & 3 deletions jmclient/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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()

Expand Down Expand Up @@ -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}]
Expand Down
6 changes: 3 additions & 3 deletions jmclient/jmclient/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ def create_psbt_from_tx(self, tx, spent_outs=None):
# 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
Expand All @@ -1218,7 +1218,7 @@ def sign_psbt(self, in_psbt, with_sign_result=False):
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)

Expand All @@ -1241,7 +1241,7 @@ def sign_psbt(self, in_psbt, with_sign_result=False):
# 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
Expand Down
2 changes: 1 addition & 1 deletion jmclient/test/test_taker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
26 changes: 12 additions & 14 deletions jmclient/test/test_tx_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand All @@ -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 changes: 6 additions & 6 deletions jmclient/test/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 8718c0e

Please sign in to comment.