Skip to content
This repository has been archived by the owner on May 13, 2022. It is now read-only.

Using the JoinMarket internal wallet

David Bakin edited this page Oct 20, 2021 · 27 revisions

For many JoinMarket applications, like the tumbler, yield generator and patient send payments, an internal wallet is required. The wallet is hierarchical and deterministic, it can be entirely recovered from a single seed. Use --help on the command line for all options.

Contents

Creating a Wallet

Run wallet-tool.py with the 'generate' method. Write down the 12 word seed on paper.

$ python wallet-tool.py generate
Write down this wallet recovery seed

upon upon release grace led brain skill cost back clothes bump trouble

Enter wallet encryption passphrase: 
Reenter wallet encryption passphrase: 
Input wallet file name (default: wallet.json): 
saved to wallet.json
$

Funding Wallet and Displaying Balance

Run wallet-tool.py with the name of your wallet file as first argument. Bitcoins should be sent to empty external addresses (sometimes known as receive addresses.) Note for Joinmarket 0.2 : read the instructions here before funding the wallet. Empty internal addresses (also known as change addresses) are hidden by default. Using 'displayall' as the second argument will show all addresses in wallet, including empty used ones.

$ python wallet-tool.py wallet.json
Enter wallet decryption passphrase: 
2016-12-30 21:57:12,262 [MainThread  ] [INFO ]  downloading wallet history
2016-12-30 21:57:22,018 [MainThread  ] [DEBUG]  blockr sync_unspent took 3.74924492836sec
mixing depth 0 m/0/0/
 external addresses m/0/0/0 xpub6Btm9LzaCFjWozatijp1sQ68a9eTAzfv2MS2SGei8NTyhCMc8UkqENm8xagcukVtZwDecgeBzn5zA4CKqq887Pp5u7F4ZG9S1DaDXH64dQT
  m/0/0/0/000 1JPFmg1RSa2gtzcsow9fBjwdvWPsxcP3eX  new 0.00000000 btc
  m/0/0/0/001 1AaCpeMit59ExfSvP3M3bTnMkhXgecSPeY  new 0.00000000 btc 
  m/0/0/0/002 1NmDrVbtk6kfAYbBVo7Miv8eCYHHefZkjs  new 0.00000000 btc 
  m/0/0/0/003 1NKitLXm7FdgbHuENvFXRCxVH32N5XXMQ5  new 0.00000000 btc 
  m/0/0/0/004 1EwkvF8SrHLh17LKCNQ9w4u4HY2akuzhx3  new 0.00000000 btc 
  m/0/0/0/005 1HkHyB8DbZBNvZYwyAutgedaBSsrNUDt7G  new 0.00000000 btc 
 internal addresses m/0/0/1 
for mixdepth=0 balance=0.00000000btc
mixing depth 1 m/0/1/
 external addresses m/0/1/0 xpub6CbyR17RGdX3ZRnijcH9hH5TT1ErABfR66UzLWqGPRo6DJBo9xAKQAZrMHe57H2GbS5Qas33QLGkqbMy3KsZC9WBJQi73EfQCzmhHfTPkwg
  m/0/1/0/000 1LQw8K7V2KQePFVscLKiiH1NU2v6KzwdhW  new 0.00000000 btc 
  m/0/1/0/001 1EcZ7w1EEb1UK1qWYT6FMLsbRoizFCfAZ7  new 0.00000000 btc 
  m/0/1/0/002 1CV7L2b23sEYNhnu35MP9gbzPAD3j3ofgc  new 0.00000000 btc 
  m/0/1/0/003 1DMYRugQNJZRQPcAPAYBcE1p9u15VFTkD9  new 0.00000000 btc 
  m/0/1/0/004 1CCnPgGhecXmFz8DrB3Wew9kHT1En53Lq   new 0.00000000 btc 
  m/0/1/0/005 1LuwwyEv86BV4miaKVScsFxE4rrKngVt8F  new 0.00000000 btc 
 internal addresses m/0/1/1 
for mixdepth=1 balance=0.00000000btc
$

The BIP32 xpub keys of each external branch are there to help users avoid address re-use.

Recovering a Wallet from mnemonic

In the event of loss of encrypted wallet file, use the 12 word seed to recover by running wallet-tool.py with 'recover' as first argument.

$ python wallet-tool.py recover
Input 12 word recovery seed: upon upon release grace led brain skill cost back clothes bump trouble
5e65114c8db9a6555a95d925959c1b18
Enter wallet encryption passphrase: 
Reenter wallet encryption passphrase: 
Input wallet file name (default: wallet.json): 
saved to wallet.json
$

Help! I recovered but some of my money is missing

