Browse Source

Add support for OP_CLTV timelock addresses

The new functions implement creating fund freezing redeem scripts
and transactions which spend from such scripts.

Also there is a new test function.
master
chris-belcher 6 years ago
parent
commit
ee70cd793e
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 72
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  2. 2
      jmclient/jmclient/cryptoengine.py
  3. 56
      jmclient/test/test_tx_creation.py

72
jmbitcoin/jmbitcoin/secp256k1_transaction.py

@ -10,6 +10,7 @@ import struct
import random import random
from jmbitcoin.secp256k1_main import * from jmbitcoin.secp256k1_main import *
from jmbitcoin.bech32 import * from jmbitcoin.bech32 import *
import jmbitcoin as btc
P2PKH_PRE, P2PKH_POST = b'\x76\xa9\x14', b'\x88\xac' P2PKH_PRE, P2PKH_POST = b'\x76\xa9\x14', b'\x88\xac'
P2SH_P2WPKH_PRE, P2SH_P2WPKH_POST = b'\xa9\x14', b'\x87' P2SH_P2WPKH_PRE, P2SH_P2WPKH_POST = b'\xa9\x14', b'\x87'
@ -541,7 +542,7 @@ def pubkeys_to_p2wsh_multisig_script(pubs):
""" """
N = len(pubs) N = len(pubs)
script = mk_multisig_script(pubs, N) script = mk_multisig_script(pubs, N)
return P2WSH_PRE + bin_sha256(binascii.unhexlify(script)) return redeem_script_to_p2wsh_script(script)
def pubkeys_to_p2wsh_multisig_address(pubs): def pubkeys_to_p2wsh_multisig_address(pubs):
""" Given a list of N pubkeys, constructs an N of N """ Given a list of N pubkeys, constructs an N of N
@ -551,6 +552,12 @@ def pubkeys_to_p2wsh_multisig_address(pubs):
script = pubkeys_to_p2wsh_multisig_script(pubs) script = pubkeys_to_p2wsh_multisig_script(pubs)
return script_to_address(script) return script_to_address(script)
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)
def deserialize_script(scriptinp): def deserialize_script(scriptinp):
""" Note that this is not used internally, in """ Note that this is not used internally, in
the jmbitcoin package, to deserialize() transactions; the jmbitcoin package, to deserialize() transactions;
@ -603,10 +610,14 @@ def deserialize_script(scriptinp):
def serialize_script_unit(unit): def serialize_script_unit(unit):
if isinstance(unit, int): if isinstance(unit, int):
if unit < 16: if unit == 0:
return from_int_to_byte(unit)
elif unit < 16:
return from_int_to_byte(unit + 80) return from_int_to_byte(unit + 80)
else: elif unit < 256:
return from_int_to_byte(unit) return from_int_to_byte(unit)
else:
return b'\x04' + struct.pack(b"<I", unit)
elif unit is None: elif unit is None:
return b'\x00' return b'\x00'
else: else:
@ -634,15 +645,30 @@ def serialize_script(script):
else: else:
return result return result
def mk_multisig_script(pubs, k): def mk_multisig_script(pubs, k):
""" Given a list of pubkeys and an integer k, """ Given a list of pubkeys and an integer k,
construct a multisig script for k of N, where N is construct a multisig script for k of N, where N is
the length of the list `pubs`; script is returned the length of the list `pubs`; script is returned
as hex string. as hex string.
""" """
return serialize_script([k] + pubs + [len(pubs)]) + 'ae' return serialize_script([k] + pubs + [len(pubs)]) \
+ 'ae' #OP_CHECKMULTISIG
def mk_freeze_script(pub, locktime):
"""
Given a pubkey and locktime, create a script which can only be spent
after the locktime has passed using OP_CHECKLOCKTIMEVERIFY
"""
if not isinstance(locktime, int):
raise TypeError("locktime must be int")
if not isinstance(pub, bytes):
raise TypeError("pubkey must be in bytes")
usehex = False
if not is_valid_pubkey(pub, usehex, 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()
# Signing and verifying # Signing and verifying
@ -701,8 +727,8 @@ def sign(tx, i, priv, hashcode=SIGHASH_ALL, usenonce=None, amount=None,
`amount` flags whether segwit signing is to be done, and the field `amount` flags whether segwit signing is to be done, and the field
`native` flags that native segwit p2wpkh signing is to be done. Note `native` flags that native segwit p2wpkh signing is to be done. Note
that signing multisig is to be done with the alternative functions that signing multisig is to be done with the alternative functions
multisign or p2wsh_multisign (and non N of N multisig scripthash get_p2sh_signature or get_p2wsh_signature (and non N of N multisig
signing is not currently supported). scripthash signing is not currently supported).
""" """
if isinstance(tx, basestring) and not isinstance(tx, bytes): if isinstance(tx, basestring) and not isinstance(tx, bytes):
tx = binascii.unhexlify(tx) tx = binascii.unhexlify(tx)
@ -762,28 +788,28 @@ def signall(tx, priv):
tx = sign(tx, i, priv) tx = sign(tx, i, priv)
return tx return tx
def get_p2sh_signature(tx, i, redeem_script, pk, amount=None, hashcode=SIGHASH_ALL):
def multisign(tx, i, script, pk, amount=None, hashcode=SIGHASH_ALL): """
""" Tx is assumed to be serialized. The script passed here is Tx is assumed to be serialized. redeem_script is for example the
the redeemscript, for example the output of mk_multisig_script. output of mk_multisig_script.
pk is the private key, and must be passed in hex. pk is the private key, and must be passed in hex.
If amount is not None, the output of p2wsh_multisign is returned. If amount is not None, the output of get_p2wsh_signature is returned.
What is returned is a single signature. What is returned is a single signature.
""" """
if isinstance(tx, str): if isinstance(tx, str):
tx = binascii.unhexlify(tx) tx = binascii.unhexlify(tx)
if isinstance(script, str): if isinstance(redeem_script, str):
script = binascii.unhexlify(script) redeem_script = binascii.unhexlify(redeem_script)
if amount: if amount:
return p2wsh_multisign(tx, i, script, pk, amount, hashcode) return get_p2wsh_signature(tx, i, redeem_script, pk, amount, hashcode)
modtx = signature_form(tx, i, script, hashcode) modtx = signature_form(tx, i, redeem_script, hashcode)
return ecdsa_tx_sign(modtx, pk, hashcode) return ecdsa_tx_sign(modtx, pk, hashcode)
def p2wsh_multisign(tx, i, script, pk, amount, hashcode=SIGHASH_ALL): def get_p2wsh_signature(tx, i, redeem_script, pk, amount, hashcode=SIGHASH_ALL):
""" See note to multisign for the value to pass in as `script`. """ See note to multisign for the value to pass in as `script`.
Tx is assumed to be serialized. Tx is assumed to be serialized.
""" """
modtx = segwit_signature_form(deserialize(tx), i, script, amount, modtx = segwit_signature_form(deserialize(tx), i, redeem_script, amount,
hashcode, decoder_func=lambda x: x) hashcode, decoder_func=lambda x: x)
return ecdsa_tx_sign(modtx, pk, hashcode) return ecdsa_tx_sign(modtx, pk, hashcode)
@ -819,6 +845,16 @@ def apply_multisignatures(*args):
txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script]) txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
return serialize(txobj) return serialize(txobj)
def apply_freeze_signature(tx, i, redeem_script, sig):
if isinstance(redeem_script, str):
redeem_script = binascii.unhexlify(redeem_script)
if isinstance(sig, str):
sig = binascii.unhexlify(sig)
txobj = deserialize(tx)
txobj["ins"][i]["script"] = ""
txobj["ins"][i]["txinwitness"] = [sig, redeem_script]
return serialize(txobj)
def mktx(ins, outs, version=1, locktime=0): def mktx(ins, outs, version=1, locktime=0):
""" Given a list of input dicts with key "output" """ Given a list of input dicts with key "output"
which are txid:n strings in hex, and a list of outputs which are txid:n strings in hex, and a list of outputs

