Browse Source

Add timelock fidelity bond wallet sync and display

Fidelity bond wallets are intended to be used when at a later date
using fidelity bonds to greatly increase joinmarket's resistance to
sybil attacks. This commit adds support for timelocked addresses.
It allows users to optionally create wallet which support such
addresses. The synchronization code is modified to also scan for
timelocked addresses. The keypairs of the timelocked addresses go in
the newly created 2nd address type, where before the zeroth index were
receive addresses and first index was change.

The locktime dates are fixed at the first of each month for the next 30
years. This means users dont need to remember any dates, and so just
their seed phrase and wallet type will still be enough to recover all
funds. Each keypair used for timelocking requires an additional 360
addresses to be scanned for, which isn't a problem for Bitcoin Core.
Fidelity bonds are only stored in the zeroth mixdepth, as they are not
used in repeated coinjoins so theres no point having them in multiple
mixdepths.

Timelocked addresses don't use the get_new_script() family of functions
because they all assume that one index equals one address, and that
therefore it's possible to ask for a "next" address. For timelocked
addresses knowing the index is not enough to know the address, the
timestamp must be known too. Also once one address made of
(index, timestamp) is used you mustn't use that index and pubkey again,
even though all the other timelocks for that index/pubkey are unused.
This is for privacy reasons, as its equivalent to address reuse.
master
chris-belcher 6 years ago
parent
commit
dc715c9481
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 2
      jmbitcoin/jmbitcoin/secp256k1_main.py
  2. 2
      jmclient/jmclient/__init__.py
  3. 60
      jmclient/jmclient/cryptoengine.py
  4. 215
      jmclient/jmclient/wallet.py
  5. 28
      jmclient/jmclient/wallet_service.py
  6. 131
      jmclient/jmclient/wallet_utils.py
  7. 3
      scripts/joinmarket-qt.py

2
jmbitcoin/jmbitcoin/secp256k1_main.py

@ -10,7 +10,7 @@ import coincurve as secp256k1
#Required only for PoDLE calculation:
N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f'}
BTC_P2PK_VBYTE = {"mainnet": b'\x00', "testnet": b'\x6f', "regtest": 100}
BTC_P2SH_VBYTE = {"mainnet": b'\x05', "testnet": b'\xc4'}
#Standard prefix for Bitcoin message signing.

2
jmclient/jmclient/__init__.py

@ -13,7 +13,7 @@ from .old_mnemonic import mn_decode, mn_encode
from .taker import Taker, P2EPTaker
from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportWalletMixin,
BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet,
SegwitWallet, SegwitLegacyWallet, UTXOManager,
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin, UTXOManager,
WALLET_IMPLEMENTATIONS, compute_tx_locktime)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage)

60
jmclient/jmclient/cryptoengine.py

@ -5,10 +5,11 @@ from collections import OrderedDict
import struct
import jmbitcoin as btc
from .configure import get_network
from .configure import get_network, jm_single
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N = range(4)
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS = range(6)
NET_MAINNET, NET_TESTNET = range(2)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET}
WIF_PREFIX_MAP = {'mainnet': b'\x80', 'testnet': b'\xef'}
@ -283,8 +284,61 @@ class BTC_P2WPKH(BTCEngine):
return btc.sign(btc.serialize(tx), index, privkey,
hashcode=hashcode, amount=amount, native=True)
class BTC_Timelocked_P2WSH(BTCEngine):
"""
In this class many instances of "privkey" or "pubkey" are actually tuples
of (privkey, timelock) or (pubkey, timelock)
"""
@classproperty
def VBYTE(cls):
#slight hack here, network can be either "mainnet" or "testnet"
#but we need to distinguish between actual testnet and regtest
if get_network() == "mainnet":
return btc.BTC_P2PK_VBYTE["mainnet"]
else:
if jm_single().config.get("BLOCKCHAIN", "blockchain_source")\
== "regtest":
return btc.BTC_P2PK_VBYTE["regtest"]
else:
assert get_network() == "testnet"
return btc.BTC_P2PK_VBYTE["testnet"]
@classmethod
def privkey_to_script(cls, privkey_locktime):
privkey, locktime = privkey_locktime
pub = cls.privkey_to_pubkey(privkey)
return cls.pubkey_to_script((pub, locktime))
@classmethod
def pubkey_to_script(cls, pubkey_locktime):
redeem_script = cls.pubkey_to_script_code(pubkey_locktime)
return btc.redeem_script_to_p2wsh_script(redeem_script)
@classmethod
def pubkey_to_script_code(cls, pubkey_locktime):
pubkey, locktime = pubkey_locktime
return btc.mk_freeze_script(pubkey, locktime)
@classmethod
def privkey_to_wif(cls, privkey_locktime):
priv, locktime = privkey_locktime
return btc.bin_to_b58check(priv, cls.WIF_PREFIX)
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise Exception("not implemented yet")
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")
ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH
TYPE_P2WPKH: BTC_P2WPKH,
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH
}