Try increasing the gap limit up from its default of 6. This likely happened because you were running a yield-generator bot and somebody (poorly) attempted to DOS you.

$ python wallet-tool.py -g 50 my-wallet-file.json

Another possible cause is you were running the tumbler script and it was stopped halfway through, with your coins currently at mixing depth 8. In that case increase the maximum mixing depth

$ python wallet-tool.py -m 15 my-wallet-file.json

Recovering a Wallet's Mnemonic Phrase

The showseed command will display the mnemonic for an existing wallet file, in case you've forgotten it. It is highly recommended to keep a written backup of this phrase, lest you forget it!

$ python wallet-tool.py wallet.json showseed

Recovering Private Keys

In the event of bitcoins stuck in the wallet, or simply wanting to use the features of a different wallet software, use the -p command line flag to print out private keys, which can be imported into another wallet. However, if you are running a full node as the backend to joinmarket, do not use the bitcoin core wallet that is used during that time because joinmarket stores the public keys of it's wallet in the bitcoin core wallet as watch-only addresses in an account named after the joinmarket wallet. If you import a private keys with a different account name in the same bitcoin core wallet, joinmarket will crash when it tries to sync on startup.

$ python wallet-tool.py -p wallet.json
Enter wallet decryption passphrase: 
[2015/04/23 02:02:22] downloading wallet history
[2015/04/23 02:02:34] blockr sync_unspent took 2.29012322426sec
mixing depth 0 m/0/0/
 external addresses m/0/0/0/
  m/0/0/0/000 1JPFmg1RSa2gtzcsow9fBjwdvWPsxcP3eX  new 0.00000000 btc Ky1bG7ba51yUE8rfvTTTUyZr1z4aKJEdfAbUo5iDLoTQK8HSJZNR
  m/0/0/0/001 1AaCpeMit59ExfSvP3M3bTnMkhXgecSPeY  new 0.00000000 btc L3eKPoMrHbfK96Lp3jZNJgxzUyqceJ4JwD9dZ8hqLSxp2hrHwnCb
  m/0/0/0/002 1NmDrVbtk6kfAYbBVo7Miv8eCYHHefZkjs  new 0.00000000 btc KyUZem3yazQT1hw4tVPP3Z6HG2QJh2hStkYQwpT33PzmhcQFPFPg
  m/0/0/0/003 1NKitLXm7FdgbHuENvFXRCxVH32N5XXMQ5  new 0.00000000 btc L3wPhBrxMFZraqBsRqt54uijDDvTtqejgSHJfQqD7pyDbZcq7HGv
  m/0/0/0/004 1EwkvF8SrHLh17LKCNQ9w4u4HY2akuzhx3  new 0.00000000 btc Kx95MLcfSgEG3jdXZRS9uTF7aVXMGtGoVF26s4p8g6rMNPrAx5ok
  m/0/0/0/005 1HkHyB8DbZBNvZYwyAutgedaBSsrNUDt7G  new 0.00000000 btc L1QezwwFenmEG3kxmrVBBywdNWHVee3PcFqC27CJb1F4ZvaotQcT
 internal addresses m/0/0/1/
for mixdepth=0 balance=0.00000000btc
mixing depth 1 m/0/1/
 external addresses m/0/1/0/
  m/0/1/0/000 1LQw8K7V2KQePFVscLKiiH1NU2v6KzwdhW  new 0.00000000 btc KyH8CqoqeAthf9HUxGdGQfrkZJoTtneMwbbrYqfiAn5fwSqfkXSz
  m/0/1/0/001 1EcZ7w1EEb1UK1qWYT6FMLsbRoizFCfAZ7  new 0.00000000 btc KyZY1p2MHWjVFxk2uePtbaUi64tWDA43AjjRskCMW49bgKypELjG
  m/0/1/0/002 1CV7L2b23sEYNhnu35MP9gbzPAD3j3ofgc  new 0.00000000 btc L1pgR97neuXewxhtU58XyWBdByeg6cNPqLjWcwAdKT612GDoCTTC
  m/0/1/0/003 1DMYRugQNJZRQPcAPAYBcE1p9u15VFTkD9  new 0.00000000 btc L2ePgHdi5gw31djaLusnWesdMtFoLuJr4ag1sHXc6X9meCZT9ycc
  m/0/1/0/004 1CCnPgGhecXmFz8DrB3Wew9kHT1En53Lq   new 0.00000000 btc KyppEz25HyegaAycjwrbUJ4F2B2Cic2BL5GYDtiSrwGqTXoyyrmJ
  m/0/1/0/005 1LuwwyEv86BV4miaKVScsFxE4rrKngVt8F  new 0.00000000 btc L4ecJDUNcK8jv7NNynKUbv6XeCbCWkG78LAvLxv6mVcnGmLdCn1X
 internal addresses m/0/1/1/
