move most bitcoin-specific functionality to virtualchain, and use virtualchain's API to access them

This commit is contained in:
Jude Nelson
2017-04-20 17:44:42 -04:00
parent c1b94c1227
commit 762ec22eee

View File

@@ -24,17 +24,17 @@
from binascii import hexlify, unhexlify
from decimal import *
import pybitcoin
import bitcoin
import ecdsa
import hashlib
import virtualchain
from hashlib import sha256
from binascii import hexlify, unhexlify
from utilitybelt import is_hex
from virtualchain import tx_serialize, tx_deserialize, tx_script_to_asm, tx_output_parse_scriptPubKey
from virtualchain.lib.ecdsalib import *
from virtualchain.lib.hashing import *
from virtualchain import tx_script_to_asm, tx_extend, \
tx_sign_all_unsigned_inputs, tx_sign_input
from .b40 import *
from .constants import MAGIC_BYTES, NAME_OPCODES, LENGTH_MAX_NAME, LENGTH_MAX_NAMESPACE_ID, TX_MIN_CONFIRMATIONS, BLOCKSTACK_TEST
@@ -52,28 +52,6 @@ def add_magic_bytes(hex_script):
return '{}{}'.format(hexlify(MAGIC_BYTES), hex_script)
def bin_hash160(s, hex_format=False):
""" s is in hex or binary format
"""
if hex_format and is_hex(s):
s = unhexlify(s)
return hashlib.new('ripemd160', bin_sha256(s)).digest()
def hex_hash160(s, hex_format=False):
""" s is in hex or binary format
"""
if hex_format and is_hex(s):
s = unhexlify(s)
return hexlify(bin_hash160(s))
def bin_sha256(bin_s):
return sha256(bin_s).digest()
def bin_double_sha256(bin_s):
return bin_sha256(bin_sha256(bin_s))
def common_checks(n):
"""
Checks common to both name and namespace_id
@@ -124,7 +102,7 @@ def is_name_valid(fqn):
return False
# validate max length
return len(fqn) < LENGTH_MAX_NAME
return len(fqn) <= LENGTH_MAX_NAME
def is_valid_hash(value):
@@ -193,233 +171,6 @@ def hash256_trunc128(data):
return hexlify(bin_sha256(data)[0:16])
def tx_output_is_op_return(output):
"""
Is an output's script an OP_RETURN script?
"""
return int(output['script_hex'][0:2], 16) == virtualchain.opcodes.OPCODE_VALUES['OP_RETURN']
def tx_extend(partial_tx_hex, new_inputs, new_outputs):
"""
Given an unsigned serialized transaction, add more inputs and outputs to it.
@new_inputs and @new_outputs will be virtualchain-formatted:
* new_inputs[i] will have {'output_index': ..., 'script_hex': ..., 'transaction_hash': ...}
* new_outputs[i] will have {'script_hex': ..., 'value': ... (in fundamental units, e.g. satoshis!)}
"""
# recover tx
tx = tx_deserialize(partial_tx_hex)
tx_inputs, tx_outputs = tx['vin'], tx['vout']
locktime, version = tx['locktime'], tx['version']
# format new inputs
btc_new_inputs = []
for inp in new_inputs:
new_inp = {
'vout': inp['output_index'],
'txid': inp['transaction_hash'],
'scriptSig': {
'asm': tx_script_to_asm(inp['script_hex']),
'hex': inp['script_hex']
}
}
btc_new_inputs.append(new_inp)
# format new outputs
btc_new_outputs = []
for i, new_output in enumerate(new_outputs):
new_outp = {
'n': i + len(tx_outputs),
'value': Decimal(new_output['value']) / Decimal(10 ** 8),
'scriptPubKey': tx_output_parse_scriptPubKey(new_output['script_hex']),
'script_hex': new_output['script_hex']
}
btc_new_outputs.append(new_outp)
new_tx = {
'vin': tx_inputs + btc_new_inputs,
'vout': tx_outputs + btc_new_outputs,
'locktime': locktime,
'version': version
}
# new tx
new_unsigned_tx = tx_serialize(new_tx)
return new_unsigned_tx
def tx_make_subsidization_output(payer_utxo_inputs, payer_address, op_fee, dust_fee):
"""
Given the set of utxo inputs for both the client and payer, as well as the client's
desired tx outputs, generate the inputs and outputs that will cause the payer to pay
the operation's fees and dust fees.
The client should send its own address as an input, with the same amount of BTC as the output.
Return the payer output to include in the transaction on success, which should pay for the operation's
fee and dust.
Raise ValueError it here aren't enough inputs to subsidize
"""
return {
'script_hex': virtualchain.make_payment_script(payer_address),
'value': virtualchain.calculate_change_amount(payer_utxo_inputs, op_fee, int(round(dust_fee)))
}
def tx_make_input_signature(tx, idx, script, privkey_str, hashcode):
"""
Sign a single input of a transaction, given the serialized tx,
the input index, the output's scriptPubkey, and the hashcode.
privkey_str must be a hex-encoded private key
TODO: move to virtualchain
Return the hex signature.
"""
pk = ecdsa_private_key(str(privkey_str))
pubk = pk.public_key()
priv = pk.to_hex()
pub = pubk.to_hex()
addr = virtualchain.address_reencode( pubk.address() )
signing_tx = bitcoin.signature_form(tx, idx, script, hashcode)
txhash = bitcoin.bin_txhash(signing_tx, hashcode)
# sign using uncompressed private key
pk_uncompressed_hex, pubk_uncompressed_hex = get_uncompressed_private_and_public_keys(priv)
sigb64 = sign_digest( txhash.encode('hex'), priv )
# sanity check
assert verify_digest( txhash.encode('hex'), pubk_uncompressed_hex, sigb64 )
sig_r, sig_s = decode_signature(sigb64)
sig_bin = ecdsa.util.sigencode_der( sig_r, sig_s, ecdsa.SECP256k1.order )
sig = sig_bin.encode('hex') + bitcoin.encode(hashcode, 16, 2)
return sig
def tx_sign_multisig(tx, idx, redeem_script, private_keys, hashcode=bitcoin.SIGHASH_ALL):
"""
Sign a p2sh multisig input.
Return the signed transaction
TODO: move to virtualchain
"""
# sign in the right order. map all possible public keys to their private key
privs = {}
for pk in private_keys:
pubk = ecdsa_private_key(pk).public_key().to_hex()
compressed_pubkey = keylib.key_formatting.compress(pubk)
uncompressed_pubkey = keylib.key_formatting.decompress(pubk)
privs[compressed_pubkey] = pk
privs[uncompressed_pubkey] = pk
m, public_keys = virtualchain.parse_multisig_redeemscript(str(redeem_script))
used_keys, sigs = [], []
for public_key in public_keys:
if public_key not in privs:
continue
if len(used_keys) == m:
break
assert public_key not in used_keys, 'Tried to reuse key {}'.format(public_key)
pk_str = privs[public_key]
used_keys.append(public_key)
sig = tx_make_input_signature(tx, idx, redeem_script, pk_str, hashcode)
sigs.append(sig)
assert len(used_keys) == m, 'Missing private keys (used {}, required {})'.format(len(used_keys), m)
return bitcoin.apply_multisignatures(tx, idx, str(redeem_script), sigs)
def tx_sign_singlesig(tx, idx, private_key_info, hashcode=bitcoin.SIGHASH_ALL):
"""
Sign a p2pkh input
Return the signed transaction
TODO: move to virtualchain
NOTE: implemented here instead of bitcoin, since bitcoin.sign() can cause a stack overflow
while converting the private key to a public key.
"""
pk = ecdsa_private_key(str(private_key_info))
pubk = pk.public_key()
pub = pubk.to_hex()
addr = virtualchain.address_reencode( pubk.address() )
script = virtualchain.make_payment_script(addr)
sig = tx_make_input_signature(tx, idx, script, private_key_info, hashcode)
txobj = bitcoin.deserialize(str(tx))
txobj['ins'][idx]['script'] = bitcoin.serialize_script([sig, pub])
return bitcoin.serialize(txobj)
def tx_sign_input(blockstack_tx, idx, private_key_info, hashcode=bitcoin.SIGHASH_ALL):
"""
Sign a particular input in the given transaction.
@private_key_info can either be a private key, or it can be a dict with 'redeem_script' and 'private_keys' defined
"""
if is_singlesig(private_key_info):
# single private key
return tx_sign_singlesig(blockstack_tx, idx, private_key_info, hashcode=hashcode)
elif is_multisig(private_key_info):
redeem_script = private_key_info['redeem_script']
private_keys = private_key_info['private_keys']
redeem_script = str(redeem_script)
# multisig
return tx_sign_multisig(blockstack_tx, idx, redeem_script, private_keys, hashcode=bitcoin.SIGHASH_ALL)
else:
if BLOCKSTACK_TEST:
log.debug("Invalid private key info: {}".format(private_key_info))
raise ValueError("Invalid private key info")
def tx_sign_all_unsigned_inputs(private_key_info, unsigned_tx_hex):
"""
Sign all unsigned inputs in the given transaction.
@private_key_info: either a hex private key, or a dict with 'private_keys' and 'redeem_script'
defined as keys.
@unsigned_hex_tx: hex transaction with unsigned inputs
Returns: signed hex transaction
"""
inputs, outputs, locktime, version = pybitcoin.deserialize_transaction(unsigned_tx_hex)
tx_hex = unsigned_tx_hex
for i, input in enumerate(inputs):
if input['script_sig']:
continue
# tx with index i signed with privkey
tx_hex = tx_sign_input(str(unsigned_tx_hex), i, private_key_info)
unsigned_tx_hex = tx_hex
return tx_hex
def tx_get_address_and_utxos(private_key_info, utxo_client, address=None):
"""
Get information about a private key (or a set of private keys used for multisig).
@@ -432,19 +183,9 @@ def tx_get_address_and_utxos(private_key_info, utxo_client, address=None):
unspents = get_unspents(address, utxo_client)
return addr, unspents
if is_singlesig(private_key_info):
payer_address = virtualchain.address_reencode( ecdsa_private_key(private_key_info).public_key().address() )
payer_utxos = get_unspents(payer_address, utxo_client)
return payer_address, payer_utxos
if is_multisig(private_key_info):
redeem_script = str(private_key_info['redeem_script'])
addr = virtualchain.make_multisig_address(redeem_script)
unspents = get_unspents(addr, utxo_client)
return addr, unspents
raise ValueError('Invalid private key info')
addr = virtualchain.get_privkey_address(private_key_info)
payer_utxos = get_unspents(addr, utxo_client)
return addr, payer_utxos
def tx_get_subsidy_info(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_client, subsidy_address=None, tx_fee=0):
@@ -462,19 +203,16 @@ def tx_get_subsidy_info(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_c
Return a dict with the above
Return {'error': ...} on error
"""
from .tx import deserialize_tx
# get subsidizer key info
payer_address, payer_utxo_inputs = tx_get_address_and_utxos(
subsidy_key_info, utxo_client, address=subsidy_address
)
tx = tx_deserialize(blockstack_tx)
# NOTE: will be in BTC; convert to satoshis below
tx_inputs, tx_outputs = tx['vin'], tx['vout']
for tx_output in tx_outputs:
tx_output['value'] = int(tx_output['value'] * Decimal(10 ** 8))
# NOTE: units are in satoshis
tx_inputs, tx_outputs = deserialize_tx(blockstack_tx)
# what's the fee? does it exceed the subsidy?
# NOTE: units are satoshis here
@@ -490,9 +228,9 @@ def tx_get_subsidy_info(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_c
else:
if tx_fee > 0:
log.debug('{} will subsidize {} (ops+dust) + {} (txfee) satoshi'.format(payer_address, dust_fee + op_fee, tx_fee ))
log.debug('{} will subsidize {} (ops) + {} (dust) ({}) + {} (txfee) satoshi'.format(payer_address, op_fee, dust_fee, dust_fee + op_fee, tx_fee ))
else:
log.debug('{} will subsidize {} (ops+dust) satoshi'.format(payer_address, dust_fee + op_fee ))
log.debug('{} will subsidize {} (ops) + {} (dust) ({}) satoshi'.format(payer_address, op_fee, dust_fee, dust_fee + op_fee ))
res = {
'op_fee': op_fee,
@@ -500,11 +238,32 @@ def tx_get_subsidy_info(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_c
'tx_fee': tx_fee,
'payer_address': payer_address,
'payer_utxos': payer_utxo_inputs,
'tx': tx
'ins': tx_inputs,
'outs': tx_outputs
}
return res
def tx_make_subsidization_output(payer_utxo_inputs, payer_address, op_fee, dust_fee):
"""
Given the set of utxo inputs for both the client and payer, as well as the client's
desired tx outputs, generate the inputs and outputs that will cause the payer to pay
the operation's fees and dust fees.
The client should send its own address as an input, with the same amount of BTC as the output.
Return the payer output to include in the transaction on success, which should pay for the operation's
fee and dust.
Raise ValueError it here aren't enough inputs to subsidize
"""
return {
'script': virtualchain.make_payment_script(payer_address),
'value': virtualchain.calculate_change_amount(payer_utxo_inputs, op_fee, int(round(dust_fee)))
}
def tx_make_subsidizable(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_client, tx_fee=0, subsidy_address=None):
"""
Given an unsigned serialized transaction from Blockstack, make it into a subsidized transaction
@@ -530,8 +289,7 @@ def tx_make_subsidizable(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_
op_fee = subsidy_info['op_fee']
dust_fee = subsidy_info['dust_fee']
tx_fee = subsidy_info['tx_fee']
tx = subsidy_info['tx']
tx_inputs = tx['vin']
tx_inputs = subsidy_info['ins']
# NOTE: virtualchain-formatted output; values are still in satoshis!
subsidy_output = tx_make_subsidization_output(
@@ -547,7 +305,7 @@ def tx_make_subsidizable(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_
for i in range(len(payer_utxo_inputs)):
idx = i + len(tx_inputs)
subsidized_tx = tx_sign_input(
subsidized_tx, idx, subsidy_key_info, hashcode=bitcoin.SIGHASH_ANYONECANPAY
subsidized_tx, idx, subsidy_key_info, hashcode=virtualchain.SIGHASH_ANYONECANPAY
)
else: