mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-08 22:35:15 +08:00
341 lines
11 KiB
Python
341 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack-client
|
|
~~~~~
|
|
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2016 by Blockstack.org
|
|
|
|
This file is part of Blockstack-client.
|
|
|
|
Blockstack-client is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Blockstack-client is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with Blockstack-client. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from binascii import hexlify, unhexlify
|
|
from decimal import *
|
|
|
|
import virtualchain
|
|
|
|
from binascii import hexlify, unhexlify
|
|
|
|
from virtualchain.lib.ecdsalib import *
|
|
from virtualchain.lib.hashing import *
|
|
|
|
from virtualchain import tx_extend, tx_sign_input
|
|
|
|
from .b40 import *
|
|
from .constants import MAGIC_BYTES, NAME_OPCODES, LENGTH_MAX_NAME, LENGTH_MAX_NAMESPACE_ID, TX_MIN_CONFIRMATIONS
|
|
from .keys import *
|
|
from .utxo import get_unspents
|
|
from .logger import get_logger
|
|
|
|
log = get_logger('blockstack-client')
|
|
|
|
class UTXOException(Exception):
|
|
pass
|
|
|
|
|
|
def add_magic_bytes(hex_script):
|
|
return '{}{}'.format(hexlify(MAGIC_BYTES), hex_script)
|
|
|
|
|
|
def common_checks(n):
|
|
"""
|
|
Checks common to both name and namespace_id
|
|
"""
|
|
if not n:
|
|
return False
|
|
|
|
if '+' in n or '.' in n:
|
|
return False
|
|
|
|
if len(n) > LENGTH_MAX_NAME:
|
|
# too long
|
|
return False
|
|
|
|
if not is_b40(n):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def is_namespace_valid(namespace_id):
|
|
"""
|
|
Is a namespace ID valid?
|
|
"""
|
|
if not common_checks(namespace_id):
|
|
return False
|
|
|
|
# validate max length
|
|
return len(namespace_id) <= LENGTH_MAX_NAMESPACE_ID
|
|
|
|
|
|
def is_name_valid(fqn):
|
|
"""
|
|
Is a fully-qualified name acceptable?
|
|
Return True if so
|
|
Return False if not
|
|
"""
|
|
|
|
if fqn.count('.') != 1:
|
|
return False
|
|
|
|
name, namespace_id = fqn.split('.')
|
|
|
|
if not common_checks(name):
|
|
return False
|
|
|
|
if not is_namespace_valid(namespace_id):
|
|
return False
|
|
|
|
# validate max length
|
|
return len(fqn) <= LENGTH_MAX_NAME
|
|
|
|
|
|
def is_valid_hash(value):
|
|
"""
|
|
Is this string a valid 32-byte hash?
|
|
"""
|
|
if not isinstance(value, (str, unicode)):
|
|
return False
|
|
|
|
strvalue = str(value)
|
|
|
|
if re.match(r'^[a-fA-F0-9]+$', strvalue) is None:
|
|
return False
|
|
|
|
return len(strvalue) == 64
|
|
|
|
|
|
def blockstack_script_to_hex(script):
|
|
""" Parse the readable version of a script, return the hex version.
|
|
"""
|
|
hex_script = ''
|
|
parts = script.split(' ')
|
|
for part in parts:
|
|
if part in NAME_OPCODES:
|
|
try:
|
|
hex_script += '{:02x}'.format(ord(NAME_OPCODES[part]))
|
|
except:
|
|
raise Exception('Invalid opcode: {}'.format(part))
|
|
elif part.startswith('0x'):
|
|
# literal hex string
|
|
hex_script += part[2:]
|
|
elif is_valid_int(part):
|
|
hex_part = '{:02x}'.format(int(part))
|
|
if len(hex_part) % 2 != 0:
|
|
hex_part = '0' + hex_part
|
|
hex_script += hex_part
|
|
elif is_hex(part) and len(part) % 2 == 0:
|
|
hex_script += part
|
|
else:
|
|
raise ValueError(
|
|
'Invalid script (at {}), contains invalid characters: {}'.format(part, script))
|
|
|
|
if len(hex_script) % 2 != 0:
|
|
raise ValueError('Invalid script: must have an even number of chars (got {}).'.format(hex_script))
|
|
|
|
return hex_script
|
|
|
|
|
|
def hash_name(name, script_pubkey, register_addr=None):
|
|
"""
|
|
Generate the hash over a name and hex-string script pubkey
|
|
"""
|
|
bin_name = b40_to_bin(name)
|
|
name_and_pubkey = bin_name + unhexlify(script_pubkey)
|
|
|
|
if register_addr is not None:
|
|
name_and_pubkey += str(register_addr)
|
|
|
|
return hex_hash160(name_and_pubkey)
|
|
|
|
|
|
def hash256_trunc128(data):
|
|
"""
|
|
Hash a string of data by taking its 256-bit sha256 and truncating it to 128 bits.
|
|
"""
|
|
return hexlify(bin_sha256(data)[0:16])
|
|
|
|
|
|
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).
|
|
Return (payer_address, payer_utxos) on success.
|
|
UTXOs will be in BTC, not satoshis!
|
|
"""
|
|
|
|
if private_key_info is None:
|
|
# just go with the address
|
|
unspents = get_unspents(address, utxo_client)
|
|
return addr, unspents
|
|
|
|
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):
|
|
"""
|
|
Get the requisite information to subsidize the given transaction:
|
|
* parse the given transaction (tx)
|
|
* calculate the operation-specific fee (op_fee)
|
|
* calculate the dust fee (dust_fee)
|
|
* calculate the transaction fee (tx_fee)
|
|
* calculate the paying key's UTXOs (payer_utxos)
|
|
* calculate the paying key's address (payer_address)
|
|
|
|
All fees will be in satoshis
|
|
|
|
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
|
|
)
|
|
|
|
# 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
|
|
dust_fee, op_fee = fee_cb(tx_inputs, tx_outputs)
|
|
|
|
if dust_fee is None or op_fee is None:
|
|
log.error('Invalid fee structure')
|
|
return {'error': 'Invalid fee structure'}
|
|
|
|
if dust_fee + op_fee + tx_fee > max_fee:
|
|
log.error('Op fee ({}) + dust fee ({}) exceeds maximum subsidy {}'.format(dust_fee, op_fee, max_fee))
|
|
return {'error': 'Fee exceeds maximum subsidy'}
|
|
|
|
else:
|
|
if tx_fee > 0:
|
|
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, op_fee, dust_fee, dust_fee + op_fee ))
|
|
|
|
res = {
|
|
'op_fee': op_fee,
|
|
'dust_fee': dust_fee,
|
|
'tx_fee': tx_fee,
|
|
'payer_address': payer_address,
|
|
'payer_utxos': payer_utxo_inputs,
|
|
'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
|
|
for the client to go sign off on.
|
|
* Add subsidization inputs/outputs
|
|
* Make sure the subsidy does not exceed the maximum subsidy fee
|
|
* Sign our inputs with SIGHASH_ANYONECANPAY (if subsidy_key_info is not None)
|
|
|
|
@tx_fee should be in satoshis
|
|
|
|
Returns the transaction; signed if subsidy_key_info is given; unsigned otherwise
|
|
Returns None if we can't get subsidy info
|
|
Raise ValueError if there are not enough inputs to subsidize
|
|
"""
|
|
|
|
subsidy_info = tx_get_subsidy_info(blockstack_tx, fee_cb, max_fee, subsidy_key_info, utxo_client, tx_fee=tx_fee, subsidy_address=subsidy_address)
|
|
if 'error' in subsidy_info:
|
|
log.error("Failed to get subsidy info: {}".format(subsidy_info['error']))
|
|
return None
|
|
|
|
payer_utxo_inputs = subsidy_info['payer_utxos']
|
|
payer_address = subsidy_info['payer_address']
|
|
op_fee = subsidy_info['op_fee']
|
|
dust_fee = subsidy_info['dust_fee']
|
|
tx_fee = subsidy_info['tx_fee']
|
|
tx_inputs = subsidy_info['ins']
|
|
|
|
# NOTE: virtualchain-formatted output; values are still in satoshis!
|
|
subsidy_output = tx_make_subsidization_output(
|
|
payer_utxo_inputs, payer_address, op_fee, dust_fee + tx_fee
|
|
)
|
|
|
|
# add our inputs and output (recall: virtualchain-formatted; so values are fundamental units (i.e. satoshis))
|
|
subsidized_tx = tx_extend(blockstack_tx, payer_utxo_inputs, [subsidy_output])
|
|
|
|
# sign each of our inputs with our key, but use
|
|
# SIGHASH_ANYONECANPAY so the client can sign its inputs
|
|
if subsidy_key_info is not None:
|
|
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=virtualchain.SIGHASH_ANYONECANPAY
|
|
)
|
|
|
|
else:
|
|
log.debug("Warning: no subsidy key given; transaction will be subsidized but not signed")
|
|
|
|
return subsidized_tx
|
|
|
|
|
|
def tx_get_unspents(address, utxo_client, min_confirmations=TX_MIN_CONFIRMATIONS):
|
|
"""
|
|
Given an address get unspent outputs (UTXOs)
|
|
Return array of UTXOs on success
|
|
Raise UTXOException on error
|
|
"""
|
|
|
|
if min_confirmations is None:
|
|
min_confirmations = TX_MIN_CONFIRMATIONS
|
|
|
|
if min_confirmations != TX_MIN_CONFIRMATIONS:
|
|
log.warning("Using UTXOs with {} confirmations instead of the default {}".format(min_confirmations, TX_MIN_CONFIRMATIONS))
|
|
|
|
data = get_unspents(address, utxo_client)
|
|
|
|
try:
|
|
assert type(data) == list, "No UTXO list returned"
|
|
for d in data:
|
|
assert isinstance(d, dict), 'Invalid UTXO information returned'
|
|
assert 'value' in d, 'Missing value in UTXOs from {}'.format(address)
|
|
|
|
except AssertionError, ae:
|
|
log.exception(ae)
|
|
raise UTXOException()
|
|
|
|
# filter minimum confirmations
|
|
return [d for d in data if d.get('confirmations', 0) >= min_confirmations]
|