for mixdepth=1 balance=0.00000000btc
$

The above method still requires synchronizing the JoinMarket wallet. In the case where this isn't possible, individual private keys can still be exported.

$ python wallet-tool.py -H m/0/0/0/000 wallet.json dumpprivkey
Enter wallet decryption passphrase: 
Ky1bG7ba51yUE8rfvTTTUyZr1z4aKJEdfAbUo5iDLoTQK8HSJZNR

$ python wallet-tool.py -H m/0/0/0/001 wallet.json dumpprivkey
Enter wallet decryption passphrase: 
L3eKPoMrHbfK96Lp3jZNJgxzUyqceJ4JwD9dZ8hqLSxp2hrHwnCb

Remember that if you want to import/sweep a private key into Electrum, you have to supply the correct prefix for the address type (typically p2wpkh-p2sh: for 0.3.0+ wallets).

Wallet History

The wallet transaction history can be displayed. Requires running with a Bitcoin full node. Prints a summary for every transaction. If you have numpy/scipy installed it also calculates the effective interest rate you achieved as if your yield-generator was a savings account.

$ python wallet-tool.py wallet.json history

 tx# timestamp type amount/btc balance-change/btc balance/btc coinjoin-n total-fees utxo-count mixdepth-from mixdepth-to
   0 2016-04-20 21:37 deposit     0.15000000 +0.15000000 0.15000000  # #             1  #  0
   1 2016-04-20 22:22 cj internal 0.02115585 +0.00006341 0.15006341  3 #             2  0  1
   2 2016-04-20 23:27 cj internal 0.15046475 +0.00021085 0.15027426  4 #             3  0  1
   3 2016-04-21 23:45 cj internal 0.01209051 +0.00003159 0.15030585  5 #             4  0  1
   4 2016-04-21 00:07 cj internal 0.03120432 +0.00006307 0.15036892  3 #             5  1  2
   5 2016-04-21 00:07 cj internal 0.05538475 +0.00017932 0.15054824  4 #             6  1  2
     2016-04-21 18:55 best block is 000000000000000005009c707b2427224c784c6224a5c44ee449d93b727739e7
continuously compounded equivalent annual interest rate = 0.459494243045 %
(as if yield generator was a bank account)
$ 

You can create a csv file for opening with spreadsheet software:

$ python wallet-tool.py --csv wallet.json history > history.csv

Importing External Private Keys

Individual private keys can be imported into JoinMarket wallets by using the 'importprivkey' method. Multiple private keys are imported by separating them with commas or spaces. Use the '-M' flag to control which mixing depth the private keys are imported into.

Be warned that handling raw private keys like this is dangerous. You should read the warnings and understand the non-intuitive behaviour before using. With this and this page. It is best to avoid importing private keys if you can.

$ python wallet-tool.py -M 0 example.json importprivkey
2016-04-05 20:45:38,632 [MainThread  ] [DEBUG]  hello joinmarket
Enter wallet decryption passphrase: 
WARNING: This imported key will not be recoverable with your 12 word mnemonic seed. Make sure you have backups.
WARNING: Handling of raw ECDSA bitcoin private keys can lead to non-intuitive behaviour and loss of funds.
  Recommended instead is to use the 'sweep' feature of sendpayment.py 
Enter private key(s) to import: KzHJDZrSmmwkZKdLNS8L91qGsL9By6b48deaZRExBg4vAiyiBE7V Kxo3mHpUcx6KcLsyGTETh3ZJHEeU73tNCwYM1Yk7MMoTcW4jZ7Mi L3SdjpTu8tGdtht74wwsUX37bqGmr44AoyvZSqvrhTieN2GhbP7e
Private key(s) successfully imported
$

These keys now appear in the wallet in that mixing depth.

