Browse Source

Add support for burning coins with sendpayment

Users burn coins by passing "BURN" as an address to sendpayment
master
chris-belcher 6 years ago
parent
commit
2271ce05d7
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 7
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  2. 5
      jmclient/jmclient/__init__.py
  3. 2
      jmclient/jmclient/cli_options.py
  4. 5
      jmclient/jmclient/configure.py
  5. 66
      jmclient/jmclient/taker_utils.py
  6. 10
      jmclient/jmclient/wallet.py
  7. 11
      scripts/sendpayment.py

7
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):

5
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

2
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 '
+

5
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:

66
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

10
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'

11
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) +

Loading…
Cancel
Save