215
jmclient/jmclient/wallet.py

@ -7,6 +7,7 @@ import numbers
import random
from binascii import hexlify, unhexlify
from datetime import datetime
from calendar import timegm
from copy import deepcopy
from mnemonic import Mnemonic as MnemonicParent
from hashlib import sha256
@ -20,7 +21,8 @@ from .blockchaininterface import INF_HEIGHT
from .support import select_gradual, select_greedy, select_greediest, \
select
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\
TYPE_P2WPKH, ENGINES
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS,\
ENGINES
from .support import get_random_bytes
from . import mn_encode, mn_decode
import jmbitcoin as btc
@ -559,7 +561,7 @@ class BaseWallet(object):
returns:
tuple (mixdepth, type, index)
type is one of 0, 1, 'imported'
type is one of 0, 1, 'imported', 2, 3
"""
raise NotImplementedError()
@ -976,27 +978,28 @@ class BaseWallet(object):
def rewind_wallet_indices(self, used_indices, saved_indices):
for md in used_indices:
for address_type in (self.ADDRESS_TYPE_EXTERNAL,
self.ADDRESS_TYPE_INTERNAL):
for address_type in range(min(len(used_indices[md]), len(saved_indices[md]))):
index = max(used_indices[md][address_type],
saved_indices[md][address_type])
self.set_next_index(md, address_type, index, force=True)
def _get_default_used_indices(self):
return {x: [0, 0] for x in range(self.max_mixdepth + 1)}
def get_used_indices(self, addr_gen):
""" Returns a dict of max used indices for each branch in
the wallet, from the given addresses addr_gen, assuming
that they are known to the wallet.
"""
indices = {x: [0, 0] for x in range(self.max_mixdepth + 1)}
indices = self._get_default_used_indices()
for addr in addr_gen:
if not self.is_known_addr(addr):
continue
md, address_type, index = self.get_details(
self.addr_to_path(addr))
if address_type not in (self.ADDRESS_TYPE_EXTERNAL,
self.ADDRESS_TYPE_INTERNAL):
if address_type not in (self.BIP32_EXT_ID, self.BIP32_INT_ID,
FidelityBondMixin.BIP32_TIMELOCK_ID, FidelityBondMixin.BIP32_BURN_ID):
assert address_type == 'imported'
continue
indices[md][address_type] = max(indices[md][address_type], index + 1)
@ -1379,6 +1382,10 @@ class BIP32Wallet(BaseWallet):
def _derive_bip32_master_key(cls, seed):
return cls._ENGINE.derive_bip32_master_key(seed)
@classmethod
def _get_supported_address_types(cls):
return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID)
def get_script_from_path(self, path):
if not self._is_my_bip32_path(path):
raise WalletError("unable to get script for unknown key path")
@ -1387,11 +1394,14 @@ class BIP32Wallet(BaseWallet):
if not 0 <= md <= self.max_mixdepth:
raise WalletError("Mixdepth outside of wallet's range.")
assert address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID)
assert address_type in self._get_supported_address_types()
current_index = self._index_cache[md][address_type]
if index == current_index:
if index == current_index \
and address_type != FidelityBondMixin.BIP32_TIMELOCK_ID:
#special case for timelocked addresses because for them the
#concept of a "next address" cant be used
return self.get_new_script_override_disable(md, address_type)
priv, engine = self._get_priv_from_path(path)
@ -1481,9 +1491,16 @@ class BIP32Wallet(BaseWallet):
def get_new_script_override_disable(self, mixdepth, address_type):
# This is called by get_script_from_path and calls back there. We need to
# ensure all conditions match to avoid endless recursion.
index = self.get_index_cache_and_increment(mixdepth, address_type)
return self.get_script_and_update_map(mixdepth, address_type, index)
def get_index_cache_and_increment(self, mixdepth, address_type):
index = self._index_cache[mixdepth][address_type]
self._index_cache[mixdepth][address_type] += 1
path = self.get_path(mixdepth, address_type, index)
return index
def get_script_and_update_map(self, *args):
path = self.get_path(*args)
script = self.get_script_from_path(path)
self._script_map[script] = path
return script
@ -1571,8 +1588,6 @@ class LegacyWallet(ImportWalletMixin, BIP32Wallet):
def _get_bip32_base_path(self):
return self._key_ident, 0
class BIP32PurposedWallet(BIP32Wallet):
""" A class to encapsulate cases like
BIP44, 49 and 84, all of which are derivatives
@ -1595,6 +1610,174 @@ class BIP32PurposedWallet(BIP32Wallet):
return path[len(self._get_bip32_base_path())] - 2**31
class FidelityBondMixin(object):
BIP32_TIMELOCK_ID = 2
BIP32_BURN_ID = 3
"""
Explaination of time number
incrementing time numbers (0, 1, 2, 3, 4...
will produce datetimes
suitable for timelocking (1st january, 1st april, 1st july ....
this greatly reduces the number of possible timelock values
and is helpful for recovery of funds because the wallet can search
the only addresses corresponding to timenumbers which are far fewer
For example, if TIMENUMBER_UNIT = 2 (i.e. every time number is two months)
then there are 6 timelocks per year so just 600 possible
addresses per century per pubkey. Easily searchable when recovering a
wallet from seed phrase. Therefore the user doesn't need to store any
dates, the seed phrase is sufficent for recovery.
"""
#should be a factor of 12, the number of months in a year
TIMENUMBER_UNIT = 1
# all timelocks are 1st of the month at midnight
TIMELOCK_DAY_AND_SHORTER = (1, 0, 0, 0, 0)
TIMELOCK_EPOCH_YEAR = 2020
TIMELOCK_EPOCH_MONTH = 1 #january
MONTHS_IN_YEAR = 12
TIMELOCK_ERA_YEARS = 30
TIMENUMBERS_PER_PUBKEY = TIMELOCK_ERA_YEARS * MONTHS_IN_YEAR // TIMENUMBER_UNIT
"""
As each pubkey corresponds to hundreds of addresses, to reduce load the
given gap limit will be reduced by this factor. Also these timelocked
addresses are never handed out to takers so there wont be a problem of
having many used addresses with no transactions on them.
"""
TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR = 6
#only one mixdepth will have fidelity bonds in it
FIDELITY_BOND_MIXDEPTH = 0
@classmethod
def _time_number_to_timestamp(cls, timenumber):
"""
converts a time number to a unix timestamp
"""
year = cls.TIMELOCK_EPOCH_YEAR + (timenumber*cls.TIMENUMBER_UNIT) // cls.MONTHS_IN_YEAR
month = cls.TIMELOCK_EPOCH_MONTH + (timenumber*cls.TIMENUMBER_UNIT) % cls.MONTHS_IN_YEAR
return timegm(datetime(year, month, *cls.TIMELOCK_DAY_AND_SHORTER).timetuple())
@classmethod
def timestamp_to_time_number(cls, timestamp):
"""
converts a datetime object to a time number
"""
dt = datetime.utcfromtimestamp(timestamp)
if (dt.month - cls.TIMELOCK_EPOCH_MONTH) % cls.TIMENUMBER_UNIT != 0:
raise ValueError()
day_and_shorter_tuple = (dt.day, dt.hour, dt.minute, dt.second, dt.microsecond)
if day_and_shorter_tuple != cls.TIMELOCK_DAY_AND_SHORTER:
raise ValueError()
timenumber = (dt.year - cls.TIMELOCK_EPOCH_YEAR)*(cls.MONTHS_IN_YEAR //
cls.TIMENUMBER_UNIT) + ((dt.month - cls.TIMELOCK_EPOCH_MONTH) // cls.TIMENUMBER_UNIT)
if timenumber < 0 or timenumber > cls.TIMENUMBERS_PER_PUBKEY:
raise ValueError("datetime out of range")
return timenumber
@classmethod
def _is_timelocked_path(cls, path):
return len(path) > 4 and path[4] == cls.BIP32_TIMELOCK_ID
def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk):
if mpk.startswith(cls._BIP32_PUBKEY_PREFIX):
return mpk[len(cls._BIP32_PUBKEY_PREFIX):]
else:
return False
def _get_key_ident(self):
first_path = self.get_path(0, 0)
priv, engine = self._get_priv_from_path(first_path)
pub = engine.privkey_to_pubkey(priv)
return sha256(sha256(pub).digest()).digest()[:3]
def _populate_script_map(self):
super(FidelityBondMixin, self)._populate_script_map()
for md in self._index_cache:
address_type = self.BIP32_TIMELOCK_ID
for i in range(self._index_cache[md][address_type]):
for timenumber in range(self.TIMENUMBERS_PER_PUBKEY):
path = self.get_path(md, address_type, i, timenumber)
script = self.get_script_from_path(path)
self._script_map[script] = path
@classmethod
def _get_supported_address_types(cls):
return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID, cls.BIP32_TIMELOCK_ID, cls.BIP32_BURN_ID)
def _get_priv_from_path(self, path):
if self._is_timelocked_path(path):
key_path = path[:-1]
locktime = path[-1]
engine = ENGINES[TYPE_TIMELOCK_P2WSH]
privkey = engine.derive_bip32_privkey(self._master_key, key_path)
return (privkey, locktime), engine
else:
return super(FidelityBondMixin, self)._get_priv_from_path(path)
def get_path(self, mixdepth=None, address_type=None, index=None, timenumber=None):
if address_type == None or address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID,
self.BIP32_BURN_ID) or index == None:
return super(FidelityBondMixin, self).get_path(mixdepth, address_type, index)
elif address_type == self.BIP32_TIMELOCK_ID:
assert timenumber != None
timestamp = self._time_number_to_timestamp(timenumber)
return tuple(chain(self._get_bip32_export_path(mixdepth, address_type),
(index, timestamp)))
else:
assert 0
"""
We define a new serialization of the bip32 path to include bip65 timelock addresses
Previously it was m/44'/1'/0'/3/0
For a timelocked address it will be m/44'/1'/0'/3/0:13245432
The timelock will be in unix format and added to the end with a colon ":" character
refering to the pubkey plus the timelock value which together are needed to create the address
"""
def get_path_repr(self, path):
if self._is_timelocked_path(path) and len(path) == 7:
return super(FidelityBondMixin, self).get_path_repr(path[:-1]) +\
":" + str(path[-1])
else:
return super(FidelityBondMixin, self).get_path_repr(path)
def path_repr_to_path(self, pathstr):
if pathstr.find(":") == -1:
return super(FidelityBondMixin, self).path_repr_to_path(pathstr)
else:
colon_chunks = pathstr.split(":")
if len(colon_chunks) != 2:
raise WalletError("Not a valid wallet timelock path: {}".format(pathstr))
return tuple(chain(
super(FidelityBondMixin, self).path_repr_to_path(colon_chunks[0]),
(int(colon_chunks[1]),)
))
def get_details(self, path):
if self._is_timelocked_path(path):
return self._get_mixdepth_from_path(path), path[-3], path[-2]
else:
return super(FidelityBondMixin, self).get_details(path)
def _get_default_used_indices(self):
return {x: [0, 0, 0, 0] for x in range(self.max_mixdepth + 1)}
def get_script(self, mixdepth, address_type, index, timenumber=None):
path = self.get_path(mixdepth, address_type, index, timenumber)
return self.get_script_from_path(path)
def get_addr(self, mixdepth, address_type, index, timenumber=None):
script = self.get_script(mixdepth, address_type, index, timenumber)
return self.script_to_addr(script)
#class FidelityBondWatchonlyWallet(ImportWalletMixin, BIP39WalletMixin, FidelityBondMixin):
class BIP49Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 49
_ENGINE = ENGINES[TYPE_P2SH_P2WPKH]
@ -1609,8 +1792,12 @@ class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, BIP49Wallet):
class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, BIP84Wallet):
TYPE = TYPE_P2WPKH
class SegwitLegacyWalletFidelityBonds(FidelityBondMixin, SegwitLegacyWallet):
TYPE = TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
WALLET_IMPLEMENTATIONS = {
LegacyWallet.TYPE: LegacyWallet,
SegwitLegacyWallet.TYPE: SegwitLegacyWallet,
SegwitWallet.TYPE: SegwitWallet
SegwitWallet.TYPE: SegwitWallet,
SegwitLegacyWalletFidelityBonds.TYPE: SegwitLegacyWalletFidelityBonds
}