$ python wallet-tool.py -m 1 -p example.json
2016-04-05 21:07:19,671 [MainThread  ] [DEBUG]  hello joinmarket
Enter wallet decryption passphrase: 
2016-04-05 21:07:21,396 [MainThread  ] [DEBUG]  downloading wallet history
2016-04-05 21:07:25,117 [MainThread  ] [DEBUG]  blockr sync_unspent took 1.75068998337sec
mixing depth 0 m/0/0/
 external addresses m/0/0/0/
  m/0/0/0/000 1JPFmg1RSa2gtzcsow9fBjwdvWPsxcP3eX used 0.00000000 btc Ky1bG7ba51yUE8rfvTTTUyZr1z4aKJEdfAbUo5iDLoTQK8HSJZNR
  m/0/0/0/001 1AaCpeMit59ExfSvP3M3bTnMkhXgecSPeY  new 0.00000000 btc L3eKPoMrHbfK96Lp3jZNJgxzUyqceJ4JwD9dZ8hqLSxp2hrHwnCb
  m/0/0/0/002 1NmDrVbtk6kfAYbBVo7Miv8eCYHHefZkjs  new 0.00000000 btc KyUZem3yazQT1hw4tVPP3Z6HG2QJh2hStkYQwpT33PzmhcQFPFPg
  m/0/0/0/003 1NKitLXm7FdgbHuENvFXRCxVH32N5XXMQ5  new 0.00000000 btc L3wPhBrxMFZraqBsRqt54uijDDvTtqejgSHJfQqD7pyDbZcq7HGv
  m/0/0/0/004 1EwkvF8SrHLh17LKCNQ9w4u4HY2akuzhx3  new 0.00000000 btc Kx95MLcfSgEG3jdXZRS9uTF7aVXMGtGoVF26s4p8g6rMNPrAx5ok
  m/0/0/0/005 1HkHyB8DbZBNvZYwyAutgedaBSsrNUDt7G  new 0.00000000 btc L1QezwwFenmEG3kxmrVBBywdNWHVee3PcFqC27CJb1F4ZvaotQcT
  m/0/0/0/006 1126wt4tPVGshr4H81BeNh6MCDJ3oexEJ8  new 0.00000000 btc Kyci24thdfwfLvYFkznPtQyZGxvE37r3VxQYvCxYPBy2WKU3LJui
 internal addresses m/0/0/1/
 import addresses
             19kFHnEXySWKU4M4d7tBMMwrv4VBU9adUf empty 0.00000000 btc KzHJDZrSmmwkZKdLNS8L91qGsL9By6b48deaZRExBg4vAiyiBE7V
             1M7wX7oBdYwru7XWLMddZMZRV1uMcqtcJT empty 0.00000000 btc Kxo3mHpUcx6KcLsyGTETh3ZJHEeU73tNCwYM1Yk7MMoTcW4jZ7Mi
             1HnhFzdZU43Y375aBFF8zdzTjBHokhuuLK empty 0.00000000 btc L3SdjpTu8tGdtht74wwsUX37bqGmr44AoyvZSqvrhTieN2GhbP7e
for mixdepth=0 balance=0.00000000btc
total balance = 0.00000000btc
$

Importing Non-Interactively

By default, importing private keys is done interactively by the script asking for them via stdin, This avoids the key appearing in the .bash_history file or ps process. Despite this if you want to import private keys non-interactively this is possible.

$ python wallet-tool.py wallet.json importprivkey cli-import-WARNING-DANGEROUS-DONT-USE-WITHOUT-UNDERSTANDING KzHJDZrSmmwkZKdLNS8L91qGsL9By6b48deaZRExBg4vAiyiBE7V

What is the BIP32 wallet structure

The Mixing Depth Concept

The point of JoinMarket is to improve privacy. Merged transaction inputs are damaging to privacy because they provide evidence of common ownership. Each mixing depth is a different identity, coins are never merged in the same transaction across mixing depths, but may be merged within mixing depths. Coins move between mixing depths through coinjoins. A change output stays in the same mixing depth. This prevents the situation where a change output is merged with a coinjoin output in a later transaction, which would render the coinjoin easily unmixable.

An example of the different identities being used is to not leak a lower limit of your wallet balance. Imagine if someone pays you $10 and sees it combined with $1 million, they could deduce you own at least that much. If instead those two payments go to different mixing levels then this analysis becomes harder. As coins move up the mixing levels via coinjoin, their identity becomes more uncertain. To introduce more uncertainty, have the coins separated by more mixing levels. E.G. A coin in level 0 and a second coin with level 1 will be merged with one set of coinjoins between them, the second coin at level 5 will be merged with 5 sets of coinjoins.

BIP32 Structure

m - generated from seed
m/0 - joinmarket root
m/0/n - nth mixing depth
m/0/n/0/k - kth external address, for mixing depth n
m/0/n/1/k - kth internal address, for mixing depth n

What is the Gap Limit?

With a deterministic wallet you create a sequence of bitcoin addresses and private keys from an initial seed. This list is the same every time it's generated, it means the entire wallet can be backed up by saving only the initial seed.

You can create as many addresses as you like, but not all of them will appear the blockchain. For instance I might create one especially for you to give me 1,000,000 BTC. That is (alas!) probably not going to be used so will likely never appear on the blockchain.

When you are starting JoinMarket it does not know which is the last address used. So you start at the beginning and see what is on the blockchain. Then you look for the next one in the sequence. The gap limit is how many misses you accept before you give up and stop looking. The same concept is used in other deterministic wallets like Electrum or Armoury.