2
jmclient/jmclient/cryptoengine.py

@ -287,4 +287,4 @@ ENGINES = {
TYPE_P2PKH: BTC_P2PKH, TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH, TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH TYPE_P2WPKH: BTC_P2WPKH
} }

56
jmclient/test/test_tx_creation.py

@ -6,6 +6,8 @@ import time
import binascii import binascii
import struct import struct
from commontest import make_wallets, make_sign_and_push from commontest import make_wallets, make_sign_and_push
from binascii import unhexlify
>>>>>>> c158543... only allow pubkey in bytes for mk_freeze_script()
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
import pytest import pytest
@ -190,7 +192,7 @@ def test_spend_p2sh_utxos(setup_tx_creation):
tx = bitcoin.mktx(ins, outs) tx = bitcoin.mktx(ins, outs)
sigs = [] sigs = []
for priv in privs[:2]: for priv in privs[:2]:
sigs.append(bitcoin.multisign(tx, 0, script, binascii.hexlify(priv).decode('ascii'))) sigs.append(bitcoin.get_p2sh_signature(tx, 0, script, binascii.hexlify(priv).decode('ascii')))
tx = bitcoin.apply_multisignatures(tx, 0, script, sigs) tx = bitcoin.apply_multisignatures(tx, 0, script, sigs)
txid = jm_single().bc_interface.pushtx(tx) txid = jm_single().bc_interface.pushtx(tx)
assert txid assert txid
@ -268,7 +270,7 @@ def test_spend_p2wsh(setup_tx_creation):
sigs = [] sigs = []
for priv in privs[i*2:i*2+2]: for priv in privs[i*2:i*2+2]:
# sign input j with each of 2 keys # sign input j with each of 2 keys
sig = bitcoin.multisign(tx, i, redeemScripts[i], priv, amount=amount) sig = bitcoin.get_p2sh_signature(tx, i, redeemScripts[i], priv, amount=amount)
sigs.append(sig) sigs.append(sig)
# check that verify_tx_input correctly validates; # check that verify_tx_input correctly validates;
assert bitcoin.verify_tx_input(tx, i, scriptPubKeys[i], sig, assert bitcoin.verify_tx_input(tx, i, scriptPubKeys[i], sig,
@ -278,6 +280,56 @@ def test_spend_p2wsh(setup_tx_creation):
txid = jm_single().bc_interface.pushtx(tx) txid = jm_single().bc_interface.pushtx(tx)
assert txid assert txid
def ensure_bip65_activated():
#on regtest bip65 activates on height 1351
#https://github.com/bitcoin/bitcoin/blob/1d1f8bbf57118e01904448108a104e20f50d2544/src/chainparams.cpp#L262
BIP65Height = 1351
current_height = jm_single().bc_interface.rpc("getblockchaininfo", [])["blocks"]
until_bip65_activation = BIP65Height - current_height + 1
if until_bip65_activation > 0:
jm_single().bc_interface.tick_forward_chain(until_bip65_activation)
def test_spend_freeze_script(setup_tx_creation):
ensure_bip65_activated()
wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
mediantime = jm_single().bc_interface.rpc("getblockchaininfo", [])["mediantime"]
timeoffset_success_tests = [(2, False), (-60*60*24*30, True), (60*60*24*30, False)]
for timeoffset, required_success in timeoffset_success_tests:
#generate keypair
priv = "aa"*32 + "01"
pub = unhexlify(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)
#fund frozen funds address
amount = 100000000
funding_ins_full = wallet_service.select_utxos(0, amount)
funding_txid = make_sign_and_push(funding_ins_full, wallet_service, amount, output_addr=addr)
assert funding_txid
#spend frozen funds
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)
assert push_success == required_success
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_tx_creation(): def setup_tx_creation():

Loading…
Cancel
Save