Calculate the set of UTXOs that will be used over a sequence of transactions, including the ones that will be generated by transactions within the sequence. Do not assume that the whole set of UTXOs will be used by each transaction.

This commit is contained in:
Jude Nelson
2017-06-15 11:04:14 -04:00
parent 4a9143f610
commit e6fcd2ee46

View File

@@ -22,6 +22,7 @@
import os
import threading
import functools
from ..constants import *
from ..keys import *
@@ -232,6 +233,7 @@ class ScatterGather(object):
else:
# for testing purposes
for task_name, task_call in self.tasks.items():
log.debug("Run task '{}'".format(task_name))
res = ScatterGatherThread.do_work(task_call)
self.results[task_name] = res
@@ -472,6 +474,12 @@ def operation_sanity_checks(fqu_or_ns, operations, scatter_gather, payment_privk
_cleanup()
return {'error': 'The first imported name must have the namespace reveal address as its owner'}
# is this address usable?
res = _is_address_usable(import_addr)
if 'error' in res:
_cleanup()
return res
return {'status': True}
@@ -553,6 +561,45 @@ def interpret_operation_sanity_checks( operations, scatter_gather ):
return reply
def estimate_transaction_inputs(operations, inputs, owner_address=None, payment_address=None):
"""
Estimate the inputs that will be consumed by each
operation's transaction. These inputs do *not* correspond to the inputs
that will actually be used, but are represenatitive of the *quantity*
of inputs consumed by an operation and the *type* consumed. It does *not*
reflect the *value* consumed by the inputs.
The first operation consumes all inputs, and emits one output
that will be consumed by other inputs.
@owner_address or @payment_address must be given (but not both)
Returns {'status': True, 'inputs': [ [utxos], [utxos], ... ]}
"""
op_inputs = []
def _consolidate(inps):
ret = inps[0]
ret['value'] += sum([inp['value'] for inp in inps])
return ret
# one_input: what if all inputs were consolidated to a single input of the same value
one_input = _consolidate(inputs)
for i in xrange(0, len(operations)):
if i == 0:
# the first operation will consume everything, and emit a single output
# to be consumed by the next operation
op_inputs.append(inputs[:])
else:
# operations consume one input
op_inputs.append([one_input])
return {'status': True, 'inputs': op_inputs}
def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_info, owner_privkey_info, tx_fee_per_byte,
proxy=None, config_path=CONFIG_PATH, payment_address=None,
min_payment_confs=TX_MIN_CONFIRMATIONS, owner_address=None, transfer_address=None):
@@ -629,22 +676,46 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
if len(set(operations).intersection(set(name_operations))) > 0 and len(set(operations).intersection(set(namespace_operations))) > 0:
return {'error': 'Cannot mix name and namespace operations'}
if len(operations) != len(set(operations)):
return {'error': 'No duplicate operations allowed at this time'}
# first things first: get UTXOs for owner and payment addresses
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
log.debug("Getting UTXOs for {}".format(owner_address))
owner_utxos = get_utxos(owner_address, utxo_client=utxo_client, config_path=config_path, min_confirmations=min_payment_confs)
if 'error' in owner_utxos:
log.error("Failed to get UTXOs for {}: {}".format(owner_address, owner_utxos['error']))
return {'error': 'Failed to get UTXOs for {}'.format(owner_address)}
payment_utxos = get_utxos(payment_address, utxo_client=utxo_client, config_path=config_path, min_confirmations=min_payment_confs)
if 'error' in payment_utxos:
log.error("Failed to get UTXOs for {}: {}".format(payment_address, payment_utxos['error']))
return {'error': 'Failed to get UTXOs for {}'.format(payment_address)}
balance = sum([utxo.get('value', None) for utxo in payment_utxos])
log.debug("Balance of {} is {} satoshis".format(payment_address, balance))
# find out what our UTXOs will look like for each operation
estimated_payment_inputs = estimate_transaction_inputs(operations, payment_utxos, payment_address=payment_address)['inputs']
estimated_owner_inputs = estimate_transaction_inputs(operations, owner_utxos, owner_address=owner_address)['inputs']
log.debug("Get total operation fees for running '{}' on {} owned by {} paid by {}".format(','.join(operations), name_or_ns, owner_address, payment_address))
for i in xrange(0, len(operations)):
if BLOCKSTACK_TEST:
log.debug("Operation {} may consume owner inputs\n{}".format(operations[i], json.dumps(estimated_owner_inputs[i], indent=4, sort_keys=True)))
log.debug("Operation {} may consume payment inputs\n{}".format(operations[i], json.dumps(estimated_payment_inputs[i], indent=4, sort_keys=True)))
else:
log.debug("Operation {} may consume up to {} owner inputs and {} payment inputs".format(operations[i], len(estimated_owner_inputs[i]), len(estimated_payment_inputs[i])))
def _get_balance():
"""
get payment address balance (scatter/gather worker)
"""
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
balance = get_balance(payment_address, config_path=config_path, utxo_client=utxo_client)
if balance is None:
msg = 'Failed to get balance'
return {'error': msg}
else:
return {'status': balance}
return {'status': balance, 'balance': balance}
def _estimate_preorder_tx():
def _estimate_preorder_tx( operation_index ):
"""
Estimate preorder tx cost
Return {'status': True, 'name_cost': ..., 'tx_fee': ..., 'insufficient': ...} on success
@@ -662,13 +733,12 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Could not get name price'}
try:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
preorder_tx_fee = estimate_preorder_tx_fee(
name_or_ns, name_cost, owner_address, payment_address, tx_fee_per_byte, utxo_client,
owner_privkey_params=owner_privkey_params,
name_or_ns, name_cost, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True
)
@@ -689,7 +759,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_PREORDER fee estimation. Please try again.'}
def _estimate_register_tx():
def _estimate_register_tx( operation_index ):
"""
Estimate register tx cost
Return {'status': True, 'tx_fee': ..., 'insufficient': ...} on success
@@ -697,13 +767,12 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
"""
try:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
register_tx_fee = estimate_register_tx_fee(
name_or_ns, owner_address, payment_address, tx_fee_per_byte, utxo_client,
owner_privkey_params=owner_privkey_params,
name_or_ns, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True
)
@@ -723,22 +792,21 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_REGISTRATION fee estimation. Please try again.'}
def _estimate_update_tx():
def _estimate_update_tx( operation_index ):
"""
Estimate update tx fee
Return {'status': True, 'tx_fee': ..., 'insufficient': ...} on success
Return {'error': ...} on failure
"""
try:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
estimate = False
update_tx_fee = estimate_update_tx_fee(
name_or_ns, payment_privkey_info, owner_address, tx_fee_per_byte, utxo_client,
owner_privkey_params=owner_privkey_params,
config_path=config_path, payment_address=payment_address, include_dust=True
name_or_ns, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index], owner_utxos=estimated_owner_inputs[operation_index],
config_path=config_path, include_dust=True
)
if update_tx_fee is not None:
@@ -761,7 +829,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_UPDATE fee estimation. Please try again.'}
def _estimate_transfer_tx():
def _estimate_transfer_tx( operation_index ):
"""
Estiamte transfer tx fee
Return {'status': True, 'tx_fee': ..., 'insufficient': ...} on success
@@ -773,16 +841,15 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
try:
if transfer_address is not None:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
estimate = False
transfer_tx_fee = estimate_transfer_tx_fee(
name_or_ns, payment_privkey_info, owner_address, tx_fee_per_byte, utxo_client,
owner_privkey_params=owner_privkey_params,
config_path=config_path, payment_address=payment_address, include_dust=True
name_or_ns, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index], owner_utxos=estimated_owner_inputs[operation_index],
config_path=config_path, include_dust=True
)
if transfer_tx_fee is not None:
@@ -808,22 +875,21 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_TRANSFER fee estimation. Please try again.'}
def _estimate_revoke_tx():
def _estimate_revoke_tx( operation_index ):
"""
Estimate revoke tx fee
Return {'status': True, 'tx_fee': ..., 'insufficient': ...} on success
Return {'error': ...} on failure
"""
try:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
estimate = False
tx_fee = estimate_revoke_tx_fee(
name_or_ns, payment_privkey_info, owner_address, tx_fee_per_byte, utxo_client,
owner_privkey_params=owner_privkey_params,
name_or_ns, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index], owner_utxos=estimated_owner_inputs[operation_index],
config_path=config_path, include_dust=True
)
@@ -847,7 +913,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_REVOKE fee estimation. Please try again.'}
def _estimate_renewal_tx():
def _estimate_renewal_tx( operation_index ):
"""
Estimate renewal tx fee
Return {'status': True, 'name_cost': ..., 'tx_fee': ..., 'insufficient': ...} on success
@@ -862,7 +928,6 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Could not get name price'}
try:
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
insufficient_funds = False
@@ -870,6 +935,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
tx_fee = estimate_renewal_tx_fee(
name_or_ns, name_cost, payment_privkey_info, owner_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index], owner_utxos=estimated_owner_inputs[operation_index],
config_path=config_path, include_dust=True
)
@@ -893,7 +959,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAME_RENEWAL fee estimation. Please try again.'}
def _estimate_namespace_preorder_tx():
def _estimate_namespace_preorder_tx( operation_index ):
"""
Estimate namespace preorder tx fee
Return {'status': True, 'namespace_cost': ..., 'tx_fee': ..., 'insufficient': ..., 'estimate': ...} on success
@@ -913,7 +979,11 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
try:
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
tx_fee = estimate_namespace_preorder_tx_fee( name_or_ns, namespace_cost, payment_address, tx_fee_per_byte, utxo_client, config_path=config_path, include_dust=True )
log.debug("Estimate namespace preorder TX fee (from {})".format(payment_address))
tx_fee = estimate_namespace_preorder_tx_fee( name_or_ns, namespace_cost, payment_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True )
if tx_fee is not None:
tx_fee = int(tx_fee)
@@ -932,7 +1002,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAMESPACE_PREORDER fee estimation. Please try again.'}
def _estimate_namespace_reveal_tx():
def _estimate_namespace_reveal_tx( operation_index ):
"""
Estimate namespace reveal tx fee
payment_address here corresponds to the NAMESPACE_PREORDER key's address
@@ -945,7 +1015,11 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
try:
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
tx_fee = estimate_namespace_reveal_tx_fee( name_or_ns, payment_address, tx_fee_per_byte, utxo_client, config_path=config_path, include_dust=True )
log.debug("Estimate namespace reveal TX fee (from {})".format(payment_address))
tx_fee = estimate_namespace_reveal_tx_fee( name_or_ns, payment_privkey_info, tx_fee_per_byte, utxo_client,
payment_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True )
if tx_fee is not None:
tx_fee = int(tx_fee)
@@ -964,7 +1038,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAMESPACE_REVEAL fee estimation. Please try again.'}
def _estimate_namespace_ready_tx():
def _estimate_namespace_ready_tx( operation_index ):
"""
Estimate namespace ready tx fee
payment_address here corresponds to the reveal address (i.e. the reveal key pays for the NAMESPACE_READY)
@@ -977,7 +1051,11 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
try:
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
tx_fee = estimate_namespace_ready_tx_fee( name_or_ns, payment_address, tx_fee_per_byte, utxo_client, config_path=config_path, include_dust=True )
log.debug("Estimate namespace ready TX fee (from {})".format(payment_address))
tx_fee = estimate_namespace_ready_tx_fee( name_or_ns, payment_privkey_info, tx_fee_per_byte, utxo_client,
revealer_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True )
if tx_fee is not None:
tx_fee = int(tx_fee)
@@ -996,7 +1074,7 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
return {'error': 'Failed to query UTXO provider for NAMESPACE_READY fee estimation. Please try again'}
def _estimate_name_import_tx():
def _estimate_name_import_tx( operation_index ):
"""
Estimate name import tx fee
payment_address is address of the derived reveal key used to issue the import
@@ -1012,7 +1090,9 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
try:
utxo_client = get_utxo_provider_client(config_path=config_path, min_confirmations=min_payment_confs)
tx_fee = estimate_name_import_tx_fee( name_or_ns, payment_address, transfer_address, tx_fee_per_byte, utxo_client, config_path=config_path, include_dust=True )
tx_fee = estimate_name_import_tx_fee( name_or_ns, payment_privkey_info, transfer_address, tx_fee_per_byte, utxo_client,
importer_utxos=estimated_payment_inputs[operation_index],
config_path=config_path, include_dust=True )
if tx_fee is not None:
tx_fee = int(tx_fee)
@@ -1050,14 +1130,18 @@ def get_operation_fees(name_or_ns, operations, scatter_gather, payment_privkey_i
sg.add_task('get_balance', _get_balance)
# queue each operation
for op in set(operations):
for i in xrange(0, len(operations)):
op = operations[i]
if op not in tx_fee_checks.keys():
log.error("Unrecognized operation {}".format(op))
return {'error': 'Unrecognized operation'}
check_name = '{}_tx_fee'.format(op)
sg.add_task( check_name, tx_fee_checks[op] )
log.debug("Add task {} ({})".format(check_name, tx_fee_checks[op]))
check_func = functools.partial(tx_fee_checks[op], i)
sg.add_task( check_name, check_func )
# scatter/gather primed!
return {'status': True}
@@ -1103,7 +1187,7 @@ def interpret_operation_fees( operations, scatter_gather, balance=None ):
log.error("Failed to get balance")
else:
balance = results['get_balance']['status']
balance = results['get_balance']['balance']
log.debug("Balance is {} satoshis".format(balance))
failed_tasks = []
@@ -1227,7 +1311,7 @@ def check_operations( fqu_or_ns, operations, owner_privkey_info, payment_privkey
'opchecks': opchecks,
'tx_fee_per_byte': tx_fee_per_byte}
balance = sg.get_result('get_balance')['status']
balance = sg.get_result('get_balance')['balance']
if balance < opchecks['total_estimated_cost']:
msg = 'Address {} does not have enough balance (need {}, have {}).'
@@ -1394,5 +1478,6 @@ def check_name_import(name, importer_privkey_info, recipient_address, min_paymen
Verify that a name import can go through
@importer_privkey_info is the private key information derived from the reveal private key. It will be used as the "payment" key by the op-checker
"""
required_checks = ['is_namespace_still_revealed', 'is_payment_address_usable', 'is_name_import_key', 'owner_can_receive']
return _check_op(name, 'name_import', required_checks, None, importer_privkey_info, owner_address=recipient_address, min_payment_confs=min_payment_confs, config_path=config_path, proxy=proxy)
required_checks = ['is_namespace_still_revealed', 'is_payment_address_usable', 'is_owner_address_usable', 'is_name_import_key', 'recipient_can_receive']
return _check_op(name, 'name_import', required_checks, importer_privkey_info, importer_privkey_info, transfer_address=recipient_address, min_payment_confs=min_payment_confs, config_path=config_path, proxy=proxy)