You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

456 lines
18 KiB

from __future__ import print_function
import json
import os
import pprint
import sys
import datetime
from decimal import Decimal
from ConfigParser import NoSectionError
from getpass import getpass
import btc
from jmclient.slowaes import encryptData, decryptData
from jmclient.blockchaininterface import BitcoinCoreInterface, RegtestBitcoinCoreInterface
from jmclient.configure import jm_single, get_network, get_p2pk_vbyte
from jmbase.support import get_log
from jmclient.support import select_gradual, select_greedy,select_greediest, select
log = get_log()
JM_WALLET_P2PKH = "00"
JM_WALLET_SW_P2SH_P2WPKH = "01"
class WalletError(Exception):
pass
def estimate_tx_fee(ins, outs, txtype='p2pkh'):
'''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.
'''
fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb(
jm_single().config.getint("POLICY","tx_fees"))
absurd_fee = jm_single().config.getint("POLICY", "absurd_fee_per_kb")
if fee_per_kb > absurd_fee:
#This error is considered critical; for safety reasons, shut down.
raise ValueError("Estimated fee per kB greater than absurd value: " + \
str(absurd_fee) + ", quitting.")
if txtype=='p2pkh':
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype)
log.debug("Estimated transaction size: "+str(tx_estimated_bytes))
return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
elif txtype=='p2sh-p2wpkh':
witness_estimate, non_witness_estimate = btc.estimate_tx_size(
ins, outs, 'p2sh-p2wpkh')
return int(int((
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
def create_wallet_file(pwd, seed):
password_key = btc.bin_dbl_sha256(pwd)
encrypted_seed = encryptData(password_key, seed.decode('hex'))
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
return json.dumps({'creator': 'joinmarket project',
'creation_time': timestamp,
'encrypted_seed': encrypted_seed.encode('hex'),
'network': get_network()})
class AbstractWallet(object):
"""
Abstract wallet for use with JoinMarket
Mostly written with Wallet in mind, the default JoinMarket HD wallet
"""
def __init__(self):
self.max_mix_depth = 0
self.unspent = None
self.utxo_selector = select
try:
config = jm_single().config
if config.get("POLICY", "merge_algorithm") == "gradual":
self.utxo_selector = select_gradual
elif config.get("POLICY", "merge_algorithm") == "greedy":
self.utxo_selector = select_greedy
elif config.get("POLICY", "merge_algorithm") == "greediest":
self.utxo_selector = select_greediest
elif config.get("POLICY", "merge_algorithm") != "default":
raise Exception("Unknown merge algorithm")
except NoSectionError:
pass
def get_key_from_addr(self, addr):
return None
def get_utxos_by_mixdepth(self):
return None
def get_external_addr(self, mixing_depth):
"""
Return an address suitable for external distribution, including funding
the wallet from other sources, or receiving payments or donations.
JoinMarket will never generate these addresses for internal use.
"""
return None
def get_internal_addr(self, mixing_depth):
"""
Return an address for internal usage, as change addresses and when
participating in transactions initiated by other parties.
"""
return None
def update_cache_index(self):
pass
def remove_old_utxos(self, tx):
pass
def add_new_utxos(self, tx, txid):
pass
def select_utxos(self, mixdepth, amount, utxo_filter=None):
if utxo_filter is None:
utxo_filter = []
utxo_list = self.get_utxos_by_mixdepth()[mixdepth]
unspent = [{'utxo': utxo,
'value': addrval['value']}
for utxo, addrval in utxo_list.iteritems() if utxo not in utxo_filter]
inputs = self.utxo_selector(unspent, amount)
log.debug('for mixdepth={} amount={} selected:'.format(
mixdepth, amount))
log.debug(pprint.pformat(inputs))
return dict([(i['utxo'], {'value': i['value'],
'address': utxo_list[i['utxo']]['address']})
for i in inputs])
def get_balance_by_mixdepth(self, verbose=True):
mix_balance = {}
for m in range(self.max_mix_depth):
mix_balance[m] = 0
for mixdepth, utxos in self.get_utxos_by_mixdepth(verbose).iteritems():
mix_balance[mixdepth] = sum(
[addrval['value'] for addrval in utxos.values()])
return mix_balance
class Wallet(AbstractWallet):
def __init__(self,
seedarg,
pwd,
max_mix_depth=2,
gaplimit=6,
extend_mixdepth=False,
storepassword=False,
wallet_dir=None):
super(Wallet, self).__init__()
self.vflag = JM_WALLET_P2PKH
self.max_mix_depth = max_mix_depth
self.storepassword = storepassword
# key is address, value is (mixdepth, forchange, index) if mixdepth =
# -1 it's an imported key and index refers to imported_privkeys
self.addr_cache = {}
self.unspent = {}
self.spent_utxos = []
self.imported_privkeys = {}
self.seed = self.read_wallet_file_data(seedarg, pwd, wallet_dir=wallet_dir)
if not self.seed:
raise WalletError("Failed to decrypt wallet")
if extend_mixdepth and len(self.index_cache) > max_mix_depth:
self.max_mix_depth = len(self.index_cache)
self.gaplimit = gaplimit
master = btc.bip32_master_key(self.seed, (btc.MAINNET_PRIVATE if
get_network() == 'mainnet' else btc.TESTNET_PRIVATE))
m_0 = btc.bip32_ckd(master, 0)
mixing_depth_keys = [btc.bip32_ckd(m_0, c)
for c in range(self.max_mix_depth)]
self.keys = [(btc.bip32_ckd(m, 0), btc.bip32_ckd(m, 1))
for m in mixing_depth_keys]
# self.index = [[0, 0]]*max_mix_depth
self.index = []
for i in range(self.max_mix_depth):
self.index.append([0, 0])
def get_txtype(self):
"""Return string defining wallet type
for purposes of transaction size estimates
"""
return 'p2pkh'
def sign(self, tx, i, priv, amount):
"""Sign a transaction for pushing
onto the network. The amount field
is not used in this case (p2pkh)
"""
return btc.sign(tx, i, priv)
def script_to_address(self, script):
"""Return the address for a given output script,
which will be p2pkh for the default Wallet object,
and reading the correct network byte from the config.
"""
return btc.script_to_address(script, get_p2pk_vbyte())
def read_wallet_file_data(self, filename, pwd=None, wallet_dir=None):
self.path = None
wallet_dir = wallet_dir if wallet_dir else 'wallets'
self.index_cache = [[0, 0]] * self.max_mix_depth
path = os.path.join(wallet_dir, filename)
if not os.path.isfile(path):
if get_network() == 'testnet':
log.debug('filename interpreted as seed, only available in '
'testnet because this probably has lower entropy')
return filename
else:
raise IOError('wallet file not found')
if not pwd:
log.info("Password required for non-testnet seed wallet")
return None
self.path = path
fd = open(path, 'r')
walletfile = fd.read()
fd.close()
walletdata = json.loads(walletfile)
if walletdata['network'] != get_network():
raise ValueError('wallet network(%s) does not match '
'joinmarket configured network(%s)' % (
walletdata['network'], get_network()))
if 'index_cache' in walletdata:
self.index_cache = walletdata['index_cache']
if self.max_mix_depth > len(self.index_cache):
#This can happen e.g. in tumbler when we need more mixdepths
#than currently exist. Since we have no info for those extra
#depths, we must default to (0,0) (but sync should find used
#adddresses).
self.index_cache += [[0,0]] * (
self.max_mix_depth - len(self.index_cache))
password_key = btc.bin_dbl_sha256(pwd)
encrypted_seed = walletdata['encrypted_seed']
try:
decrypted_seed = decryptData(
password_key,
encrypted_seed.decode('hex')).encode('hex')
# there is a small probability of getting a valid PKCS7
# padding by chance from a wrong password; sanity check the
# seed length
if len(decrypted_seed) != 32:
raise ValueError
except ValueError:
log.info('Incorrect password')
return None
if self.storepassword:
self.password_key = password_key
self.walletdata = walletdata
if 'imported_keys' in walletdata:
for epk_m in walletdata['imported_keys']:
privkey = decryptData(
password_key,
epk_m['encrypted_privkey'].decode( 'hex')).encode('hex')
#Imported keys are stored as 32 byte strings only, so the
#second version below is sufficient, really.
if len(privkey) != 64:
raise Exception(
"Unexpected privkey format; already compressed?:" + privkey)
privkey += "01"
if epk_m['mixdepth'] not in self.imported_privkeys:
self.imported_privkeys[epk_m['mixdepth']] = []
self.addr_cache[btc.privtoaddr(
privkey, magicbyte=get_p2pk_vbyte())] = (epk_m['mixdepth'], -1,
len(self.imported_privkeys[epk_m['mixdepth']]))
self.imported_privkeys[epk_m['mixdepth']].append(privkey)
return decrypted_seed
def update_cache_index(self):
if not self.path:
return
if not os.path.isfile(self.path):
return
fd = open(self.path, 'r')
walletfile = fd.read()
fd.close()
walletdata = json.loads(walletfile)
walletdata['index_cache'] = self.index
walletfile = json.dumps(walletdata)
fd = open(self.path, 'w')
fd.write(walletfile)
fd.close()
def get_key(self, mixing_depth, forchange, i):
return btc.bip32_extract_key(btc.bip32_ckd(
self.keys[mixing_depth][forchange], i))
def get_addr(self, mixing_depth, forchange, i):
return btc.privtoaddr(
self.get_key(mixing_depth, forchange, i), magicbyte=get_p2pk_vbyte())
def get_new_addr(self, mixing_depth, forchange, import_required=False):
index = self.index[mixing_depth]
addr = self.get_addr(mixing_depth, forchange, index[forchange])
self.addr_cache[addr] = (mixing_depth, forchange, index[forchange])
index[forchange] += 1
# self.update_cache_index()
bc_interface = jm_single().bc_interface
if isinstance(bc_interface, BitcoinCoreInterface) or isinstance(
bc_interface, RegtestBitcoinCoreInterface) or import_required:
# do not import in the middle of sync_wallet()
if bc_interface.wallet_synced:
if bc_interface.rpc('getaccount', [addr]) == '':
log.debug('importing address ' + addr + ' to bitcoin core')
bc_interface.rpc(
'importaddress',
[addr, bc_interface.get_wallet_name(self), False])
return addr
def get_external_addr(self, mixing_depth):
return self.get_new_addr(mixing_depth, 0)
def get_internal_addr(self, mixing_depth):
return self.get_new_addr(mixing_depth, 1)
def get_key_from_addr(self, addr):
if addr not in self.addr_cache:
return None
ac = self.addr_cache[addr]
if ac[1] >= 0:
return self.get_key(*ac)
else:
return self.imported_privkeys[ac[0]][ac[2]]
def remove_old_utxos(self, tx):
removed_utxos = {}
for ins in tx['ins']:
utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index'])
if utxo not in self.unspent:
continue
removed_utxos[utxo] = self.unspent[utxo]
del self.unspent[utxo]
log.debug('removed utxos, wallet now is \n' + pprint.pformat(
self.get_utxos_by_mixdepth()))
self.spent_utxos += removed_utxos.keys()
return removed_utxos
def get_vbyte(self):
return get_p2pk_vbyte()
def add_new_utxos(self, tx, txid):
added_utxos = {}
for index, outs in enumerate(tx['outs']):
addr = btc.script_to_address(outs['script'], self.get_vbyte())
if addr not in self.addr_cache:
continue
addrdict = {'address': addr, 'value': outs['value']}
utxo = txid + ':' + str(index)
added_utxos[utxo] = addrdict
self.unspent[utxo] = addrdict
log.debug('added utxos, wallet now is \n' + pprint.pformat(
self.get_utxos_by_mixdepth()))
return added_utxos
def get_utxos_by_mixdepth(self, verbose=True):
"""
returns a list of utxos sorted by different mix levels
"""
mix_utxo_list = {}
for m in range(self.max_mix_depth):
mix_utxo_list[m] = {}
for utxo, addrvalue in self.unspent.iteritems():
mixdepth = self.addr_cache[addrvalue['address']][0]
if mixdepth not in mix_utxo_list:
mix_utxo_list[mixdepth] = {}
mix_utxo_list[mixdepth][utxo] = addrvalue
if verbose:
log.debug('get_utxos_by_mixdepth = \n' + pprint.pformat(mix_utxo_list))
return mix_utxo_list
class SegwitWallet(Wallet):
def __init__(self, seedarg, max_mix_depth=2, gaplimit=6,
extend_mixdepth=False, storepassword=False):
super(SegwitWallet, self).__init__(seedarg, max_mix_depth, gaplimit,
extend_mixdepth, storepassword)
self.vflag = JM_WALLET_SW_P2SH_P2WPKH
def get_vbyte(self):
return get_p2sh_vbyte()
def get_txtype(self):
"""Return string defining wallet type
for purposes of transaction size estimates
"""
return 'p2sh-p2wpkh'
def get_addr(self, mixing_depth, forchange, i):
"""Construct a p2sh-p2wpkh style address for the
keypair corresponding to mixing depth mixing_depth,
branch forchange and index i
"""
pub = btc.privtopub(self.get_key(mixing_depth, forchange, i))
return btc.pubkey_to_p2sh_p2wpkh_address(pub, magicbyte=self.get_vbyte())
def script_to_address(self, script):
"""Return the address for a given output script,
which will be p2sh-p2wpkh for the segwit (currently).
The underlying witness is however invisible at this layer;
so it's just a p2sh address.
"""
return btc.script_to_address(script, get_p2sh_vbyte())
def sign(self, tx, i, priv, amount):
"""Sign a transaction; the amount field
triggers the segwit style signing.
"""
log.debug("About to sign for this amount: " + str(amount))
return btc.sign(tx, i, priv, amount=amount)
class BitcoinCoreWallet(AbstractWallet): #pragma: no cover
def __init__(self, fromaccount):
super(BitcoinCoreWallet, self).__init__()
if not isinstance(jm_single().bc_interface,
BitcoinCoreInterface):
raise RuntimeError('Bitcoin Core wallet can only be used when '
'blockchain interface is BitcoinCoreInterface')
self.fromaccount = fromaccount
self.max_mix_depth = 1
def get_key_from_addr(self, addr):
self.ensure_wallet_unlocked()
wifkey = jm_single().bc_interface.rpc('dumpprivkey', [addr])
return btc.from_wif_privkey(wifkey, vbyte=get_p2pk_vbyte())
def get_utxos_by_mixdepth(self):
unspent_list = jm_single().bc_interface.rpc('listunspent', [])
result = {0: {}}
for u in unspent_list:
if not u['spendable']:
continue
if self.fromaccount and (
('account' not in u) or u['account'] !=
self.fromaccount):
continue
result[0][u['txid'] + ':' + str(u['vout'])] = {
'address': u['address'],
'value': int(Decimal(str(u['amount'])) * Decimal('1e8'))}
return result
def get_internal_addr(self, mixing_depth):
return jm_single().bc_interface.rpc('getrawchangeaddress', [])
@staticmethod
def ensure_wallet_unlocked():
wallet_info = jm_single().bc_interface.rpc('getwalletinfo', [])
if 'unlocked_until' in wallet_info and wallet_info[
'unlocked_until'] <= 0:
while True:
password = getpass(
'Enter passphrase to unlock wallet: ')
if password == '':
raise RuntimeError('Aborting wallet unlock')
try:
# TODO cleanly unlock wallet after use, not with arbitrary timeout
jm_single().bc_interface.rpc(
'walletpassphrase', [password, 10])
break
except jm_single().JsonRpcError as exc:
if exc.code != -14:
raise exc
# Wrong passphrase, try again.