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
from jmbitcoin.secp256k1_main import *
from jmbitcoin.bech32 import *
import jmbitcoin as btc
P2PKH_PRE, P2PKH_POST = b'\x76\xa9\x14', b'\x88\xac'
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)
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):
""" 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)
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):
""" Note that this is not used internally, in
the jmbitcoin package, to deserialize() transactions;
@ -603,10 +610,14 @@ def deserialize_script(scriptinp):
def serialize_script_unit(unit):
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)
else:
elif unit < 256:
return from_int_to_byte(unit)
else:
return b'\x04' + struct.pack(b"<I", unit)
elif unit is None:
return b'\x00'
else:
@ -634,15 +645,30 @@ def serialize_script(script):
else:
return result
def mk_multisig_script(pubs, k):
""" Given a list of pubkeys and an integer k,
construct a multisig script for k of N, where N is
the length of the list `pubs`; script is returned
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
@ -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
`native` flags that native segwit p2wpkh signing is to be done. Note
that signing multisig is to be done with the alternative functions
multisign or p2wsh_multisign (and non N of N multisig scripthash
signing is not currently supported).
get_p2sh_signature or get_p2wsh_signature (and non N of N multisig
scripthash signing is not currently supported).
"""
if isinstance(tx, basestring) and not isinstance(tx, bytes):
tx = binascii.unhexlify(tx)
@ -762,28 +788,28 @@ def signall(tx, priv):
tx = sign(tx, i, priv)
return tx
def multisign(tx, i, script, pk, amount=None, hashcode=SIGHASH_ALL):
""" Tx is assumed to be serialized. The script passed here is
the redeemscript, for example the output of mk_multisig_script.
def get_p2sh_signature(tx, i, redeem_script, pk, amount=None, hashcode=SIGHASH_ALL):
"""
Tx is assumed to be serialized. redeem_script is for example the
output of mk_multisig_script.
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.
"""
if isinstance(tx, str):
tx = binascii.unhexlify(tx)
if isinstance(script, str):
script = binascii.unhexlify(script)
if isinstance(redeem_script, str):
redeem_script = binascii.unhexlify(redeem_script)
if amount:
return p2wsh_multisign(tx, i, script, pk, amount, hashcode)
modtx = signature_form(tx, i, script, hashcode)
return get_p2wsh_signature(tx, i, redeem_script, pk, amount, hashcode)
modtx = signature_form(tx, i, redeem_script, 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`.
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)
return ecdsa_tx_sign(modtx, pk, hashcode)
@ -819,6 +845,16 @@ def apply_multisignatures(*args):
txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
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):
""" Given a list of input dicts with key "output"
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_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH
}
}

56
jmclient/test/test_tx_creation.py

@ -6,6 +6,8 @@ import time
import binascii
import struct
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 pytest
@ -190,7 +192,7 @@ def test_spend_p2sh_utxos(setup_tx_creation):
tx = bitcoin.mktx(ins, outs)
sigs = []
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)
txid = jm_single().bc_interface.pushtx(tx)
assert txid
@ -268,7 +270,7 @@ def test_spend_p2wsh(setup_tx_creation):
sigs = []
for priv in privs[i*2:i*2+2]:
# 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)
# check that verify_tx_input correctly validates;
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)
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")
def setup_tx_creation():

Loading…
Cancel
Save