diff --git a/jmbitcoin/jmbitcoin/secp256k1_transaction.py b/jmbitcoin/jmbitcoin/secp256k1_transaction.py index 34df966..b8982da 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_transaction.py +++ b/jmbitcoin/jmbitcoin/secp256k1_transaction.py @@ -670,6 +670,13 @@ def mk_freeze_script(pub, locktime): btc.OP_CHECKSIG] return binascii.hexlify(serialize_script(scr)).decode() +def mk_burn_script(data): + if not isinstance(data, bytes): + raise TypeError("data must be in bytes") + data = binascii.hexlify(data).decode() + scr = [btc.OP_RETURN, data] + return serialize_script(scr) + # Signing and verifying def verify_tx_input(tx, i, script, sig, pub, scriptCode=None, amount=None): diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index c00fe7e..14930ea 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -20,8 +20,9 @@ from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError, from .cryptoengine import BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError from .configure import (load_test_config, load_program_config, get_p2pk_vbyte, jm_single, get_network, update_persist_config, - validate_address, get_irc_mchannels, get_blockchain_interface_instance, - get_p2sh_vbyte, set_config, is_segwit_mode, is_native_segwit_mode) + validate_address, is_burn_destination, get_irc_mchannels, + get_blockchain_interface_instance, get_p2sh_vbyte, set_config, is_segwit_mode, + is_native_segwit_mode) from .blockchaininterface import (BlockchainInterface, RegtestBitcoinCoreInterface, BitcoinCoreInterface) from .electruminterface import ElectrumInterface diff --git a/jmclient/jmclient/cli_options.py b/jmclient/jmclient/cli_options.py index 95dee6c..3e1e584 100644 --- a/jmclient/jmclient/cli_options.py +++ b/jmclient/jmclient/cli_options.py @@ -442,7 +442,7 @@ def get_tumbler_parser(): def get_sendpayment_parser(): parser = OptionParser( usage= - 'usage: %prog [options] wallet_file amount destaddr\n' + + 'usage: %prog [options] wallet_file amount destination\n' + ' %prog [options] wallet_file bitcoin_uri', description='Sends a single payment from a given mixing depth of your ' + diff --git a/jmclient/jmclient/configure.py b/jmclient/jmclient/configure.py index d7e321a..48372b2 100644 --- a/jmclient/jmclient/configure.py +++ b/jmclient/jmclient/configure.py @@ -405,6 +405,11 @@ def validate_address(addr): return True, 'address validated' +_BURN_DESTINATION = "BURN" + +def is_burn_destination(destination): + return destination == _BURN_DESTINATION + def donation_address(reusable_donation_pubkey=None): #pragma: no cover #Donation code currently disabled, so not tested. if not reusable_donation_pubkey: diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index e6fe31b..c5f958a 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -5,13 +5,16 @@ import os import sys import time import numbers +from binascii import hexlify + from jmbase import get_log, jmprint -from .configure import jm_single, validate_address +from .configure import jm_single, validate_address, is_burn_destination from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ schedule_to_text -from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime +from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \ + FidelityBondMixin from jmbitcoin import deserialize, make_shuffled_tx, serialize, txhash,\ - amount_to_str + amount_to_str, mk_burn_script, bin_hash160 from jmbase.support import EXIT_SUCCESS log = get_log() @@ -20,7 +23,7 @@ Utility functions for tumbler-style takers; Currently re-used by CLI script tumbler.py and joinmarket-qt """ -def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, +def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, accept_callback=None, info_callback=None): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). @@ -41,24 +44,65 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, The txid if transaction is pushed, False otherwise """ #Sanity checks - assert validate_address(destaddr)[0] + assert validate_address(destination)[0] or is_burn_destination(destination) assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >=0 assert isinstance(wallet_service.wallet, BaseWallet) + if is_burn_destination(destination): + #Additional checks + if not isinstance(wallet_service.wallet, FidelityBondMixin): + log.error("Only fidelity bond wallets can burn coins") + return + if answeryes: + log.error("Burning coins not allowed without asking for confirmation") + return + if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH: + log.error("Burning coins only allowed from mixdepth " + str( + FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)) + return + if amount != 0: + log.error("Only sweeping allowed when burning coins, to keep the tx " + + "small. Tip: use the coin control feature to freeze utxos") + return + from pprint import pformat txtype = wallet_service.get_txtype() if amount == 0: utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error( - "There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.") + "There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.") return + total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) - fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) - outs = [{"address": destaddr, "value": total_inputs_val - fee_est}] + + if is_burn_destination(destination): + if len(utxos) > 1: + log.error("Only one input allowed when burning coins, to keep " + + "the tx small. Tip: use the coin control feature to freeze utxos") + return + address_type = FidelityBondMixin.BIP32_BURN_ID + index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type) + path = wallet_service.wallet.get_path(mixdepth, address_type, index) + privkey, engine = wallet_service.wallet._get_priv_from_path(path) + pubkey = engine.privkey_to_pubkey(privkey) + pubkeyhash = bin_hash160(pubkey) + + #size of burn output is slightly different from regular outputs + burn_script = mk_burn_script(pubkeyhash) #in hex + fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2) + + outs = [{"script": burn_script, "value": total_inputs_val - fee_est}] + destination = "BURNER OUTPUT embedding pubkey at " \ + + wallet_service.wallet.get_path_repr(path) \ + + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n" + else: + #regular send (non-burn) + fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) + outs = [{"address": destination, "value": total_inputs_val - fee_est}] else: #8 inputs to be conservative initial_fee_est = estimate_tx_fee(8,2, txtype=txtype) @@ -69,7 +113,7 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, fee_est = initial_fee_est total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount - outs = [{"value": amount, "address": destaddr}] + outs = [{"value": amount, "address": destination}] change_addr = wallet_service.get_internal_addr(mixdepth) outs.append({"value": changeval, "address": change_addr}) @@ -85,14 +129,14 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est - log.info("Sends: " + amount_to_str(actual_amount) + " to address: " + destaddr) + log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination) if not answeryes: if not accept_callback: if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return False else: - accepted = accept_callback(pformat(txsigned), destaddr, actual_amount, + accepted = accept_callback(pformat(txsigned), destination, actual_amount, fee_est) if not accepted: return False diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 3f1dae5..499cc90 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -68,7 +68,7 @@ class Mnemonic(MnemonicParent): def detect_language(cls, code): return "english" -def estimate_tx_fee(ins, outs, txtype='p2pkh'): +def estimate_tx_fee(ins, outs, txtype='p2pkh', extra_bytes=0): '''Returns an estimate of the number of satoshis required for a transaction with the given number of inputs and outputs, based on information from the blockchain interface. @@ -81,13 +81,14 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh'): raise ValueError("Estimated fee per kB greater than absurd value: " + \ str(absurd_fee) + ", quitting.") if txtype in ['p2pkh', 'p2shMofN']: - tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype) + tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype) + extra_bytes return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0)) elif txtype in ['p2wpkh', 'p2sh-p2wpkh']: witness_estimate, non_witness_estimate = btc.estimate_tx_size( ins, outs, txtype) + non_witness_estimate += extra_bytes return int(int(( - non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0)) + non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0)) else: raise NotImplementedError("Txtype: " + txtype + " not implemented.") @@ -416,7 +417,8 @@ class BaseWallet(object): """ if self.TYPE == TYPE_P2PKH: return 'p2pkh' - elif self.TYPE == TYPE_P2SH_P2WPKH: + elif self.TYPE in (TYPE_P2SH_P2WPKH, + TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS): return 'p2sh-p2wpkh' elif self.TYPE == TYPE_P2WPKH: return 'p2wpkh' diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 5fc2c54..587c7ef 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -12,8 +12,8 @@ from twisted.internet import reactor import pprint from jmclient import Taker, P2EPTaker, load_program_config, get_schedule,\ - JMClientProtocolFactory, start_reactor, validate_address, jm_single,\ - estimate_tx_fee, direct_send, WalletService,\ + JMClientProtocolFactory, start_reactor, validate_address, is_burn_destination, \ + jm_single, estimate_tx_fee, direct_send, WalletService,\ open_test_wallet_maybe, get_wallet_path, NO_ROUNDING, \ get_sendpayment_parser, get_max_cj_fee_values, check_regtest from twisted.python.log import startLogging @@ -81,8 +81,13 @@ def main(): destaddr = args[2] mixdepth = options.mixdepth addr_valid, errormsg = validate_address(destaddr) - if not addr_valid: + command_to_burn = (is_burn_destination(destaddr) and sweeping and + options.makercount == 0 and not options.p2ep) + if not addr_valid and not command_to_burn: jmprint('ERROR: Address invalid. ' + errormsg, "error") + if is_burn_destination(destaddr): + jmprint("The required options for burning coins are zero makers" + + " (-N 0), sweeping (amount = 0) and not using P2EP", "info") sys.exit(EXIT_ARGERROR) if sweeping == False and amount < DUST_THRESHOLD: jmprint('ERROR: Amount ' + btc.amount_to_str(amount) +