28
jmclient/jmclient/wallet_service.py

@ -15,6 +15,7 @@ from jmclient.configure import jm_single, get_log
from jmclient.output import fmt_tx_data
from jmclient.blockchaininterface import (INF_HEIGHT, BitcoinCoreInterface,
BitcoinCoreNoHistoryInterface)
from jmclient.wallet import FidelityBondMixin
from jmbase.support import jmprint, EXIT_SUCCESS
"""Wallet service
@ -735,6 +736,22 @@ class WalletService(Service):
for path in self.yield_imported_paths(md):
addresses.add(self.get_address_from_path(path))
if isinstance(self.wallet, FidelityBondMixin):
md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
saved_indices[md] += [0]
next_unused = self.get_next_unused_index(md, address_type)
for index in range(next_unused):
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
addresses.add(self.get_addr(md, address_type, index, timenumber))
for index in range(self.gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR):
index += next_unused
assert self.wallet.get_index_cache_and_increment(md, address_type) == index
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
self.wallet.get_script_and_update_map(md, address_type, index, timenumber)
addresses.add(self.get_addr(md, address_type, index, timenumber))
self.wallet.set_next_index(md, address_type, next_unused)
return addresses, saved_indices
def collect_addresses_gap(self, gap_limit=None):
@ -748,6 +765,17 @@ class WalletService(Service):
addresses.add(self.get_new_addr(md, address_type))
self.set_next_index(md, address_type, old_next)
if isinstance(self.wallet, FidelityBondMixin):
md = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
old_next = self.get_next_unused_index(md, address_type)
for ii in range(gap_limit // FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR):
index = self.wallet.get_index_cache_and_increment(md, address_type)
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
self.wallet.get_script_and_update_map(md, address_type, index, timenumber)
addresses.add(self.get_addr(md, address_type, index, timenumber))
self.set_next_index(md, address_type, old_next)
return addresses
def get_external_addr(self, mixdepth):

131
jmclient/jmclient/wallet_utils.py

@ -5,6 +5,7 @@ import sys
import sqlite3
import binascii
from datetime import datetime
from calendar import timegm
from optparse import OptionParser
from numbers import Integral
from collections import Counter
@ -12,12 +13,13 @@ from itertools import islice
from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle,
jm_single, BitcoinCoreInterface, WalletError,
VolatileStorage, StoragePasswordError, is_segwit_mode, SegwitLegacyWallet,
LegacyWallet, SegwitWallet, is_native_segwit_mode, load_program_config,
add_base_options, check_regtest)
LegacyWallet, SegwitWallet, FidelityBondMixin, is_native_segwit_mode,
load_program_config, add_base_options, check_regtest)
from jmclient.wallet_service import WalletService
from jmbase.support import get_password, jmprint, EXIT_FAILURE, EXIT_ARGERROR
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
from .output import fmt_utxo
import jmbitcoin as btc
@ -42,8 +44,9 @@ def get_wallettool_parser():
'(dumpprivkey) Export a single private key, specify an hd wallet path\n'
'(signmessage) Sign a message with the private key from an address in \n'
'the wallet. Use with -H and specify an HD wallet path for the address.\n'
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]',
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.\n'
'(gettimelockaddress) Obtain a timelocked address. Argument is locktime value as yyyy-mm. For example `2021-03`')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]',
description=description)
add_base_options(parser)
parser.add_option('-p',
@ -167,7 +170,9 @@ class WalletViewEntry(WalletViewBase):
super(WalletViewEntry, self).__init__(wallet_path_repr, serclass=serclass,
custom_separator=custom_separator)
self.account = account
assert address_type in [0, 1, -1]
assert address_type in [SegwitWallet.BIP32_EXT_ID,
SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID,
FidelityBondMixin.BIP32_BURN_ID]
self.address_type = address_type
assert isinstance(aindex, Integral)
assert aindex >= 0
@ -219,7 +224,9 @@ class WalletViewBranch(WalletViewBase):
serclass=serclass,
custom_separator=custom_separator)
self.account = account
assert address_type in [0, 1, -1]
assert address_type in [SegwitWallet.BIP32_EXT_ID,
SegwitWallet.BIP32_INT_ID, -1, FidelityBondMixin.BIP32_TIMELOCK_ID,
FidelityBondMixin.BIP32_BURN_ID]
self.address_type = address_type
if xpub:
assert xpub.startswith('xpub') or xpub.startswith('tpub')
@ -441,11 +448,42 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k, addr,
[balance, balance], priv=privkey, used=used))
wallet_service.set_next_index(m, address_type, unused_index)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
xpub=xpub_key))
if m == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
isinstance(wallet_service.wallet, FidelityBondMixin):
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
unused_index = wallet_service.get_next_unused_index(m, address_type)
timelocked_gaplimit = (wallet_service.wallet.gap_limit
// FidelityBondMixin.TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR)
entrylist = []
for k in range(unused_index + timelocked_gaplimit):
for timenumber in range(FidelityBondMixin.TIMENUMBERS_PER_PUBKEY):
path = wallet_service.get_path(m, address_type, k, timenumber)
addr = wallet_service.get_address_from_path(path)
timelock = datetime.utcfromtimestamp(path[-1])
balance = sum([utxodata["value"] for utxo, utxodata in
iteritems(utxos[m]) if path == utxodata["path"]])
status = timelock.strftime("%Y-%m-%d") + " [" + (
"LOCKED" if datetime.now() < timelock else "UNLOCKED") + "]"
privkey = ""
if showprivkey:
privkey = wallet_service.get_wif_path(path)
if displayall or balance > 0:
entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k,
addr, [balance, balance], priv=privkey, used=status))
#TODO fidelity bond master pub key is this, although it should include burner too
xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
xpub=xpub_key))
ipb = get_imported_privkey_branch(wallet_service, m, showprivkey)
if ipb:
branchlist.append(ipb)
@ -499,11 +537,19 @@ def cli_get_mnemonic_extension():
"info")
return input("Enter mnemonic extension: ")
def cli_do_support_fidelity_bonds():
uin = input("Would you like this wallet to support fidelity bonds? "
"write 'n' if you don't know what this is (y/n): ")
if len(uin) == 0 or uin[0] != 'y':
jmprint("Not supporting fidelity bonds", "info")
return False
else:
return True
def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
display_seed_callback, enter_seed_callback, enter_wallet_password_callback,
enter_wallet_file_name_callback, enter_if_use_seed_extension,
enter_seed_extension_callback, mixdepth=DEFAULT_MIXDEPTH):
enter_seed_extension_callback, enter_do_support_fidelity_bonds, mixdepth=DEFAULT_MIXDEPTH):
entropy = None
mnemonic_extension = None
if method == "generate":
@ -537,7 +583,10 @@ def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
wallet_name = default_wallet_name
wallet_path = os.path.join(walletspath, wallet_name)
wallet = create_wallet(wallet_path, password, mixdepth,
support_fidelity_bonds = enter_do_support_fidelity_bonds()
wallet_cls = get_wallet_cls(get_configured_wallet_type(support_fidelity_bonds))
wallet = create_wallet(wallet_path, password, mixdepth, wallet_cls,
entropy=entropy,
entropy_extension=mnemonic_extension)
mnemonic, mnext = wallet.get_mnemonic_words()
@ -555,7 +604,7 @@ def wallet_generate_recover(method, walletspath,
default_wallet_name, cli_display_user_words, cli_user_mnemonic_entry,
cli_get_wallet_passphrase_check, cli_get_wallet_file_name,
cli_do_use_mnemonic_extension, cli_get_mnemonic_extension,
mixdepth=mixdepth)
cli_do_support_fidelity_bonds, mixdepth=mixdepth)
entropy = None
if method == 'recover':
@ -1035,29 +1084,54 @@ def wallet_freezeutxo(wallet, md, display_callback=None, info_callback=None):
.format(fmt_utxo((txid, index))))
return "Done"
def get_wallet_type():
def wallet_gettimelockaddress(wallet_service, locktime_string):
if not isinstance(wallet_service.wallet, FidelityBondMixin):
jmprint("Error: not a fidelity bond wallet", "error")
return ""
m = FidelityBondMixin.FIDELITY_BOND_MIXDEPTH
address_type = FidelityBondMixin.BIP32_TIMELOCK_ID
index = wallet_service.get_next_unused_index(m, address_type)
lock_datetime = datetime.strptime(locktime_string, "%Y-%m")
timenumber = FidelityBondMixin.timestamp_to_time_number(timegm(
lock_datetime.timetuple()))
path = wallet_service.get_path(m, address_type, index, timenumber)
jmprint("path = " + wallet_service.get_path_repr(path), "info")
jmprint("Coins sent to this address will be not be spendable until "
+ lock_datetime.strftime("%B %Y") + ". Full date: "
+ str(lock_datetime))
addr = wallet_service.get_address_from_path(path)
return addr
def get_configured_wallet_type(support_fidelity_bonds):
configured_type = TYPE_P2PKH
if is_segwit_mode():
if is_native_segwit_mode():
return TYPE_P2WPKH
return TYPE_P2SH_P2WPKH
return TYPE_P2PKH
configured_type = TYPE_P2WPKH
else:
configured_type = TYPE_P2SH_P2WPKH
if not support_fidelity_bonds:
return configured_type
def get_wallet_cls(wtype=None):
if wtype is None:
wtype = get_wallet_type()
if configured_type == TYPE_P2SH_P2WPKH:
return TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
else:
raise ValueError("Fidelity bonds not supported with the configured "
"options of segwit and native. Edit joinmarket.cfg")
def get_wallet_cls(wtype):
cls = WALLET_IMPLEMENTATIONS.get(wtype)
if not cls:
raise WalletError("No wallet implementation found for type {}."
"".format(wtype))
return cls
def create_wallet(path, password, max_mixdepth, wallet_cls=None, **kwargs):
def create_wallet(path, password, max_mixdepth, wallet_cls, **kwargs):
storage = Storage(path, password, create=True)
wallet_cls = wallet_cls or get_wallet_cls()
wallet_cls.initialize(storage, get_network(), max_mixdepth=max_mixdepth,
**kwargs)
storage.save()
@ -1195,11 +1269,12 @@ def wallet_tool_main(wallet_root_path):
wallet_root_path = os.path.join(jm_single().datadir, wallet_root_path)
noseed_methods = ['generate', 'recover']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos', 'freeze']
'history', 'showutxos', 'freeze', 'gettimelockaddress']
methods.extend(noseed_methods)
noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage']
readonly_methods = ['display', 'displayall', 'summary', 'showseed',
'history', 'showutxos', 'dumpprivkey', 'signmessage']
'history', 'showutxos', 'dumpprivkey', 'signmessage',
'gettimelockaddress']
if len(args) < 1:
parser.error('Needs a wallet file or method')
@ -1273,9 +1348,17 @@ def wallet_tool_main(wallet_root_path):
map_key_type(options.key_type))
return "Key import completed."
elif method == "signmessage":
if len(args) < 3:
jmprint('Must provide message to sign', "error")
sys.exit(EXIT_ARGERROR)
return wallet_signmessage(wallet_service, options.hd_path, args[2])
elif method == "freeze":
return wallet_freezeutxo(wallet_service, options.mixdepth)
elif method == "gettimelockaddress":
if len(args) < 3:
jmprint('Must have locktime value yyyy-mm. For example 2021-03', "error")
sys.exit(EXIT_ARGERROR)
return wallet_gettimelockaddress(wallet_service, args[2])
else:
parser.error("Unknown wallet-tool method: " + method)
sys.exit(EXIT_ARGERROR)

3
scripts/joinmarket-qt.py

@ -1808,7 +1808,8 @@ class JMMainWindow(QMainWindow):
enter_wallet_password_callback=self.getPassword,
enter_wallet_file_name_callback=self.getWalletFileName,
enter_if_use_seed_extension=self.promptUseMnemonicExtension,
enter_seed_extension_callback=self.promptInputMnemonicExtension)
enter_seed_extension_callback=self.promptInputMnemonicExtension,
enter_do_support_fidelity_bonds=lambda: False)
if not success:
JMQtMessageBox(self, "Failed to create new wallet file.",

Loading…
Cancel
Save