mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-21 09:55:29 +08:00
move most bitcoin-specific functionality to virtualchain, and use virtualchain's API to access them
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user